diff --git a/.gitignore b/.gitignore index f5a5e0b5..e1f5df3a 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,7 @@ testem.log # System Files .DS_Store Thumbs.db + +# testing folder from cli +my-first-vulcan-project +test-vulcan-project \ No newline at end of file diff --git a/nx.json b/nx.json index af41aa5b..d4ac6441 100644 --- a/nx.json +++ b/nx.json @@ -11,7 +11,7 @@ "default": { "runner": "nx/tasks-runners/default", "options": { - "cacheableOperations": ["build", "lint", "test", "e2e"] + "cacheableOperations": ["build", "lint", "test", "e2e", "tsc"] } } } diff --git a/package.json b/package.json index 5b9fa5b2..e33525ff 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,10 @@ "dependencies": { "@koa/cors": "^3.3.0", "class-validator": "^0.13.2", + "commander": "^9.4.0", "dayjs": "^1.11.2", "glob": "^8.0.1", + "inquirer": "^8.0.0", "inversify": "^6.0.1", "joi": "^17.6.0", "js-yaml": "^4.1.0", @@ -22,6 +24,7 @@ "lodash": "^4.17.21", "nunjucks": "^3.2.3", "openapi3-ts": "^2.0.2", + "ora": "^5.4.1", "reflect-metadata": "^0.1.13", "tslib": "^2.3.0", "tslog": "^3.3.3", @@ -37,6 +40,7 @@ "@nrwl/workspace": "14.0.3", "@types/from2": "^2.3.1", "@types/glob": "^7.2.0", + "@types/inquirer": "^8.0.0", "@types/jest": "27.4.1", "@types/js-yaml": "^4.0.5", "@types/koa": "^2.13.4", diff --git a/packages/build/package.json b/packages/build/package.json index 9dc2884d..8fda415d 100644 --- a/packages/build/package.json +++ b/packages/build/package.json @@ -1,5 +1,27 @@ { "name": "@vulcan-sql/build", - "version": "0.0.1", - "type": "commonjs" + "description": "Vulcan package for building projects", + "version": "0.1.0-alpha.1", + "type": "commonjs", + "publishConfig": { + "access": "public" + }, + "keywords": [ + "vulcan", + "vulcan-sql", + "data", + "sql", + "database", + "data-warehouse", + "data-lake", + "api-builder" + ], + "repository": { + "type": "git", + "url": "https://github.com/Canner/vulcan.git" + }, + "license": "MIT", + "peerDependencies": { + "@vulcan-sql/core": "0.1.0-alpha.1" + } } diff --git a/packages/build/project.json b/packages/build/project.json index 9d03ff4a..dbc39b24 100644 --- a/packages/build/project.json +++ b/packages/build/project.json @@ -3,14 +3,33 @@ "sourceRoot": "packages/build/src", "targets": { "build": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "command": "yarn ts-node ./tools/scripts/replaceAlias.ts build" + }, + "dependsOn": [ + { + "projects": "self", + "target": "tsc" + } + ] + }, + "tsc": { "executor": "@nrwl/js:tsc", "outputs": ["{options.outputPath}"], "options": { "outputPath": "dist/packages/build", "main": "packages/build/src/index.ts", "tsConfig": "packages/build/tsconfig.lib.json", - "assets": ["packages/build/*.md"] - } + "assets": ["packages/build/*.md"], + "buildableProjectDepsInPackageJsonType": "dependencies" + }, + "dependsOn": [ + { + "projects": "dependencies", + "target": "build" + } + ] }, "lint": { "executor": "@nrwl/linter:eslint", @@ -26,6 +45,19 @@ "jestConfig": "packages/build/jest.config.ts", "passWithNoTests": true } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "command": "node ../../../tools/scripts/publish.mjs {args.tag}", + "cwd": "dist/packages/build" + }, + "dependsOn": [ + { + "projects": "self", + "target": "build" + } + ] } }, "tags": [] diff --git a/packages/build/src/lib/vulcanBuilder.ts b/packages/build/src/lib/vulcanBuilder.ts index 303601bf..fb6686b1 100644 --- a/packages/build/src/lib/vulcanBuilder.ts +++ b/packages/build/src/lib/vulcanBuilder.ts @@ -9,9 +9,14 @@ import { import { DocumentGenerator } from './document-generator'; export class VulcanBuilder { - public async build(options: IBuildOptions) { + private options: IBuildOptions; + constructor(options: IBuildOptions) { + this.options = options; + } + + public async build() { const container = new Container(); - await container.load(options); + await container.load(this.options); const schemaParser = container.get(TYPES.SchemaParser); const templateEngine = container.get( CORE_TYPES.TemplateEngine diff --git a/packages/build/test/builder/builder.spec.ts b/packages/build/test/builder/builder.spec.ts index 1e6a8b5d..d264e608 100644 --- a/packages/build/test/builder/builder.spec.ts +++ b/packages/build/test/builder/builder.spec.ts @@ -13,7 +13,6 @@ import { it('Builder.build should work', async () => { // Arrange - const builder = new VulcanBuilder(); const options: IBuildOptions = { 'schema-parser': { reader: SchemaReaderType.LocalFile, @@ -34,7 +33,8 @@ it('Builder.build should work', async () => { }, extensions: {}, }; + const builder = new VulcanBuilder(options); // Act, Assert - await expect(builder.build(options)).resolves.not.toThrow(); + await expect(builder.build()).resolves.not.toThrow(); }); diff --git a/packages/cli/.eslintrc.json b/packages/cli/.eslintrc.json new file mode 100644 index 00000000..9d9c0db5 --- /dev/null +++ b/packages/cli/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/packages/cli/README.md b/packages/cli/README.md new file mode 100644 index 00000000..06e32266 --- /dev/null +++ b/packages/cli/README.md @@ -0,0 +1,12 @@ +# cli + +This library was generated with [Nx](https://nx.dev). + +## Building + +Run `nx build cli` to build the library. + +## Running unit tests + +Run `nx test cli` to execute the unit tests via [Jest](https://jestjs.io). + diff --git a/packages/cli/jest.config.ts b/packages/cli/jest.config.ts new file mode 100644 index 00000000..e941f150 --- /dev/null +++ b/packages/cli/jest.config.ts @@ -0,0 +1,14 @@ +module.exports = { + displayName: 'cli', + preset: '../../jest.preset.ts', + globals: { + 'ts-jest': { + tsconfig: '/tsconfig.spec.json', + }, + }, + transform: { + '^.+\\.[tj]s$': 'ts-jest', + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../coverage/packages/cli', +}; diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 00000000..04277a04 --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,27 @@ +{ + "name": "@vulcan-sql/cli", + "description": "CLI tools for Vulcan", + "version": "0.1.0-alpha.1", + "type": "commonjs", + "bin": { + "vulcan": "./src/index.js" + }, + "publishConfig": { + "access": "public" + }, + "keywords": [ + "vulcan", + "vulcan-sql", + "data", + "sql", + "database", + "data-warehouse", + "data-lake", + "api-builder" + ], + "repository": { + "type": "git", + "url": "https://github.com/Canner/vulcan.git" + }, + "license": "MIT" +} diff --git a/packages/cli/project.json b/packages/cli/project.json new file mode 100644 index 00000000..5093672a --- /dev/null +++ b/packages/cli/project.json @@ -0,0 +1,91 @@ +{ + "root": "packages/cli", + "sourceRoot": "packages/cli/src", + "targets": { + "build": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "yarn ts-node ./tools/scripts/replaceAlias.ts cli", + "forwardAllArgs": false + }, + { + "command": "chmod +x dist/packages/cli/src/index.js", + "forwardAllArgs": false + } + ] + }, + "dependsOn": [ + { + "projects": "self", + "target": "tsc" + } + ] + }, + "tsc": { + "executor": "@nrwl/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/packages/cli", + "main": "packages/cli/src/index.ts", + "tsConfig": "packages/cli/tsconfig.lib.json", + "assets": ["packages/cli/*.md", "packages/cli/src/schemas/**/*.*"], + "buildableProjectDepsInPackageJsonType": "dependencies" + }, + "dependsOn": [ + { + "projects": "dependencies", + "target": "build" + } + ] + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "command": "node ../../../tools/scripts/publish.mjs {args.tag}", + "cwd": "dist/packages/cli" + }, + "dependsOn": [ + { + "projects": "self", + "target": "build" + } + ] + }, + "lint": { + "executor": "@nrwl/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["packages/cli/**/*.ts"] + } + }, + "test": { + "executor": "@nrwl/jest:jest", + "outputs": ["coverage/packages/cli"], + "options": { + "jestConfig": "packages/cli/jest.config.ts", + "passWithNoTests": true + } + }, + "install": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "npm i -g . && echo done.", + "forwardAllArgs": false + } + ], + "cwd": "dist/packages/cli" + }, + "dependsOn": [ + { + "projects": "self", + "target": "build" + } + ] + } + }, + "tags": [] +} diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts new file mode 100644 index 00000000..e25fa5f8 --- /dev/null +++ b/packages/cli/src/cli.ts @@ -0,0 +1,61 @@ +import { program } from 'commander'; +import { handleInit, handleStart, handleVersion } from './commands'; +import { handleBuild } from './commands/build'; +import { handleServe } from './commands/serve'; + +program.exitOverride(); + +program + .command('version') + .description('show the version of CLI and Vulcan packages') + .action(async () => { + await handleVersion(); + }); + +program + .command('init') + .description('create a new Vulcan project') + .option('-p --project-name ', 'specify project name') + .action(async (options) => { + await handleInit(options); + }); + +program + .command('build') + .description('build Vulcan project') + .option( + '-c --config ', + 'path to Vulcan config file', + './vulcan.yaml' + ) + .action(async (options) => { + await handleBuild(options); + }); + +program + .command('serve') + .description('serve Vulcan project') + .option( + '-c --config ', + 'path to Vulcan config file', + './vulcan.yaml' + ) + .option('-p --port ', 'server port', '3000') + .action(async (options) => { + await handleServe(options); + }); + +program + .command('start') + .description('build and serve Vulcan project') + .option( + '-c --config ', + 'path to Vulcan config file', + './vulcan.yaml' + ) + .option('-p --port ', 'server port', '3000') + .action(async (options) => { + await handleStart(options); + }); + +export { program }; diff --git a/packages/cli/src/commands/build.ts b/packages/cli/src/commands/build.ts new file mode 100644 index 00000000..f0756f2d --- /dev/null +++ b/packages/cli/src/commands/build.ts @@ -0,0 +1,44 @@ +import * as jsYAML from 'js-yaml'; +import { promises as fs } from 'fs'; +import * as path from 'path'; +import * as ora from 'ora'; +import { localModulePath } from '../utils'; + +export interface BuildCommandOptions { + config: string; +} + +const defaultOptions: BuildCommandOptions = { + config: './vulcan.yaml', +}; + +export const buildVulcan = async (options: BuildCommandOptions) => { + const configPath = path.resolve(process.cwd(), options.config); + const config: any = jsYAML.load(await fs.readFile(configPath, 'utf-8')); + + // Import dependencies. We use dynamic import here to import dependencies at runtime. + const { VulcanBuilder } = await import(localModulePath('@vulcan-sql/build')); + + // Build project + const spinner = ora('Building project...').start(); + try { + const builder = new VulcanBuilder(config); + await builder.build(); + spinner.succeed('Built successfully.'); + } catch (e) { + spinner.fail(); + throw e; + } finally { + spinner.stop(); + } +}; + +export const handleBuild = async ( + options: Partial +): Promise => { + options = { + ...defaultOptions, + ...options, + }; + await buildVulcan(options as BuildCommandOptions); +}; diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts new file mode 100644 index 00000000..f9f1cf5a --- /dev/null +++ b/packages/cli/src/commands/index.ts @@ -0,0 +1,3 @@ +export * from './init'; +export * from './start'; +export * from './version'; diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts new file mode 100644 index 00000000..5dc123cb --- /dev/null +++ b/packages/cli/src/commands/init.ts @@ -0,0 +1,157 @@ +import * as inquirer from 'inquirer'; +import { promises as fs } from 'fs'; +import * as path from 'path'; +import { version } from '../../package.json'; +import * as ora from 'ora'; +import { exec } from 'child_process'; +import * as nunjucks from 'nunjucks'; +import * as glob from 'glob'; +import { logger } from '../utils'; + +interface InitCommandOptions { + projectName: string; + version: string; +} + +const validators: Record< + string, + { + regex: RegExp; + errorMessage: string; + } +> = { + projectName: { + regex: /^[a-zA-Z0-9_-]+$/, + errorMessage: `Project name should contain only letters, numbers, or dashes.`, + }, +}; + +const validateAnswer = (name: string) => (input: string) => { + const validator = validators[name]; + if (!validator.regex.test(input)) { + throw new Error(validator.errorMessage); + } + return true; +}; + +export const createProject = async ( + options: InitCommandOptions +): Promise => { + const projectPath = path.resolve(process.cwd(), options.projectName); + await fs.mkdir(projectPath); + const existedFiles = await fs.readdir(projectPath); + if (existedFiles.length > 0) + throw new Error(`Path ${projectPath} is not empty`); + + const installSpinner = ora('Creating project...').start(); + try { + await fs.writeFile( + path.resolve(projectPath, 'package.json'), + JSON.stringify( + { + name: options.projectName, + dependencies: { + '@vulcan-sql/core': options.version, + '@vulcan-sql/build': options.version, + '@vulcan-sql/serve': options.version, + }, + }, + null, + 2 + ), + 'utf-8' + ); + installSpinner.succeed('Project has been created.'); + installSpinner.start('Installing dependencies...'); + await execAndWait(`npm install --silent`, projectPath); + installSpinner.succeed(`Dependencies have been installed.`); + installSpinner.start('Writing initial content...'); + await addInitFiles(projectPath, options); + installSpinner.succeed('Initial done.'); + + logger.info( + `Project has been initialized. Run "cd ${projectPath} && vulcan start" to start the server.` + ); + } catch (e) { + installSpinner.fail(); + throw e; + } finally { + installSpinner.stop(); + } +}; + +export const execAndWait = async (command: string, cwd: string) => { + return new Promise((resolve, reject) => { + exec(command, { cwd }, (error, _, stderr) => { + if (error) { + reject(stderr); + } + resolve(); + }); + }); +}; + +const addInitFiles = async ( + projectPath: string, + options: InitCommandOptions +) => { + const files = await listFiles( + path.resolve(__dirname, '..', 'schemas', 'init', '**/*.*') + ); + for (const file of files) { + const relativePath = path.relative( + path.resolve(__dirname, '..', 'schemas', 'init'), + file + ); + let templateContent = await fs.readFile(file, 'utf8'); + if (path.extname(file) === '.yaml') { + // Only render yaml files because sql files have some template scripts which are used by Vulcan. + templateContent = nunjucks.renderString(templateContent, { + options, + }); + } + const targetPath = path.resolve(projectPath, relativePath); + const dir = path.dirname(targetPath); + await fs.mkdir(dir, { recursive: true }); + await fs.writeFile(targetPath, templateContent, 'utf8'); + } +}; + +const listFiles = async (pattern: string) => { + return new Promise((resolve, reject) => { + // Windows use backslash when using path.resolve, e.g. C:\\Users\\xxxxx\\cli. but glob accepts only forward ward slash + // Glob: https://github.com/isaacs/node-glob#windows + // Path separator: https://nodejs.org/api/path.html#pathsep + const normalizedPattern = pattern.split(path.sep).join('/'); + glob(normalizedPattern, (err, files) => { + if (err) return reject(err); + return resolve(files); + }); + }); +}; + +export const handleInit = async ( + options: Partial +): Promise => { + const question = []; + + if (!options.projectName) { + question.push({ + type: 'input', + name: 'projectName', + message: 'Project name:', + default: 'my-first-vulcan-project', + validate: validateAnswer('projectName'), + }); + } else { + validateAnswer('projectName')(options.projectName); + } + + options = { + ...{ version }, + ...options, + ...(await inquirer.prompt(question)), + }; + + await createProject(options as InitCommandOptions); +}; diff --git a/packages/cli/src/commands/serve.ts b/packages/cli/src/commands/serve.ts new file mode 100644 index 00000000..b372b89b --- /dev/null +++ b/packages/cli/src/commands/serve.ts @@ -0,0 +1,43 @@ +import * as jsYAML from 'js-yaml'; +import { promises as fs } from 'fs'; +import * as path from 'path'; +import { addShutdownJob, localModulePath, logger } from '../utils'; + +export interface ServeCommandOptions { + config: string; + port: number; +} + +const defaultOptions: ServeCommandOptions = { + config: './vulcan.yaml', + port: 3000, +}; + +export const serveVulcan = async (options: ServeCommandOptions) => { + const configPath = path.resolve(process.cwd(), options.config); + const config: any = jsYAML.load(await fs.readFile(configPath, 'utf-8')); + + // Import dependencies. We use dynamic import here to import dependencies at runtime. + const { VulcanServer } = await import(localModulePath('@vulcan-sql/serve')); + + // Start server + logger.info(`Starting server...`); + const server = new VulcanServer(config); + await server.start(options.port); + logger.info(`Server is listening at port ${options.port}.`); + addShutdownJob(async () => { + logger.info(`Stopping server...`); + await server.close(); + logger.info(`Server stopped`); + }); +}; + +export const handleServe = async ( + options: Partial +): Promise => { + options = { + ...defaultOptions, + ...options, + }; + await serveVulcan(options as ServeCommandOptions); +}; diff --git a/packages/cli/src/commands/start.ts b/packages/cli/src/commands/start.ts new file mode 100644 index 00000000..84f16ac3 --- /dev/null +++ b/packages/cli/src/commands/start.ts @@ -0,0 +1,9 @@ +import { BuildCommandOptions, handleBuild } from './build'; +import { handleServe, ServeCommandOptions } from './serve'; + +export const handleStart = async ( + options: Partial +): Promise => { + await handleBuild(options); + await handleServe(options); +}; diff --git a/packages/cli/src/commands/version.ts b/packages/cli/src/commands/version.ts new file mode 100644 index 00000000..a2814c22 --- /dev/null +++ b/packages/cli/src/commands/version.ts @@ -0,0 +1,35 @@ +import * as path from 'path'; +import { promises as fs } from 'fs'; +import { logger } from '../utils'; + +const cliVersion = async () => { + return JSON.parse( + await fs.readFile( + path.resolve(__dirname, '..', '..', 'package.json'), + 'utf8' + ) + ).version; +}; + +const localModuleVersion = async (moduleName: string): Promise => { + try { + const packageJson = path.resolve( + process.cwd(), + 'node_modules', + moduleName, + 'package.json' + ); + return JSON.parse(await fs.readFile(packageJson, 'utf8')).version; + } catch { + return '-'; + } +}; + +export const handleVersion = async (): Promise => { + logger.info(`cli version: ${await cliVersion()}`); + for (const pkg of ['core', 'build', 'serve']) { + logger.info( + `${pkg} version: ${await localModuleVersion(`@vulcan-sql/${pkg}`)}` + ); + } +}; diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts new file mode 100644 index 00000000..caa9967b --- /dev/null +++ b/packages/cli/src/index.ts @@ -0,0 +1,15 @@ +#!/usr/bin/env node + +import { logger } from './utils'; +import { program } from './cli'; + +(async () => { + try { + await program.parseAsync(); + } catch (e: any) { + // Ignore error with exit code = 0, e.g. commander.helpDisplayed error + if (e?.exitCode === 0) return; + logger.prettyError(e, true, false, false); + process.exit(e?.exitCode ?? 1); + } +})(); diff --git a/packages/cli/src/schemas/init/sqls/user.sql b/packages/cli/src/schemas/init/sqls/user.sql new file mode 100644 index 00000000..6f9bab8a --- /dev/null +++ b/packages/cli/src/schemas/init/sqls/user.sql @@ -0,0 +1,4 @@ +select * +from public.users +where id = "{{ context.params.id }}" +limit 1; \ No newline at end of file diff --git a/packages/cli/src/schemas/init/sqls/user.yaml b/packages/cli/src/schemas/init/sqls/user.yaml new file mode 100644 index 00000000..6880eaa3 --- /dev/null +++ b/packages/cli/src/schemas/init/sqls/user.yaml @@ -0,0 +1,8 @@ +url: /user/:id +request: + - fieldName: id + fieldIn: path + description: user id + validators: + - uuid + - required diff --git a/packages/cli/src/schemas/init/vulcan.yaml b/packages/cli/src/schemas/init/vulcan.yaml new file mode 100644 index 00000000..4ed27194 --- /dev/null +++ b/packages/cli/src/schemas/init/vulcan.yaml @@ -0,0 +1,23 @@ +name: '{{ options.projectName }}' +description: A starter Vulcan project +version: 0.1.0-alpha.1 +template: + provider: LocalFile + # Path to .sql files + folderPath: sqls + codeLoader: InMemory +artifact: + provider: LocalFile + serializer: JSON + # Path to build result + filePath: result.json +schema-parser: + reader: LocalFile + # Path to .yaml files + folderPath: sqls +document-generator: + specs: + - oas3 + folderPath: . +types: + - RESTFUL diff --git a/packages/cli/src/utils/index.ts b/packages/cli/src/utils/index.ts new file mode 100644 index 00000000..3d957931 --- /dev/null +++ b/packages/cli/src/utils/index.ts @@ -0,0 +1,3 @@ +export * from './module'; +export * from './logger'; +export * from './shutdown'; diff --git a/packages/cli/src/utils/logger.ts b/packages/cli/src/utils/logger.ts new file mode 100644 index 00000000..938c851e --- /dev/null +++ b/packages/cli/src/utils/logger.ts @@ -0,0 +1,10 @@ +import { Logger } from 'tslog'; + +// We don't use createLogger helper from core package because CLI will be installed before all packages. +export const logger = new Logger({ + name: 'CLI', + minLevel: 'info', + exposeErrorCodeFrame: false, + displayFilePath: 'hidden', + displayFunctionName: false, +}); diff --git a/packages/cli/src/utils/module.ts b/packages/cli/src/utils/module.ts new file mode 100644 index 00000000..96bc8bbb --- /dev/null +++ b/packages/cli/src/utils/module.ts @@ -0,0 +1,5 @@ +import * as path from 'path'; + +export const localModulePath = (moduleName: string): string => { + return path.resolve(process.cwd(), 'node_modules', moduleName); +}; diff --git a/packages/cli/src/utils/shutdown.ts b/packages/cli/src/utils/shutdown.ts new file mode 100644 index 00000000..f1f6a251 --- /dev/null +++ b/packages/cli/src/utils/shutdown.ts @@ -0,0 +1,20 @@ +import { logger } from './logger'; + +type JobBeforeShutdown = () => Promise; + +const shutdownJobs: JobBeforeShutdown[] = []; + +export const addShutdownJob = (job: JobBeforeShutdown) => { + shutdownJobs.push(job); +}; + +export const runShutdownJobs = async () => { + logger.info('Ctrl-C signal caught, stopping services...'); + await Promise.all(shutdownJobs.map((job) => job())); + logger.info('Bye.'); +}; + +process.on('SIGINT', async () => { + await runShutdownJobs(); + process.exit(0); +}); diff --git a/packages/cli/test/cli.spec.ts b/packages/cli/test/cli.spec.ts new file mode 100644 index 00000000..14e62edf --- /dev/null +++ b/packages/cli/test/cli.spec.ts @@ -0,0 +1,71 @@ +import { program } from '../src/cli'; +import { promises as fs } from 'fs'; +import * as jsYAML from 'js-yaml'; +import * as path from 'path'; +import { runShutdownJobs } from '../src/utils'; +import * as supertest from 'supertest'; + +const projectName = 'test-vulcan-project'; + +const workspaceRoot = path.resolve(__dirname, '..', '..', '..'); +const projectRoot = path.resolve(workspaceRoot, projectName); + +beforeAll(async () => { + await fs.rm(projectRoot, { recursive: true, force: true }); + await program.parseAsync(['node', 'vulcan', 'init', '-p', projectName]); + process.chdir(projectRoot); +}, 30000); + +afterAll(async () => { + await fs.rm(projectRoot, { recursive: true, force: true }); +}); + +afterEach(async () => { + await runShutdownJobs(); +}); + +it('Init command should create new folder with default config', async () => { + // Action + const config: any = jsYAML.load( + await fs.readFile(path.resolve(projectRoot, 'vulcan.yaml'), 'utf8') + ); + // Assert + expect(config.name).toBe(projectName); +}); + +it('Build command should make result.json', async () => { + // Action + await program.parseAsync(['node', 'vulcan', 'build']); + // Assert + expect( + fs.readFile(path.resolve(projectRoot, 'result.json'), 'utf-8') + ).resolves.not.toThrow(); +}); + +it('Serve command should start Vulcan server', async () => { + // Action + await program.parseAsync(['node', 'vulcan', 'build']); + await program.parseAsync(['node', 'vulcan', 'serve', '-p', '12345']); + const agent = supertest('http://localhost:12345'); + const result = await agent.get('/'); + // Assert + expect(result.statusCode).toBe(200); + await runShutdownJobs(); +}); + +it('Start command should build the project and start Vulcan server', async () => { + // Action + await program.parseAsync(['node', 'vulcan', 'start', '-p', '12345']); + const agent = supertest('http://localhost:12345'); + const result = await agent.get('/'); + // Assert + expect(result.statusCode).toBe(200); + await runShutdownJobs(); +}); + +it('Version command should execute without error', async () => { + // Action, Assert + await expect( + program.parseAsync(['node', 'vulcan', 'version']) + ).resolves.not.toThrow(); +}); diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json new file mode 100644 index 00000000..9be5c4ac --- /dev/null +++ b/packages/cli/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "resolveJsonModule": true, + "sourceMap": false + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/packages/cli/tsconfig.lib.json b/packages/cli/tsconfig.lib.json new file mode 100644 index 00000000..bc8e5e0d --- /dev/null +++ b/packages/cli/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["**/*.ts", "../../types/*.d.ts"], + "exclude": ["jest.config.ts", "**/*.spec.ts", "**/*.test.ts"] +} diff --git a/packages/cli/tsconfig.spec.json b/packages/cli/tsconfig.spec.json new file mode 100644 index 00000000..e1cfac50 --- /dev/null +++ b/packages/cli/tsconfig.spec.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "jest.config.ts", + "**/*.test.ts", + "**/*.spec.ts", + "**/*.d.ts.", + "../../types/*.d.ts" + ] +} diff --git a/packages/core/package.json b/packages/core/package.json index 32398770..99828230 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,24 @@ { "name": "@vulcan-sql/core", - "version": "0.0.1", + "description": "Core package of Vulcan", + "version": "0.1.0-alpha.1", "type": "commonjs", - "dependencies": {} + "publishConfig": { + "access": "public" + }, + "keywords": [ + "vulcan", + "vulcan-sql", + "data", + "sql", + "database", + "data-warehouse", + "data-lake", + "api-builder" + ], + "repository": { + "type": "git", + "url": "https://github.com/Canner/vulcan.git" + }, + "license": "MIT" } diff --git a/packages/core/project.json b/packages/core/project.json index 329fc7c4..e2f1ce38 100644 --- a/packages/core/project.json +++ b/packages/core/project.json @@ -3,13 +3,26 @@ "sourceRoot": "packages/core/src", "targets": { "build": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "command": "yarn ts-node ./tools/scripts/replaceAlias.ts core" + }, + "dependsOn": [ + { + "projects": "self", + "target": "tsc" + } + ] + }, + "tsc": { "executor": "@nrwl/js:tsc", "outputs": ["{options.outputPath}"], "options": { "outputPath": "dist/packages/core", "main": "packages/core/src/index.ts", "tsConfig": "packages/core/tsconfig.lib.json", - "assets": ["packages/core/*.md"] + "assets": ["packages/core/*.md"], + "buildableProjectDepsInPackageJsonType": "dependencies" } }, "lint": { @@ -26,6 +39,19 @@ "jestConfig": "packages/core/jest.config.ts", "passWithNoTests": true } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "command": "node ../../../tools/scripts/publish.mjs {args.tag}", + "cwd": "dist/packages/core" + }, + "dependsOn": [ + { + "projects": "self", + "target": "build" + } + ] } }, "tags": [] diff --git a/packages/core/src/lib/template-engine/code-loader/inMemoryCodeLoader.ts b/packages/core/src/lib/template-engine/code-loader/inMemoryCodeLoader.ts index 917c556c..f6cee168 100644 --- a/packages/core/src/lib/template-engine/code-loader/inMemoryCodeLoader.ts +++ b/packages/core/src/lib/template-engine/code-loader/inMemoryCodeLoader.ts @@ -6,7 +6,7 @@ import { } from '@vulcan-sql/core/models'; @VulcanInternalExtension() -@VulcanExtensionId('inMemory') +@VulcanExtensionId('InMemory') export class InMemoryCodeLoader extends CodeLoader { private source = new Map(); diff --git a/packages/core/src/options/templateEngine.ts b/packages/core/src/options/templateEngine.ts index 3d7a7d1e..39d95b9a 100644 --- a/packages/core/src/options/templateEngine.ts +++ b/packages/core/src/options/templateEngine.ts @@ -15,7 +15,7 @@ export class TemplateEngineOptions implements ITemplateEngineOptions { @IsString() @IsOptional() - public readonly codeLoader: string = 'inMemory'; + public readonly codeLoader: string = 'InMemory'; constructor( @inject(TYPES.TemplateEngineInputOptions) diff --git a/packages/extension-dbt/package.json b/packages/extension-dbt/package.json index 42f544bb..0778380c 100644 --- a/packages/extension-dbt/package.json +++ b/packages/extension-dbt/package.json @@ -1,5 +1,28 @@ { "name": "@vulcan-sql/extension-dbt", - "version": "0.0.1", - "type": "commonjs" + "description": "Using dbt models form Vulcan projects", + "version": "0.1.0-alpha.1", + "type": "commonjs", + "publishConfig": { + "access": "public" + }, + "keywords": [ + "vulcan", + "vulcan-sql", + "data", + "sql", + "database", + "data-warehouse", + "data-lake", + "api-builder", + "dbt" + ], + "repository": { + "type": "git", + "url": "https://github.com/Canner/vulcan.git" + }, + "license": "MIT", + "peerDependencies": { + "@vulcan-sql/core": "0.x" + } } diff --git a/packages/extension-dbt/project.json b/packages/extension-dbt/project.json index 8e9f87d6..a00b540f 100644 --- a/packages/extension-dbt/project.json +++ b/packages/extension-dbt/project.json @@ -3,14 +3,33 @@ "sourceRoot": "packages/extension-dbt/src", "targets": { "build": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "command": "yarn ts-node ./tools/scripts/replaceAlias.ts extension-dbt" + }, + "dependsOn": [ + { + "projects": "self", + "target": "tsc" + } + ] + }, + "tsc": { "executor": "@nrwl/js:tsc", "outputs": ["{options.outputPath}"], "options": { "outputPath": "dist/packages/extension-dbt", "main": "packages/extension-dbt/src/index.ts", "tsConfig": "packages/extension-dbt/tsconfig.lib.json", - "assets": ["packages/extension-dbt/*.md"] - } + "assets": ["packages/extension-dbt/*.md"], + "buildableProjectDepsInPackageJsonType": "dependencies" + }, + "dependsOn": [ + { + "projects": "dependencies", + "target": "build" + } + ] }, "lint": { "executor": "@nrwl/linter:eslint", @@ -26,6 +45,19 @@ "jestConfig": "packages/extension-dbt/jest.config.ts", "passWithNoTests": true } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "command": "node ../../../tools/scripts/publish.mjs {args.tag}", + "cwd": "dist/packages/extension-dbt" + }, + "dependsOn": [ + { + "projects": "self", + "target": "build" + } + ] } }, "tags": [] diff --git a/packages/integration-testing/project.json b/packages/integration-testing/project.json index c9858133..5667c298 100644 --- a/packages/integration-testing/project.json +++ b/packages/integration-testing/project.json @@ -2,16 +2,6 @@ "root": "packages/integration-testing", "sourceRoot": "packages/integration-testing/src", "targets": { - "build": { - "executor": "@nrwl/js:tsc", - "outputs": ["{options.outputPath}"], - "options": { - "outputPath": "dist/packages/integration-testing", - "main": "packages/integration-testing/src/index.ts", - "tsConfig": "packages/integration-testing/tsconfig.lib.json", - "assets": ["packages/integration-testing/*.md"] - } - }, "lint": { "executor": "@nrwl/linter:eslint", "outputs": ["{options.outputFile}"], diff --git a/packages/integration-testing/src/example1/buildAndServe.spec.ts b/packages/integration-testing/src/example1/buildAndServe.spec.ts index 39bac818..0bac3e22 100644 --- a/packages/integration-testing/src/example1/buildAndServe.spec.ts +++ b/packages/integration-testing/src/example1/buildAndServe.spec.ts @@ -53,8 +53,8 @@ afterEach(async () => { }); it('Example1: Build and serve should work', async () => { - const builder = new VulcanBuilder(); - await builder.build(projectConfig); + const builder = new VulcanBuilder(projectConfig); + await builder.build(); server = new VulcanServer(projectConfig); const httpServer = await server.start(3000); diff --git a/packages/serve/package.json b/packages/serve/package.json index 3df64700..c09d35cf 100644 --- a/packages/serve/package.json +++ b/packages/serve/package.json @@ -1,7 +1,27 @@ { "name": "@vulcan-sql/serve", - "version": "0.0.1", + "description": "Vulcan package for serving projects", + "version": "0.1.0-alpha.1", "type": "commonjs", - "dependencies": {}, - "devDependencies": {} + "publishConfig": { + "access": "public" + }, + "keywords": [ + "vulcan", + "vulcan-sql", + "data", + "sql", + "database", + "data-warehouse", + "data-lake", + "api-builder" + ], + "repository": { + "type": "git", + "url": "https://github.com/Canner/vulcan.git" + }, + "license": "MIT", + "peerDependencies": { + "@vulcan-sql/core": "0.1.0-alpha.1" + } } diff --git a/packages/serve/project.json b/packages/serve/project.json index 2108d3ec..f0985a1d 100644 --- a/packages/serve/project.json +++ b/packages/serve/project.json @@ -10,14 +10,33 @@ } }, "build": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "command": "yarn ts-node ./tools/scripts/replaceAlias.ts serve" + }, + "dependsOn": [ + { + "projects": "self", + "target": "tsc" + } + ] + }, + "tsc": { "executor": "@nrwl/js:tsc", "outputs": ["{options.outputPath}"], "options": { "outputPath": "dist/packages/serve", "main": "packages/serve/src/index.ts", "tsConfig": "packages/serve/tsconfig.lib.json", - "assets": ["packages/serve/*.md"] - } + "assets": ["packages/serve/*.md"], + "buildableProjectDepsInPackageJsonType": "dependencies" + }, + "dependsOn": [ + { + "projects": "dependencies", + "target": "build" + } + ] }, "lint": { "executor": "@nrwl/linter:eslint", @@ -33,6 +52,19 @@ "jestConfig": "packages/serve/jest.config.ts", "passWithNoTests": true } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "command": "node ../../../tools/scripts/publish.mjs {args.tag}", + "cwd": "dist/packages/serve" + }, + "dependsOn": [ + { + "projects": "self", + "target": "build" + } + ] } }, "tags": [] diff --git a/packages/test-utility/package.json b/packages/test-utility/package.json index 24adc2e4..07a56f71 100644 --- a/packages/test-utility/package.json +++ b/packages/test-utility/package.json @@ -1,5 +1,27 @@ { - "name": "@vulcan/test-utility", - "version": "0.0.1", - "type": "commonjs" + "name": "@vulcan-sql/test-utility", + "version": "0.1.0-alpha.1", + "type": "commonjs", + "publishConfig": { + "access": "public" + }, + "keywords": [ + "vulcan", + "vulcan-sql", + "data", + "sql", + "database", + "data-warehouse", + "data-lake", + "api-builder", + "dev-tools" + ], + "repository": { + "type": "git", + "url": "https://github.com/Canner/vulcan.git" + }, + "license": "MIT", + "peerDependencies": { + "@vulcan-sql/core": "0.1.0-alpha.1" + } } diff --git a/packages/test-utility/project.json b/packages/test-utility/project.json index 6d3d68ea..e572b8bf 100644 --- a/packages/test-utility/project.json +++ b/packages/test-utility/project.json @@ -3,14 +3,33 @@ "sourceRoot": "packages/test-utility/src", "targets": { "build": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "command": "yarn ts-node ./tools/scripts/replaceAlias.ts test-utility" + }, + "dependsOn": [ + { + "projects": "self", + "target": "tsc" + } + ] + }, + "tsc": { "executor": "@nrwl/js:tsc", "outputs": ["{options.outputPath}"], "options": { "outputPath": "dist/packages/test-utility", "main": "packages/test-utility/src/index.ts", "tsConfig": "packages/test-utility/tsconfig.lib.json", - "assets": ["packages/test-utility/*.md"] - } + "assets": ["packages/test-utility/*.md"], + "buildableProjectDepsInPackageJsonType": "dependencies" + }, + "dependsOn": [ + { + "projects": "dependencies", + "target": "build" + } + ] }, "lint": { "executor": "@nrwl/linter:eslint", @@ -26,6 +45,19 @@ "jestConfig": "packages/test-utility/jest.config.ts", "passWithNoTests": true } + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "command": "node ../../../tools/scripts/publish.mjs {args.tag}", + "cwd": "dist/packages/test-utility" + }, + "dependsOn": [ + { + "projects": "self", + "target": "build" + } + ] } }, "tags": [] diff --git a/tools/scripts/publish.mjs b/tools/scripts/publish.mjs new file mode 100644 index 00000000..6523a17d --- /dev/null +++ b/tools/scripts/publish.mjs @@ -0,0 +1,13 @@ +import { execSync } from 'child_process'; + +if (process.env.READY_FOR_PUBLISH !== 'true') { + console.log(`Set env READY_FOR_PUBLISH=true before running publish commands.`) + process.exit(1); +} + +// Executing publish script: node path/to/publish.mjs {tag} +// Default "tag" to "alpha" so we won't publish the "latest" tag by accident. +const [, , tag = 'alpha'] = process.argv; + +// Execute "npm publish" to publish +execSync(`npm publish --tag ${tag}`); diff --git a/tsconfig.base.json b/tsconfig.base.json index 8feae6ea..ec1de97e 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -32,6 +32,7 @@ "@vulcan-sql/build/schema-parser/*": [ "packages/build/src/lib/schema-parser/*" ], + "@vulcan-sql/cli": ["packages/cli/src/index"], "@vulcan-sql/core": ["packages/core/src/index"], "@vulcan-sql/core/artifact-builder": [ "packages/core/src/lib/artifact-builder/index" @@ -66,10 +67,10 @@ ], "@vulcan-sql/extension-dbt": ["packages/extension-dbt/src/index"], "@vulcan-sql/integration-testing": [ - "packages/integration-testing/src/index.ts" + "packages/integration-testing/src/index" ], "@vulcan-sql/serve": ["packages/serve/src/index"], - "@vulcan-sql/serve/app": ["packages/serve/src/lib/app.ts"], + "@vulcan-sql/serve/app": ["packages/serve/src/lib/app"], "@vulcan-sql/serve/config": ["packages/serve/src/lib/config"], "@vulcan-sql/serve/containers": ["packages/serve/src/containers/index"], "@vulcan-sql/serve/loader": ["packages/serve/src/lib/loader"], @@ -93,7 +94,7 @@ "@vulcan-sql/serve/route/*": ["packages/serve/src/lib/route/*"], "@vulcan-sql/serve/utils": ["packages/serve/src/lib/utils/index"], "@vulcan-sql/serve/utils/*": ["packages/serve/src/lib/utils/*"], - "@vulcan-sql/test-utility": ["packages/test-utility/src/index.ts"] + "@vulcan-sql/test-utility": ["packages/test-utility/src/index"] } }, "exclude": ["node_modules", "tmp"] diff --git a/workspace.json b/workspace.json index 9ca4b734..62ca7e4e 100644 --- a/workspace.json +++ b/workspace.json @@ -2,6 +2,7 @@ "version": 2, "projects": { "build": "packages/build", + "cli": "packages/cli", "core": "packages/core", "extension-dbt": "packages/extension-dbt", "integration-testing": "packages/integration-testing", diff --git a/yarn.lock b/yarn.lock index fcbfa4f4..23630a00 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1124,6 +1124,13 @@ resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-1.8.2.tgz#7315b4c4c54f82d13fa61c228ec5c2ea5cc9e0e1" integrity sha512-EqX+YQxINb+MeXaIqYDASb6U6FCHbWjkj4a1CKDBks3d/QiB2+PqBLyO72vLDgAO1wUI4O+9gweRcQK11bTL/w== +"@types/inquirer@^8.0.0": + version "8.2.2" + resolved "https://registry.yarnpkg.com/@types/inquirer/-/inquirer-8.2.2.tgz#7ec5f166026b55b10df011521c8a63920cb84a7a" + integrity sha512-HXoFOl+KS4yvmUjisi83VpuSBOeeXIzdJfoT/v2pUruqybpHy0Dz1DyXy3E2jNH0cSVKJZV92VOnFBwJR6k83A== + dependencies: + "@types/through" "*" + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.4" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" @@ -1348,6 +1355,13 @@ dependencies: "@types/superagent" "*" +"@types/through@*": + version "0.0.30" + resolved "https://registry.yarnpkg.com/@types/through/-/through-0.0.30.tgz#e0e42ce77e897bd6aead6f6ea62aeb135b8a3895" + integrity sha512-FvnCJljyxhPM3gkRgWmxmDZyAQSiBQQWLI0A0VFL0K7W1oRUrPJSqNO0NvTnLkBcotdlp3lKvaT0JrnyRDkzOg== + dependencies: + "@types/node" "*" + "@types/uuid@^8.3.4": version "8.3.4" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" @@ -2129,6 +2143,11 @@ commander@^6.2.0: resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== +commander@^9.4.0: + version "9.4.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-9.4.0.tgz#bc4a40918fefe52e22450c111ecd6b7acce6f11c" + integrity sha512-sRPT+umqkz90UA8M1yqYfnHlZA7fF6nSphDtxeywPZ49ysjxDQybzk13CL+mXekDRG92skbcqCLVovuCusNmFw== + commitizen@^4.0.3, commitizen@^4.2.5: version "4.2.5" resolved "https://registry.yarnpkg.com/commitizen/-/commitizen-4.2.5.tgz#48e5a5c28334c6e8ed845cc24fc9f072efd3961e" @@ -3440,7 +3459,7 @@ ini@^1.3.4: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== -inquirer@8.2.4: +inquirer@8.2.4, inquirer@^8.0.0: version "8.2.4" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.4.tgz#ddbfe86ca2f67649a67daa6f1051c128f684f0b4" integrity sha512-nn4F01dxU8VeKfq192IjLsxu0/OmMZ4Lg3xKAns148rCaXP6ntAoEkVYZThWjwON8AlzdZZi6oqnhNbxUG9hVg==