From 92b303f2abee3303f27996793b8e52203cc3d9bd Mon Sep 17 00:00:00 2001 From: Nicolas Froidure Date: Mon, 9 Mar 2020 15:29:03 +0100 Subject: [PATCH] feat(@whook/cli): add a command to quickly create templated files --- package-lock.json | 18 +- packages/whook-cli/README.md | 4 +- packages/whook-cli/package-lock.json | 50 + packages/whook-cli/package.json | 7 +- .../__snapshots__/create.test.ts.snap | 1163 +++++++++++++++++ .../whook-cli/src/commands/create.test.ts | 505 +++++++ packages/whook-cli/src/commands/create.ts | 488 +++++++ .../whook-create/src/services/author.test.ts | 7 +- .../whook-create/src/services/project.test.ts | 5 +- packages/whook-example/README.md | 9 +- .../src/__snapshots__/cli.test.ts.snap | 3 +- packages/whook-example/src/services/API.ts | 5 + .../services/__snapshots__/API.test.ts.snap | 5 + packages/whook-http-router/src/index.ts | 11 +- packages/whook-http-router/src/utils.ts | 2 +- packages/whook/src/index.ts | 2 + 16 files changed, 2262 insertions(+), 22 deletions(-) create mode 100644 packages/whook-cli/package-lock.json create mode 100644 packages/whook-cli/src/commands/__snapshots__/create.test.ts.snap create mode 100644 packages/whook-cli/src/commands/create.test.ts create mode 100644 packages/whook-cli/src/commands/create.ts diff --git a/package-lock.json b/package-lock.json index 896fab8a..139e645c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1306,9 +1306,9 @@ } }, "@octokit/types": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-2.3.1.tgz", - "integrity": "sha512-rvJP1Y9A/+Cky2C3var1vsw3Lf5Rjn/0sojNl2AjCX+WbpIHYccaJ46abrZoIxMYnOToul6S9tPytUVkFI7CXQ==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-2.4.0.tgz", + "integrity": "sha512-RxVKYIFUZti2POYxeASCSjj0JxtHvjlcFwpZnXQ7aDGDgkpzpve/qhQSR/nEw8zALRFiSuh9BP71AYL0rcV28A==", "dev": true, "requires": { "@types/node": ">= 8" @@ -1338,9 +1338,9 @@ "dev": true }, "@types/node": { - "version": "13.7.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-13.7.7.tgz", - "integrity": "sha512-Uo4chgKbnPNlxQwoFmYIwctkQVkMMmsAoGGU4JKwLuvBefF0pCq4FybNSnfkfRCpC7ZW7kttcC/TrRtAJsvGtg==", + "version": "13.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-13.9.0.tgz", + "integrity": "sha512-0ARSQootUG1RljH2HncpsY2TJBfGQIKOOi7kxzUY6z54ePu/ZD+wJA8zI2Q6v8rol2qpG/rvqsReco8zNMPvhQ==", "dev": true }, "@zkochan/cmd-shim": { @@ -5438,9 +5438,9 @@ } }, "nopt": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", - "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", + "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", "dev": true, "requires": { "abbrev": "1", diff --git a/packages/whook-cli/README.md b/packages/whook-cli/README.md index 8a15496b..3ecc4f3b 100644 --- a/packages/whook-cli/README.md +++ b/packages/whook-cli/README.md @@ -32,10 +32,10 @@ npm run compile cd ../whook-example # Debugging compiled commands -node ../whook-cli/bin/whook.js -- ls +node ../whook-cli/bin/whook.js ls # Debugging source commands -PROJECT_SRC="$PWD/src" NODE_ENV=${NODE_ENV:-development} npm run cli -- babel-node --extensions '.ts,.js' -- ../whook-cli/bin/whook.js -- ls +PROJECT_SRC="$PWD/src" NODE_ENV=${NODE_ENV:-development} npm run cli -- babel-node --extensions '.ts,.js' -- ../whook-cli/bin/whook.js ls ``` [//]: # (::contents:end) diff --git a/packages/whook-cli/package-lock.json b/packages/whook-cli/package-lock.json new file mode 100644 index 00000000..d53f1856 --- /dev/null +++ b/packages/whook-cli/package-lock.json @@ -0,0 +1,50 @@ +{ + "name": "@whook/cli", + "version": "3.1.3", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "babel-plugin-knifecycle": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-knifecycle/-/babel-plugin-knifecycle-1.2.0.tgz", + "integrity": "sha512-2ysCLLLdOqIwPHbOK+xCBrOhOqHrMm+OEazjlRx5NxM1otNdBmGZeIXXImGdW2bC25QbqmNcJJgvk3OYSE7+xg==", + "dev": true, + "requires": { + "knifecycle": "^8.0.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "knifecycle": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/knifecycle/-/knifecycle-8.1.0.tgz", + "integrity": "sha512-KO1lHg63tc2tvJXt3t3c2Qks148QUGb3x1kcaAzgE1/9mnRYpk2mpZ8bldNJ0M0UN/9h5yGi7UEaSyGico91fg==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "yerror": "^5.0.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "yerror": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yerror/-/yerror-5.0.0.tgz", + "integrity": "sha512-b4I0lhWrHDcFGVSP5SkSqGL8P+cuIvmoGvWfnm7YLMWMvUwngCZy9qNLjRbnABh02K/q/yFzBBz8dQuUVEoMAw==", + "dev": true + } + } + } + } +} diff --git a/packages/whook-cli/package.json b/packages/whook-cli/package.json index 163b37ee..97642ff8 100644 --- a/packages/whook-cli/package.json +++ b/packages/whook-cli/package.json @@ -70,12 +70,17 @@ }, "homepage": "https://github.com/nfroidure/whook", "dependencies": { + "@types/fs-extra": "^8.1.0", + "@types/inquirer": "^6.5.0", "@whook/whook": "^3.1.3", "ajv": "^6.11.0", + "camel-case": "^4.1.1", "common-services": "^6.2.0", + "fs-extra": "^8.1.0", "inquirer": "^7.0.0", "knifecycle": "^8.1.0", "miniquery": "^1.1.2", + "openapi-types": "^1.3.5", "yargs-parser": "^16.1.0" }, "devDependencies": { @@ -90,7 +95,7 @@ "@typescript-eslint/eslint-plugin": "^2.16.0", "@typescript-eslint/parser": "^2.16.0", "babel-eslint": "^10.0.3", - "babel-plugin-knifecycle": "^1.1.1", + "babel-plugin-knifecycle": "^1.2.0", "eslint": "^6.8.0", "eslint-plugin-prettier": "^3.1.2", "jest": "^24.9.0", diff --git a/packages/whook-cli/src/commands/__snapshots__/create.test.ts.snap b/packages/whook-cli/src/commands/__snapshots__/create.test.ts.snap new file mode 100644 index 00000000..8b63201d --- /dev/null +++ b/packages/whook-cli/src/commands/__snapshots__/create.test.ts.snap @@ -0,0 +1,1163 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`createCommand for commands should work when existing with dependencies and erase allowed 1`] = ` +Object { + "ensureDirCalls": Array [ + Array [ + "/hom/whoiam/project/src/commands", + ], + ], + "inquirerPromptCalls": Array [ + Array [ + Array [ + Object { + "choices": Array [ + "time", + "log", + "random", + "delay", + "process", + "HOST", + "PORT", + "PROJECT_DIR", + "PROJECT_SRC", + "NODE_ENV", + "DEBUG_NODE_ENVS", + "WHOOK_PLUGINS_PATHS", + "API_DEFINITIONS", + "ENV", + "APM", + "CONFIGS", + ], + "message": "Which services do you want to use?", + "name": "services", + "type": "checkbox", + }, + ], + ], + Array [ + Array [ + Object { + "message": "Give the command description", + "name": "description", + }, + ], + ], + Array [ + Array [ + Object { + "name": "Erase ?", + "type": "confirm", + }, + ], + ], + ], + "logCalls": Array [ + Array [ + "warning", + "⚠️ - The file already exists !", + ], + ], + "pathExistsCalls": Array [ + Array [ + "/hom/whoiam/project/src/commands/aCommand.ts", + ], + ], + "promptArgsCalls": Array [ + Array [], + ], + "result": undefined, + "writeFileCalls": Array [ + Array [ + "/hom/whoiam/project/src/commands/aCommand.ts", + "import { extra, autoService } from 'knifecycle'; +import { LogService } from 'common-services'; +import { + PromptArgs, + WhookCommandArgs, + WhookCommandDefinition, + WhookCommandHandler, + readArgs, +} from '@whook/cli'; + + +export const definition: WhookCommandDefinition = { + description: 'yolo', + example: \`whook aCommand --param \\"value\\"\`, + arguments: { + type: 'object', + additionalProperties: false, + required: ['param'], + properties: { + param: { + description: 'A parameter', + type: 'string', + default: 'A default value', + }, + }, + }, +}; + +export default extra(definition, autoService(initACommandCommand)); + +async function initACommandCommand({ + NODE_ENV, + PROJECT_DIR, + log, + promptArgs, +}: { + NODE_ENV: string; + PROJECT_DIR: string; + log: LogService; + promptArgs: PromptArgs; +}): Promise { + return async () => { + const { param } = readArgs( + definition.arguments, + await promptArgs(), + ) as { param: string; }; + + // Implement your command here + } +} +", + ], + ], +} +`; + +exports[`createCommand for commands should work when existing with dependencies but no erase allowed 1`] = ` +Object { + "ensureDirCalls": Array [ + Array [ + "/hom/whoiam/project/src/commands", + ], + ], + "inquirerPromptCalls": Array [ + Array [ + Array [ + Object { + "choices": Array [ + "time", + "log", + "random", + "delay", + "process", + "HOST", + "PORT", + "PROJECT_DIR", + "PROJECT_SRC", + "NODE_ENV", + "DEBUG_NODE_ENVS", + "WHOOK_PLUGINS_PATHS", + "API_DEFINITIONS", + "ENV", + "APM", + "CONFIGS", + ], + "message": "Which services do you want to use?", + "name": "services", + "type": "checkbox", + }, + ], + ], + Array [ + Array [ + Object { + "message": "Give the command description", + "name": "description", + }, + ], + ], + Array [ + Array [ + Object { + "name": "Erase ?", + "type": "confirm", + }, + ], + ], + ], + "logCalls": Array [ + Array [ + "warning", + "⚠️ - The file already exists !", + ], + ], + "pathExistsCalls": Array [ + Array [ + "/hom/whoiam/project/src/commands/aCommand.ts", + ], + ], + "promptArgsCalls": Array [ + Array [], + ], + "result": undefined, + "writeFileCalls": Array [], +} +`; + +exports[`createCommand for commands should work with no dependencies 1`] = ` +Object { + "ensureDirCalls": Array [ + Array [ + "/hom/whoiam/project/src/commands", + ], + ], + "inquirerPromptCalls": Array [ + Array [ + Array [ + Object { + "choices": Array [ + "time", + "log", + "random", + "delay", + "process", + "HOST", + "PORT", + "PROJECT_DIR", + "PROJECT_SRC", + "NODE_ENV", + "DEBUG_NODE_ENVS", + "WHOOK_PLUGINS_PATHS", + "API_DEFINITIONS", + "ENV", + "APM", + "CONFIGS", + ], + "message": "Which services do you want to use?", + "name": "services", + "type": "checkbox", + }, + ], + ], + Array [ + Array [ + Object { + "message": "Give the command description", + "name": "description", + }, + ], + ], + ], + "logCalls": Array [], + "pathExistsCalls": Array [ + Array [ + "/hom/whoiam/project/src/commands/aCommand.ts", + ], + ], + "promptArgsCalls": Array [ + Array [], + ], + "result": undefined, + "writeFileCalls": Array [ + Array [ + "/hom/whoiam/project/src/commands/aCommand.ts", + "import { extra, autoService } from 'knifecycle'; +import { + PromptArgs, + WhookCommandArgs, + WhookCommandDefinition, + WhookCommandHandler, + readArgs, +} from '@whook/cli'; + + +export const definition: WhookCommandDefinition = { + description: 'yolo', + example: \`whook aCommand --param \\"value\\"\`, + arguments: { + type: 'object', + additionalProperties: false, + required: ['param'], + properties: { + param: { + description: 'A parameter', + type: 'string', + default: 'A default value', + }, + }, + }, +}; + +export default extra(definition, autoService(initACommandCommand)); + +async function initACommandCommand({ + promptArgs, +}: { + promptArgs: PromptArgs; +}): Promise { + return async () => { + const { param } = readArgs( + definition.arguments, + await promptArgs(), + ) as { param: string; }; + + // Implement your command here + } +} +", + ], + ], +} +`; + +exports[`createCommand for handlers should work with an existing get and dependencies and erase allowed 1`] = ` +Object { + "ensureDirCalls": Array [ + Array [ + "/hom/whoiam/project/src/handlers", + ], + ], + "inquirerPromptCalls": Array [ + Array [ + Array [ + Object { + "choices": Array [ + "time", + "log", + "random", + "delay", + "process", + "HOST", + "PORT", + "PROJECT_DIR", + "PROJECT_SRC", + "NODE_ENV", + "DEBUG_NODE_ENVS", + "WHOOK_PLUGINS_PATHS", + "API_DEFINITIONS", + "ENV", + "APM", + "CONFIGS", + ], + "message": "Which services do you want to use?", + "name": "services", + "type": "checkbox", + }, + ], + ], + Array [ + Array [ + Object { + "choices": Array [ + "options", + "head", + "get", + "put", + "post", + "patch", + "delete", + "trace", + ], + "default": "get", + "message": "Give the handler method", + "name": "method", + "type": "list", + }, + Object { + "message": "Give the handler path", + "name": "path", + "type": "input", + }, + Object { + "message": "Give the handler description", + "name": "description", + "type": "input", + }, + Object { + "choices": Array [ + "system", + ], + "message": "Assing one or more tags to the handler", + "name": "tags", + "type": "checkbox", + }, + ], + ], + Array [ + Array [ + Object { + "name": "Erase ?", + "type": "confirm", + }, + ], + ], + ], + "logCalls": Array [ + Array [ + "warning", + "⚠️ - The file already exists !", + ], + ], + "pathExistsCalls": Array [ + Array [ + "/hom/whoiam/project/src/handlers/getHandler.ts", + ], + ], + "promptArgsCalls": Array [ + Array [], + ], + "result": undefined, + "writeFileCalls": Array [ + Array [ + "/hom/whoiam/project/src/handlers/getHandler.ts", + "import { autoHandler } from 'knifecycle'; +import { WhookResponse, WhookAPIHandlerDefinition } from '@whook/whook'; +import { LogService } from 'common-services'; + +export const definition: WhookAPIHandlerDefinition = { + path: '/lol', + method: 'get', + operation: { + operationId: 'getHandler', + summary: 'yolo', + tags: [], + parameters: [ + { + name: 'param', + in: 'query', + required: false, + schema: { type: 'number' }, + }, + ], + responses: { + 200: { + description: 'Success', + content: { + 'application/json': { + schema: { + type: 'object', + }, + }, + }, + }, + }, + }, +}; + +type HandlerDependencies = { + NODE_ENV: string; + PROJECT_DIR: string; + log: LogService; +}; + +export default autoHandler(getHandler); + +async function getHandler({ + NODE_ENV, + PROJECT_DIR, + log, +}: HandlerDependencies, { + param, + } : { + param: number; + }): Promise> { + return { + status: 200, + headers: {}, + body: { param }, + }; +} +", + ], + ], +} +`; + +exports[`createCommand for handlers should work with an existing get and dependencies but no erase allowed 1`] = ` +Object { + "ensureDirCalls": Array [ + Array [ + "/hom/whoiam/project/src/handlers", + ], + ], + "inquirerPromptCalls": Array [ + Array [ + Array [ + Object { + "choices": Array [ + "time", + "log", + "random", + "delay", + "process", + "HOST", + "PORT", + "PROJECT_DIR", + "PROJECT_SRC", + "NODE_ENV", + "DEBUG_NODE_ENVS", + "WHOOK_PLUGINS_PATHS", + "API_DEFINITIONS", + "ENV", + "APM", + "CONFIGS", + ], + "message": "Which services do you want to use?", + "name": "services", + "type": "checkbox", + }, + ], + ], + Array [ + Array [ + Object { + "choices": Array [ + "options", + "head", + "get", + "put", + "post", + "patch", + "delete", + "trace", + ], + "default": "get", + "message": "Give the handler method", + "name": "method", + "type": "list", + }, + Object { + "message": "Give the handler path", + "name": "path", + "type": "input", + }, + Object { + "message": "Give the handler description", + "name": "description", + "type": "input", + }, + Object { + "choices": Array [ + "system", + ], + "message": "Assing one or more tags to the handler", + "name": "tags", + "type": "checkbox", + }, + ], + ], + Array [ + Array [ + Object { + "name": "Erase ?", + "type": "confirm", + }, + ], + ], + ], + "logCalls": Array [ + Array [ + "warning", + "⚠️ - The file already exists !", + ], + ], + "pathExistsCalls": Array [ + Array [ + "/hom/whoiam/project/src/handlers/getHandler.ts", + ], + ], + "promptArgsCalls": Array [ + Array [], + ], + "result": undefined, + "writeFileCalls": Array [], +} +`; + +exports[`createCommand for handlers should work with get and no dependencies 1`] = ` +Object { + "ensureDirCalls": Array [ + Array [ + "/hom/whoiam/project/src/handlers", + ], + ], + "inquirerPromptCalls": Array [ + Array [ + Array [ + Object { + "choices": Array [ + "time", + "log", + "random", + "delay", + "process", + "HOST", + "PORT", + "PROJECT_DIR", + "PROJECT_SRC", + "NODE_ENV", + "DEBUG_NODE_ENVS", + "WHOOK_PLUGINS_PATHS", + "API_DEFINITIONS", + "ENV", + "APM", + "CONFIGS", + ], + "message": "Which services do you want to use?", + "name": "services", + "type": "checkbox", + }, + ], + ], + Array [ + Array [ + Object { + "choices": Array [ + "options", + "head", + "get", + "put", + "post", + "patch", + "delete", + "trace", + ], + "default": "get", + "message": "Give the handler method", + "name": "method", + "type": "list", + }, + Object { + "message": "Give the handler path", + "name": "path", + "type": "input", + }, + Object { + "message": "Give the handler description", + "name": "description", + "type": "input", + }, + Object { + "choices": Array [ + "system", + ], + "message": "Assing one or more tags to the handler", + "name": "tags", + "type": "checkbox", + }, + ], + ], + ], + "logCalls": Array [], + "pathExistsCalls": Array [ + Array [ + "/hom/whoiam/project/src/handlers/getHandler.ts", + ], + ], + "promptArgsCalls": Array [ + Array [], + ], + "result": undefined, + "writeFileCalls": Array [ + Array [ + "/hom/whoiam/project/src/handlers/getHandler.ts", + "import { autoHandler } from 'knifecycle'; +import { WhookResponse, WhookAPIHandlerDefinition } from '@whook/whook'; + +export const definition: WhookAPIHandlerDefinition = { + path: '/lol', + method: 'get', + operation: { + operationId: 'getHandler', + summary: 'yolo', + tags: [], + parameters: [ + { + name: 'param', + in: 'query', + required: false, + schema: { type: 'number' }, + }, + ], + responses: { + 200: { + description: 'Success', + content: { + 'application/json': { + schema: { + type: 'object', + }, + }, + }, + }, + }, + }, +}; + +type HandlerDependencies = {}; + +export default autoHandler(getHandler); + +async function getHandler(_: HandlerDependencies, { + param, + } : { + param: number; + }): Promise> { + return { + status: 200, + headers: {}, + body: { param }, + }; +} +", + ], + ], +} +`; + +exports[`createCommand for providers should work when existing with dependencies and erase allowed 1`] = ` +Object { + "ensureDirCalls": Array [ + Array [ + "/hom/whoiam/project/src/services", + ], + ], + "inquirerPromptCalls": Array [ + Array [ + Array [ + Object { + "choices": Array [ + "time", + "log", + "random", + "delay", + "process", + "HOST", + "PORT", + "PROJECT_DIR", + "PROJECT_SRC", + "NODE_ENV", + "DEBUG_NODE_ENVS", + "WHOOK_PLUGINS_PATHS", + "API_DEFINITIONS", + "ENV", + "APM", + "CONFIGS", + ], + "message": "Which services do you want to use?", + "name": "services", + "type": "checkbox", + }, + ], + ], + Array [ + Array [ + Object { + "name": "Erase ?", + "type": "confirm", + }, + ], + ], + ], + "logCalls": Array [ + Array [ + "warning", + "⚠️ - The file already exists !", + ], + ], + "pathExistsCalls": Array [ + Array [ + "/hom/whoiam/project/src/services/aProvider.ts", + ], + ], + "promptArgsCalls": Array [ + Array [], + ], + "result": undefined, + "writeFileCalls": Array [ + Array [ + "/hom/whoiam/project/src/services/aProvider.ts", + "import { autoProvider, Provider } from 'knifecycle'; +import { LogService } from 'common-services'; + +export type AProviderService = {}; +export type AProviderProvider = Provider; +export type AProviderDependencies = { + NODE_ENV: string; + PROJECT_DIR: string; + log: LogService; +}; + +export default autoProvider(initAProvider); + +async function initAProvider({ + NODE_ENV, + PROJECT_DIR, + log, +}: AProviderDependencies): Promise { + // Instantiate and return your service + return { + service: {}, + dispose: async () => { + // Do any action before the process shutdown + // (closing db connections... etc) + }, + // You can also set a promise for unexpected errors + // that shutdown the app when it happens + // errorPromise: new Promise(), + }; +} +", + ], + ], +} +`; + +exports[`createCommand for providers should work when existing with dependencies but no erase allowed 1`] = ` +Object { + "ensureDirCalls": Array [ + Array [ + "/hom/whoiam/project/src/services", + ], + ], + "inquirerPromptCalls": Array [ + Array [ + Array [ + Object { + "choices": Array [ + "time", + "log", + "random", + "delay", + "process", + "HOST", + "PORT", + "PROJECT_DIR", + "PROJECT_SRC", + "NODE_ENV", + "DEBUG_NODE_ENVS", + "WHOOK_PLUGINS_PATHS", + "API_DEFINITIONS", + "ENV", + "APM", + "CONFIGS", + ], + "message": "Which services do you want to use?", + "name": "services", + "type": "checkbox", + }, + ], + ], + Array [ + Array [ + Object { + "name": "Erase ?", + "type": "confirm", + }, + ], + ], + ], + "logCalls": Array [ + Array [ + "warning", + "⚠️ - The file already exists !", + ], + ], + "pathExistsCalls": Array [ + Array [ + "/hom/whoiam/project/src/services/aProvider.ts", + ], + ], + "promptArgsCalls": Array [ + Array [], + ], + "result": undefined, + "writeFileCalls": Array [], +} +`; + +exports[`createCommand for providers should work with no dependencies 1`] = ` +Object { + "ensureDirCalls": Array [ + Array [ + "/hom/whoiam/project/src/services", + ], + ], + "inquirerPromptCalls": Array [ + Array [ + Array [ + Object { + "choices": Array [ + "time", + "log", + "random", + "delay", + "process", + "HOST", + "PORT", + "PROJECT_DIR", + "PROJECT_SRC", + "NODE_ENV", + "DEBUG_NODE_ENVS", + "WHOOK_PLUGINS_PATHS", + "API_DEFINITIONS", + "ENV", + "APM", + "CONFIGS", + ], + "message": "Which services do you want to use?", + "name": "services", + "type": "checkbox", + }, + ], + ], + ], + "logCalls": Array [], + "pathExistsCalls": Array [ + Array [ + "/hom/whoiam/project/src/services/aProvider.ts", + ], + ], + "promptArgsCalls": Array [ + Array [], + ], + "result": undefined, + "writeFileCalls": Array [ + Array [ + "/hom/whoiam/project/src/services/aProvider.ts", + "import { autoProvider, Provider } from 'knifecycle'; + +export type AProviderService = {}; +export type AProviderProvider = Provider; +export type AProviderDependencies = {}; + +export default autoProvider(initAProvider); + +async function initAProvider(_: AProviderDependencies): Promise { + // Instantiate and return your service + return { + service: {}, + dispose: async () => { + // Do any action before the process shutdown + // (closing db connections... etc) + }, + // You can also set a promise for unexpected errors + // that shutdown the app when it happens + // errorPromise: new Promise(), + }; +} +", + ], + ], +} +`; + +exports[`createCommand for services should work when existing with dependencies and erase allowed 1`] = ` +Object { + "ensureDirCalls": Array [ + Array [ + "/hom/whoiam/project/src/services", + ], + ], + "inquirerPromptCalls": Array [ + Array [ + Array [ + Object { + "choices": Array [ + "time", + "log", + "random", + "delay", + "process", + "HOST", + "PORT", + "PROJECT_DIR", + "PROJECT_SRC", + "NODE_ENV", + "DEBUG_NODE_ENVS", + "WHOOK_PLUGINS_PATHS", + "API_DEFINITIONS", + "ENV", + "APM", + "CONFIGS", + ], + "message": "Which services do you want to use?", + "name": "services", + "type": "checkbox", + }, + ], + ], + Array [ + Array [ + Object { + "name": "Erase ?", + "type": "confirm", + }, + ], + ], + ], + "logCalls": Array [ + Array [ + "warning", + "⚠️ - The file already exists !", + ], + ], + "pathExistsCalls": Array [ + Array [ + "/hom/whoiam/project/src/services/aService.ts", + ], + ], + "promptArgsCalls": Array [ + Array [], + ], + "result": undefined, + "writeFileCalls": Array [ + Array [ + "/hom/whoiam/project/src/services/aService.ts", + "import { autoService } from 'knifecycle'; +import { LogService } from 'common-services'; + +export type AServiceService = {}; +export type AServiceDependencies = { + NODE_ENV: string; + PROJECT_DIR: string; + log: LogService; +}; + +export default autoService(initAService); + +async function initAService({ + NODE_ENV, + PROJECT_DIR, + log, +}: AServiceDependencies): Promise { + // Instantiate and return your service + return {}; +} +", + ], + ], +} +`; + +exports[`createCommand for services should work when existing with dependencies but no erase allowed 1`] = ` +Object { + "ensureDirCalls": Array [ + Array [ + "/hom/whoiam/project/src/services", + ], + ], + "inquirerPromptCalls": Array [ + Array [ + Array [ + Object { + "choices": Array [ + "time", + "log", + "random", + "delay", + "process", + "HOST", + "PORT", + "PROJECT_DIR", + "PROJECT_SRC", + "NODE_ENV", + "DEBUG_NODE_ENVS", + "WHOOK_PLUGINS_PATHS", + "API_DEFINITIONS", + "ENV", + "APM", + "CONFIGS", + ], + "message": "Which services do you want to use?", + "name": "services", + "type": "checkbox", + }, + ], + ], + Array [ + Array [ + Object { + "name": "Erase ?", + "type": "confirm", + }, + ], + ], + ], + "logCalls": Array [ + Array [ + "warning", + "⚠️ - The file already exists !", + ], + ], + "pathExistsCalls": Array [ + Array [ + "/hom/whoiam/project/src/services/aService.ts", + ], + ], + "promptArgsCalls": Array [ + Array [], + ], + "result": undefined, + "writeFileCalls": Array [], +} +`; + +exports[`createCommand for services should work with no dependencies 1`] = ` +Object { + "ensureDirCalls": Array [ + Array [ + "/hom/whoiam/project/src/services", + ], + ], + "inquirerPromptCalls": Array [ + Array [ + Array [ + Object { + "choices": Array [ + "time", + "log", + "random", + "delay", + "process", + "HOST", + "PORT", + "PROJECT_DIR", + "PROJECT_SRC", + "NODE_ENV", + "DEBUG_NODE_ENVS", + "WHOOK_PLUGINS_PATHS", + "API_DEFINITIONS", + "ENV", + "APM", + "CONFIGS", + ], + "message": "Which services do you want to use?", + "name": "services", + "type": "checkbox", + }, + ], + ], + ], + "logCalls": Array [], + "pathExistsCalls": Array [ + Array [ + "/hom/whoiam/project/src/services/aService.ts", + ], + ], + "promptArgsCalls": Array [ + Array [], + ], + "result": undefined, + "writeFileCalls": Array [ + Array [ + "/hom/whoiam/project/src/services/aService.ts", + "import { autoService } from 'knifecycle'; + +export type AServiceService = {}; +export type AServiceDependencies = {}; + +export default autoService(initAService); + +async function initAService(_: AServiceDependencies): Promise { + // Instantiate and return your service + return {}; +} +", + ], + ], +} +`; diff --git a/packages/whook-cli/src/commands/create.test.ts b/packages/whook-cli/src/commands/create.test.ts new file mode 100644 index 00000000..391f50a8 --- /dev/null +++ b/packages/whook-cli/src/commands/create.test.ts @@ -0,0 +1,505 @@ +import _inquirer from 'inquirer'; +import initCreateCommand from './create'; +import YError from 'yerror'; +import { OpenAPIV3 } from 'openapi-types'; +import { initGetPingDefinition } from '@whook/whook'; + +describe('createCommand', () => { + const PROJECT_DIR = '/hom/whoiam/project'; + const API: OpenAPIV3.Document = { + openapi: '3.0.2', + info: { + version: '1.0.0', + title: 'Sample OpenAPI', + description: 'A sample OpenAPI file for testing purpose.', + }, + paths: { + [initGetPingDefinition.path]: { + [initGetPingDefinition.method]: initGetPingDefinition.operation, + }, + }, + tags: [{ name: 'system' }], + }; + const promptArgs = jest.fn(); + const ensureDir = jest.fn(); + const writeFile = jest.fn(); + const pathExists = jest.fn(); + const inquirer = { prompt: jest.fn() }; + const log = jest.fn(); + + beforeEach(() => { + promptArgs.mockReset(); + ensureDir.mockReset(); + writeFile.mockReset(); + pathExists.mockReset(); + inquirer.prompt.mockReset(); + log.mockReset(); + }); + + describe('for handlers', () => { + it('should work with get and no dependencies', async () => { + promptArgs.mockResolvedValueOnce({ + _: ['create'], + name: 'getHandler', + type: 'handler', + }); + inquirer.prompt.mockResolvedValueOnce({ + services: [], + }); + inquirer.prompt.mockResolvedValueOnce({ + method: 'get', + path: '/lol', + description: 'yolo', + tags: [], + }); + pathExists.mockResolvedValueOnce(false); + + const createCommand = await initCreateCommand({ + PROJECT_DIR, + API, + promptArgs, + ensureDir, + writeFile, + pathExists, + inquirer: (inquirer as unknown) as typeof _inquirer, + log, + }); + const result = await createCommand(); + + expect({ + result, + promptArgsCalls: promptArgs.mock.calls, + ensureDirCalls: ensureDir.mock.calls, + writeFileCalls: writeFile.mock.calls, + pathExistsCalls: pathExists.mock.calls, + inquirerPromptCalls: inquirer.prompt.mock.calls, + logCalls: log.mock.calls.filter(args => 'stack' !== args[0]), + }).toMatchSnapshot(); + }); + + it('should work with an existing get and dependencies but no erase allowed', async () => { + promptArgs.mockResolvedValueOnce({ + _: ['create'], + name: 'getHandler', + type: 'handler', + }); + inquirer.prompt.mockResolvedValueOnce({ + services: ['PROJECT_DIR', 'log', 'NODE_ENV'], + }); + inquirer.prompt.mockResolvedValueOnce({ + method: 'get', + path: '/lol', + description: 'yolo', + tags: [], + }); + pathExists.mockResolvedValueOnce(true); + inquirer.prompt.mockResolvedValueOnce({ + erase: false, + }); + + const createCommand = await initCreateCommand({ + PROJECT_DIR, + API, + promptArgs, + ensureDir, + writeFile, + pathExists, + inquirer: (inquirer as unknown) as typeof _inquirer, + log, + }); + const result = await createCommand(); + + expect({ + result, + promptArgsCalls: promptArgs.mock.calls, + ensureDirCalls: ensureDir.mock.calls, + writeFileCalls: writeFile.mock.calls, + pathExistsCalls: pathExists.mock.calls, + inquirerPromptCalls: inquirer.prompt.mock.calls, + logCalls: log.mock.calls.filter(args => 'stack' !== args[0]), + }).toMatchSnapshot(); + }); + + it('should work with an existing get and dependencies and erase allowed', async () => { + promptArgs.mockResolvedValueOnce({ + _: ['create'], + name: 'getHandler', + type: 'handler', + }); + inquirer.prompt.mockResolvedValueOnce({ + services: ['PROJECT_DIR', 'log', 'NODE_ENV'], + }); + inquirer.prompt.mockResolvedValueOnce({ + method: 'get', + path: '/lol', + description: 'yolo', + tags: [], + }); + pathExists.mockResolvedValueOnce(true); + inquirer.prompt.mockResolvedValueOnce({ + erase: true, + }); + + const createCommand = await initCreateCommand({ + PROJECT_DIR, + API, + promptArgs, + ensureDir, + writeFile, + pathExists, + inquirer: (inquirer as unknown) as typeof _inquirer, + log, + }); + const result = await createCommand(); + + expect({ + result, + promptArgsCalls: promptArgs.mock.calls, + ensureDirCalls: ensureDir.mock.calls, + writeFileCalls: writeFile.mock.calls, + pathExistsCalls: pathExists.mock.calls, + inquirerPromptCalls: inquirer.prompt.mock.calls, + logCalls: log.mock.calls.filter(args => 'stack' !== args[0]), + }).toMatchSnapshot(); + }); + }); + + describe('for services', () => { + it('should work with no dependencies', async () => { + promptArgs.mockResolvedValueOnce({ + _: ['create'], + name: 'aService', + type: 'service', + }); + inquirer.prompt.mockResolvedValueOnce({ + services: [], + }); + pathExists.mockResolvedValueOnce(false); + + const createCommand = await initCreateCommand({ + PROJECT_DIR, + API, + promptArgs, + ensureDir, + writeFile, + pathExists, + inquirer: (inquirer as unknown) as typeof _inquirer, + log, + }); + const result = await createCommand(); + + expect({ + result, + promptArgsCalls: promptArgs.mock.calls, + ensureDirCalls: ensureDir.mock.calls, + writeFileCalls: writeFile.mock.calls, + pathExistsCalls: pathExists.mock.calls, + inquirerPromptCalls: inquirer.prompt.mock.calls, + logCalls: log.mock.calls.filter(args => 'stack' !== args[0]), + }).toMatchSnapshot(); + }); + + it('should work when existing with dependencies but no erase allowed', async () => { + promptArgs.mockResolvedValueOnce({ + _: ['create'], + name: 'aService', + type: 'service', + }); + inquirer.prompt.mockResolvedValueOnce({ + services: ['PROJECT_DIR', 'log', 'NODE_ENV'], + }); + pathExists.mockResolvedValueOnce(true); + inquirer.prompt.mockResolvedValueOnce({ + erase: false, + }); + + const createCommand = await initCreateCommand({ + PROJECT_DIR, + API, + promptArgs, + ensureDir, + writeFile, + pathExists, + inquirer: (inquirer as unknown) as typeof _inquirer, + log, + }); + const result = await createCommand(); + + expect({ + result, + promptArgsCalls: promptArgs.mock.calls, + ensureDirCalls: ensureDir.mock.calls, + writeFileCalls: writeFile.mock.calls, + pathExistsCalls: pathExists.mock.calls, + inquirerPromptCalls: inquirer.prompt.mock.calls, + logCalls: log.mock.calls.filter(args => 'stack' !== args[0]), + }).toMatchSnapshot(); + }); + + it('should work when existing with dependencies and erase allowed', async () => { + promptArgs.mockResolvedValueOnce({ + _: ['create'], + name: 'aService', + type: 'service', + }); + inquirer.prompt.mockResolvedValueOnce({ + services: ['PROJECT_DIR', 'log', 'NODE_ENV'], + }); + pathExists.mockResolvedValueOnce(true); + inquirer.prompt.mockResolvedValueOnce({ + erase: true, + }); + + const createCommand = await initCreateCommand({ + PROJECT_DIR, + API, + promptArgs, + ensureDir, + writeFile, + pathExists, + inquirer: (inquirer as unknown) as typeof _inquirer, + log, + }); + const result = await createCommand(); + + expect({ + result, + promptArgsCalls: promptArgs.mock.calls, + ensureDirCalls: ensureDir.mock.calls, + writeFileCalls: writeFile.mock.calls, + pathExistsCalls: pathExists.mock.calls, + inquirerPromptCalls: inquirer.prompt.mock.calls, + logCalls: log.mock.calls.filter(args => 'stack' !== args[0]), + }).toMatchSnapshot(); + }); + }); + + describe('for providers', () => { + it('should work with no dependencies', async () => { + promptArgs.mockResolvedValueOnce({ + _: ['create'], + name: 'aProvider', + type: 'provider', + }); + inquirer.prompt.mockResolvedValueOnce({ + services: [], + }); + pathExists.mockResolvedValueOnce(false); + + const createCommand = await initCreateCommand({ + PROJECT_DIR, + API, + promptArgs, + ensureDir, + writeFile, + pathExists, + inquirer: (inquirer as unknown) as typeof _inquirer, + log, + }); + const result = await createCommand(); + + expect({ + result, + promptArgsCalls: promptArgs.mock.calls, + ensureDirCalls: ensureDir.mock.calls, + writeFileCalls: writeFile.mock.calls, + pathExistsCalls: pathExists.mock.calls, + inquirerPromptCalls: inquirer.prompt.mock.calls, + logCalls: log.mock.calls.filter(args => 'stack' !== args[0]), + }).toMatchSnapshot(); + }); + + it('should work when existing with dependencies but no erase allowed', async () => { + promptArgs.mockResolvedValueOnce({ + _: ['create'], + name: 'aProvider', + type: 'provider', + }); + inquirer.prompt.mockResolvedValueOnce({ + services: ['PROJECT_DIR', 'log', 'NODE_ENV'], + }); + pathExists.mockResolvedValueOnce(true); + inquirer.prompt.mockResolvedValueOnce({ + erase: false, + }); + + const createCommand = await initCreateCommand({ + PROJECT_DIR, + API, + promptArgs, + ensureDir, + writeFile, + pathExists, + inquirer: (inquirer as unknown) as typeof _inquirer, + log, + }); + const result = await createCommand(); + + expect({ + result, + promptArgsCalls: promptArgs.mock.calls, + ensureDirCalls: ensureDir.mock.calls, + writeFileCalls: writeFile.mock.calls, + pathExistsCalls: pathExists.mock.calls, + inquirerPromptCalls: inquirer.prompt.mock.calls, + logCalls: log.mock.calls.filter(args => 'stack' !== args[0]), + }).toMatchSnapshot(); + }); + + it('should work when existing with dependencies and erase allowed', async () => { + promptArgs.mockResolvedValueOnce({ + _: ['create'], + name: 'aProvider', + type: 'provider', + }); + inquirer.prompt.mockResolvedValueOnce({ + services: ['PROJECT_DIR', 'log', 'NODE_ENV'], + }); + pathExists.mockResolvedValueOnce(true); + inquirer.prompt.mockResolvedValueOnce({ + erase: true, + }); + + const createCommand = await initCreateCommand({ + PROJECT_DIR, + API, + promptArgs, + ensureDir, + writeFile, + pathExists, + inquirer: (inquirer as unknown) as typeof _inquirer, + log, + }); + const result = await createCommand(); + + expect({ + result, + promptArgsCalls: promptArgs.mock.calls, + ensureDirCalls: ensureDir.mock.calls, + writeFileCalls: writeFile.mock.calls, + pathExistsCalls: pathExists.mock.calls, + inquirerPromptCalls: inquirer.prompt.mock.calls, + logCalls: log.mock.calls.filter(args => 'stack' !== args[0]), + }).toMatchSnapshot(); + }); + }); + + describe('for commands', () => { + it('should work with no dependencies', async () => { + promptArgs.mockResolvedValueOnce({ + _: ['create'], + name: 'aCommand', + type: 'command', + }); + inquirer.prompt.mockResolvedValueOnce({ + services: [], + }); + inquirer.prompt.mockResolvedValueOnce({ + description: 'yolo', + }); + pathExists.mockResolvedValueOnce(false); + + const createCommand = await initCreateCommand({ + PROJECT_DIR, + API, + promptArgs, + ensureDir, + writeFile, + pathExists, + inquirer: (inquirer as unknown) as typeof _inquirer, + log, + }); + const result = await createCommand(); + + expect({ + result, + promptArgsCalls: promptArgs.mock.calls, + ensureDirCalls: ensureDir.mock.calls, + writeFileCalls: writeFile.mock.calls, + pathExistsCalls: pathExists.mock.calls, + inquirerPromptCalls: inquirer.prompt.mock.calls, + logCalls: log.mock.calls.filter(args => 'stack' !== args[0]), + }).toMatchSnapshot(); + }); + + it('should work when existing with dependencies but no erase allowed', async () => { + promptArgs.mockResolvedValueOnce({ + _: ['create'], + name: 'aCommand', + type: 'command', + }); + inquirer.prompt.mockResolvedValueOnce({ + services: ['PROJECT_DIR', 'log', 'NODE_ENV'], + }); + inquirer.prompt.mockResolvedValueOnce({ + description: 'yolo', + }); + pathExists.mockResolvedValueOnce(true); + inquirer.prompt.mockResolvedValueOnce({ + erase: false, + }); + + const createCommand = await initCreateCommand({ + PROJECT_DIR, + API, + promptArgs, + ensureDir, + writeFile, + pathExists, + inquirer: (inquirer as unknown) as typeof _inquirer, + log, + }); + const result = await createCommand(); + + expect({ + result, + promptArgsCalls: promptArgs.mock.calls, + ensureDirCalls: ensureDir.mock.calls, + writeFileCalls: writeFile.mock.calls, + pathExistsCalls: pathExists.mock.calls, + inquirerPromptCalls: inquirer.prompt.mock.calls, + logCalls: log.mock.calls.filter(args => 'stack' !== args[0]), + }).toMatchSnapshot(); + }); + + it('should work when existing with dependencies and erase allowed', async () => { + promptArgs.mockResolvedValueOnce({ + _: ['create'], + name: 'aCommand', + type: 'command', + }); + inquirer.prompt.mockResolvedValueOnce({ + services: ['PROJECT_DIR', 'log', 'NODE_ENV'], + }); + inquirer.prompt.mockResolvedValueOnce({ + description: 'yolo', + }); + pathExists.mockResolvedValueOnce(true); + inquirer.prompt.mockResolvedValueOnce({ + erase: true, + }); + + const createCommand = await initCreateCommand({ + PROJECT_DIR, + API, + promptArgs, + ensureDir, + writeFile, + pathExists, + inquirer: (inquirer as unknown) as typeof _inquirer, + log, + }); + const result = await createCommand(); + + expect({ + result, + promptArgsCalls: promptArgs.mock.calls, + ensureDirCalls: ensureDir.mock.calls, + writeFileCalls: writeFile.mock.calls, + pathExistsCalls: pathExists.mock.calls, + inquirerPromptCalls: inquirer.prompt.mock.calls, + logCalls: log.mock.calls.filter(args => 'stack' !== args[0]), + }).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/whook-cli/src/commands/create.ts b/packages/whook-cli/src/commands/create.ts new file mode 100644 index 00000000..df8fb8f9 --- /dev/null +++ b/packages/whook-cli/src/commands/create.ts @@ -0,0 +1,488 @@ +import { extra, autoService } from 'knifecycle'; +import { readArgs } from '../libs/args'; +import YError from 'yerror'; +import { + WhookCommandHandler, + WhookCommandDefinition, + PromptArgs, +} from '../services/promptArgs'; +import { OPEN_API_METHODS, ENVService, noop, WhookConfigs } from '@whook/whook'; +import { LogService } from 'common-services'; +import { camelCase } from 'camel-case'; +import { HANDLER_REG_EXP } from '@whook/whook/dist/services/_autoload'; +import _inquirer from 'inquirer'; +import path from 'path'; +import { OpenAPIV3 } from 'openapi-types'; +import { + writeFile as _writeFile, + ensureDir as _ensureDir, + pathExists as _pathExists, +} from 'fs-extra'; + +// Currently, we rely on a static list of services but +// best would be to use TypeScript introspection and +// the autoloader to allow to retrieve a dynamic list +// of constants from the CONFIGS service and the WhookConfigs +// type and the autoloader service. +const commonServicesTypes = { + time: 'TimeService', + log: 'LogService', + random: 'RandomService', + delay: 'DelayService', + process: 'ProcessService', +}; +const whookSimpleTypes = { + HOST: 'string', + PORT: 'number', + PROJECT_DIR: 'string', + PROJECT_SRC: 'string', + NODE_ENV: 'string', + DEBUG_NODE_ENVS: 'string[]', + WHOOK_PLUGINS_PATHS: 'string[]', +}; +const whookServicesTypes = { + API_DEFINITIONS: 'DelayService', + ENV: 'ENVService', + APM: 'APMService', + CONFIGS: 'CONFIGSService', +}; +const whookCommandsTypes = { + promptArgs: 'PromptArgs', +}; +const allTypes = { + ...commonServicesTypes, + ...whookSimpleTypes, + ...whookServicesTypes, +}; + +export const definition: WhookCommandDefinition = { + description: 'A command helping to create new Whook files easily', + example: `whook create --type service --name "db"`, + arguments: { + type: 'object', + additionalProperties: false, + required: ['type', 'name'], + properties: { + type: { + description: 'Type', + type: 'string', + enum: ['handler', 'service', 'provider', 'command'], + }, + name: { + description: 'Name', + type: 'string', + }, + }, + }, +}; + +export default extra(definition, autoService(initCreateCommand)); + +async function initCreateCommand({ + PROJECT_DIR, + API, + inquirer = _inquirer, + promptArgs, + writeFile = _writeFile, + ensureDir = _ensureDir, + pathExists = _pathExists, + log = noop, +}: { + PROJECT_DIR: string; + API: OpenAPIV3.Document; + inquirer: typeof _inquirer; + promptArgs: PromptArgs; + writeFile: typeof _writeFile; + ensureDir: typeof _ensureDir; + pathExists: typeof _pathExists; + log?: LogService; +}): Promise { + return async () => { + const { type, name } = readArgs( + definition.arguments, + await promptArgs(), + ) as { type: string; name: string }; + const finalName = camelCase(name); + + if (name !== finalName) { + log('warning', '🐪 - Camelized the name:', finalName); + } + + if (type === 'handler' && !HANDLER_REG_EXP.test(finalName)) { + log( + 'error', + `💥 - The handler name is invalid, "${finalName}" does not match "${HANDLER_REG_EXP}".`, + ); + throw new YError('E_BAD_HANDLER_NAME', finalName, HANDLER_REG_EXP); + } + + const { services } = (await inquirer.prompt([ + { + name: 'services', + type: 'checkbox', + message: 'Which services do you want to use?', + choices: [ + ...Object.keys(commonServicesTypes), + ...Object.keys(whookSimpleTypes), + ...Object.keys(whookServicesTypes), + ], + }, + ])) as { services: string[] }; + + const servicesTypes = services + .sort() + .map(name => ({ + name, + type: allTypes[name], + })) + .concat( + type === 'command' ? [{ name: 'promptArgs', type: 'PromptArgs' }] : [], + ); + const parametersDeclaration = servicesTypes.length + ? `{${servicesTypes + .map( + ({ name }) => ` + ${name},`, + ) + .join('')} +}` + : ''; + const typesDeclaration = servicesTypes.length + ? `{${servicesTypes + .map( + ({ name, type }) => ` + ${name}: ${type};`, + ) + .join('')} +}` + : ''; + const commonServices = services.filter( + service => commonServicesTypes[service], + ); + const whookServices = services.filter( + service => whookServicesTypes[service], + ); + const imports = + (commonServices.length + ? ` +import { ${commonServices + .map(name => commonServicesTypes[name]) + .join(', ')} } from 'common-services';` + : '') + + (whookServices.length + ? ` +import { ${whookServices + .map(name => whookServicesTypes[name]) + .join(', ')} } from '@whook/whook';` + : '') + + (type === 'command' + ? ` +import { + PromptArgs, + WhookCommandArgs, + WhookCommandDefinition, + WhookCommandHandler, + readArgs, +} from '@whook/cli'; +` + : ''); + let fileSource = ''; + + if (type === 'handler') { + let baseQuestions = [ + { + name: 'method', + type: 'list', + message: 'Give the handler method', + choices: OPEN_API_METHODS, + default: [HANDLER_REG_EXP.exec(finalName)[1]].filter(maybeMethod => + OPEN_API_METHODS.includes(maybeMethod), + )[0], + }, + { + name: 'path', + type: 'input', + message: 'Give the handler path', + }, + { + name: 'description', + type: 'input', + message: 'Give the handler description', + }, + ] as _inquirer.Answers[]; + if (API.tags && API.tags.length) { + baseQuestions = [ + ...baseQuestions, + { + name: 'tags', + type: 'checkbox', + message: 'Assing one or more tags to the handler', + choices: API.tags.map(({ name }) => name), + }, + ]; + } + const { method, path, description, tags } = (await inquirer.prompt( + baseQuestions, + )) as { + method: string; + path: string; + description: string; + tags: string[]; + }; + fileSource = buildHandlerSource( + name, + path, + method, + description, + tags, + parametersDeclaration, + typesDeclaration, + imports, + ); + } else if (type === 'service') { + fileSource = buildServiceSource( + name, + parametersDeclaration, + typesDeclaration, + imports, + ); + } else if (type === 'provider') { + fileSource = buildProviderSource( + name, + parametersDeclaration, + typesDeclaration, + imports, + ); + } else if (type === 'command') { + const { description } = (await inquirer.prompt([ + { + name: 'description', + message: 'Give the command description', + }, + ])) as { description: string }; + + fileSource = buildCommandSource( + name, + description, + parametersDeclaration, + typesDeclaration, + imports, + ); + } else { + throw new YError('E_UNEXPECTED_TYPE'); + } + + const fileDir = path.join( + PROJECT_DIR, + 'src', + ['service', 'provider'].includes(type) + ? 'services' + : type === 'handler' + ? 'handlers' + : 'commands', + ); + await ensureDir(fileDir); + const filePath = path.join(fileDir, `${name}.ts`); + if (await pathExists(filePath)) { + log('warning', '⚠️ - The file already exists !'); + + const { erase } = (await inquirer.prompt([ + { + name: 'Erase ?', + type: 'confirm', + }, + ])) as { + erase: boolean; + }; + + if (!erase) { + return; + } + } + await writeFile(filePath, fileSource); + }; +} + +function buildHandlerSource( + name: string, + path: string, + method: string, + description: string = '', + tags: string[] = [], + parametersDeclaration, + typesDeclaration, + imports: string, +) { + return `import { autoHandler } from 'knifecycle'; +import { WhookResponse, WhookAPIHandlerDefinition } from '@whook/whook';${imports} + +export const definition: WhookAPIHandlerDefinition = { + path: '${path}', + method: '${method}', + operation: { + operationId: '${name}', + summary: '${description.replace(/'/g, "\\'")}', + tags: [${tags.map(tag => `'${tag}'`).join(', ')}], + parameters: [ + { + name: 'param', + in: 'query', + required: false, + schema: { type: 'number' }, + }, + ],${ + ['post', 'put', 'patch'].includes(method) + ? ` + requestBody: { + content: { + 'application/json': { + schema: { + type: 'object', + }, + }, + }, + },` + : '' + } + responses: { + 200: { + description: 'Success', + content: { + 'application/json': { + schema: { + type: 'object', + }, + }, + }, + }, + }, + }, +}; + +type HandlerDependencies = ${typesDeclaration || '{}'}; + +export default autoHandler(${name}); + +async function ${name}(${parametersDeclaration || '_'}: HandlerDependencies, { + param,${ + ['post', 'put'].includes(method) + ? ` + body,` + : '' + } + } : { + param: number;${ + ['post', 'put'].includes(method) + ? ` + body: {};` + : '' + } + }): Promise> { + return { + status: 200, + headers: {}, + body: { param }, + }; +} +`; +} + +function buildServiceSource( + name: string, + parametersDeclaration: string, + typesDeclaration: string, + imports: string, +) { + const upperCamelizedName = name[0].toLocaleUpperCase() + name.slice(1); + + return `import { autoService } from 'knifecycle';${imports} + +export type ${upperCamelizedName}Service = {}; +export type ${upperCamelizedName}Dependencies = ${typesDeclaration || '{}'}; + +export default autoService(init${upperCamelizedName}); + +async function init${upperCamelizedName}(${parametersDeclaration || + '_'}: ${upperCamelizedName}Dependencies): Promise<${upperCamelizedName}Service> { + // Instantiate and return your service + return {}; +} +`; +} + +function buildProviderSource( + name: string, + parametersDeclaration: string, + typesDeclaration: string, + imports: string, +) { + const upperCamelizedName = name[0].toLocaleUpperCase() + name.slice(1); + + return `import { autoProvider, Provider } from 'knifecycle';${imports} + +export type ${upperCamelizedName}Service = {}; +export type ${upperCamelizedName}Provider = Provider<${upperCamelizedName}Service>; +export type ${upperCamelizedName}Dependencies = ${typesDeclaration || '{}'}; + +export default autoProvider(init${upperCamelizedName}); + +async function init${upperCamelizedName}(${parametersDeclaration || + '_'}: ${upperCamelizedName}Dependencies): Promise<${upperCamelizedName}Provider> { + // Instantiate and return your service + return { + service: {}, + dispose: async () => { + // Do any action before the process shutdown + // (closing db connections... etc) + }, + // You can also set a promise for unexpected errors + // that shutdown the app when it happens + // errorPromise: new Promise(), + }; +} +`; +} + +function buildCommandSource( + name: string, + description: string, + parametersDeclaration: string, + typesDeclaration: string, + imports: string, +) { + const upperCamelizedName = name[0].toLocaleUpperCase() + name.slice(1); + + return `import { extra, autoService } from 'knifecycle';${imports} + +export const definition: WhookCommandDefinition = { + description: '${description.replace(/'/g, "\\'")}', + example: \`whook ${name} --param "value"\`, + arguments: { + type: 'object', + additionalProperties: false, + required: ['param'], + properties: { + param: { + description: 'A parameter', + type: 'string', + default: 'A default value', + }, + }, + }, +}; + +export default extra(definition, autoService(init${upperCamelizedName}Command)); + +async function init${upperCamelizedName}Command(${parametersDeclaration || + '_'}: ${typesDeclaration || {}}): Promise { + return async () => { + const { param } = readArgs( + definition.arguments, + await promptArgs(), + ) as { param: string; }; + + // Implement your command here + } +} +`; +} diff --git a/packages/whook-create/src/services/author.test.ts b/packages/whook-create/src/services/author.test.ts index b225274a..a18626ac 100644 --- a/packages/whook-create/src/services/author.test.ts +++ b/packages/whook-create/src/services/author.test.ts @@ -1,3 +1,4 @@ +import _inquirer from 'inquirer'; import initAuthor from './author'; describe('initAuthor', () => { @@ -28,7 +29,7 @@ describe('initAuthor', () => { lock.release.mockResolvedValueOnce(undefined); const author = await initAuthor({ - inquirer, + inquirer: (inquirer as unknown) as typeof _inquirer, exec, lock, log, @@ -55,7 +56,7 @@ describe('initAuthor', () => { lock.release.mockResolvedValueOnce(undefined); const author = await initAuthor({ - inquirer, + inquirer: (inquirer as unknown) as typeof _inquirer, exec, lock, log, @@ -80,7 +81,7 @@ describe('initAuthor', () => { try { await initAuthor({ - inquirer, + inquirer: (inquirer as unknown) as typeof _inquirer, exec, lock, log, diff --git a/packages/whook-create/src/services/project.test.ts b/packages/whook-create/src/services/project.test.ts index 019ef934..3cf32dba 100644 --- a/packages/whook-create/src/services/project.test.ts +++ b/packages/whook-create/src/services/project.test.ts @@ -1,3 +1,4 @@ +import _inquirer from 'inquirer'; import initProject from './project'; import YError from 'yerror'; @@ -28,7 +29,7 @@ describe('initProject', () => { ensureDir.mockResolvedValueOnce(undefined); const project = await initProject({ - inquirer, + inquirer: (inquirer as unknown) as typeof _inquirer, CWD, ensureDir, lock, @@ -58,7 +59,7 @@ describe('initProject', () => { try { await initProject({ - inquirer, + inquirer: (inquirer as unknown) as typeof _inquirer, CWD, ensureDir, lock, diff --git a/packages/whook-example/README.md b/packages/whook-example/README.md index b1a928ed..c6fe5d7c 100644 --- a/packages/whook-example/README.md +++ b/packages/whook-example/README.md @@ -31,6 +31,11 @@ Start the server in development mode: npm run dev ``` +Create a new endpoint / service / provider or command: +```sh +npx whook create +``` + List available commands: ```sh npx whook ls @@ -39,7 +44,7 @@ npx whook ls # Debug Execute a handler in isolation: ```sh -npm run whook -- handler --name putEcho --parameters '{"body": { "echo": "YOLO!" }}' +npx whook -- handler --name putEcho --parameters '{"body": { "echo": "YOLO!" }}' ``` Debug `whook` internals: @@ -47,7 +52,7 @@ Debug `whook` internals: DEBUG=whook npm run dev ``` -Debug `knifecycle` internals (dependency injections issues): +Debug `knifecycle` internals (dependency injection issues): ```sh DEBUG=knifecycle npm run dev ``` diff --git a/packages/whook-example/src/__snapshots__/cli.test.ts.snap b/packages/whook-example/src/__snapshots__/cli.test.ts.snap index 8ca365e4..5596c4c2 100644 --- a/packages/whook-example/src/__snapshots__/cli.test.ts.snap +++ b/packages/whook-example/src/__snapshots__/cli.test.ts.snap @@ -29,8 +29,9 @@ Object { - printEnv: A command printing every env values -# Provided by \\"@whook/cli\\": 4 commands +# Provided by \\"@whook/cli\\": 5 commands - config: A simple program that returns the queryed config value +- create: A command helping to create new Whook files easily - env: A command printing env values - handler: Runs the given server handler for testing purpose - ls: Print available commands diff --git a/packages/whook-example/src/services/API.ts b/packages/whook-example/src/services/API.ts index 80fae3ea..1f95f963 100644 --- a/packages/whook-example/src/services/API.ts +++ b/packages/whook-example/src/services/API.ts @@ -70,6 +70,11 @@ async function initAPI({ : {}), }, }, + tags: [ + { + name: 'system', + }, + ], }; // You can apply transformations to your API like diff --git a/packages/whook-example/src/services/__snapshots__/API.test.ts.snap b/packages/whook-example/src/services/__snapshots__/API.test.ts.snap index ac635354..a91ee1d4 100644 --- a/packages/whook-example/src/services/__snapshots__/API.test.ts.snap +++ b/packages/whook-example/src/services/__snapshots__/API.test.ts.snap @@ -433,6 +433,11 @@ Object { "url": "http://localhost:1337", }, ], + "tags": Array [ + Object { + "name": "system", + }, + ], }, "logCalls": Array [ Array [ diff --git a/packages/whook-http-router/src/index.ts b/packages/whook-http-router/src/index.ts index 206a8b83..4efec080 100644 --- a/packages/whook-http-router/src/index.ts +++ b/packages/whook-http-router/src/index.ts @@ -11,7 +11,11 @@ import { WhookOperation, HTTPTransactionService, } from '@whook/http-transaction'; -import { flattenOpenAPI, getOpenAPIOperations } from './utils'; +import { + OPEN_API_METHODS, + flattenOpenAPI, + getOpenAPIOperations, +} from './utils'; import { prepareParametersValidators, applyValidators, @@ -20,6 +24,8 @@ import { extractOperationSecurityParameters, } from './validation'; import { + BodySpec, + ResponseSpec, extractBodySpec, extractResponseSpec, checkResponseCharset, @@ -58,6 +64,7 @@ function identity(x) { } export { + OPEN_API_METHODS, DEFAULT_DEBUG_NODE_ENVS, DEFAULT_BUFFER_LIMIT, DEFAULT_PARSERS, @@ -74,6 +81,8 @@ export { ErrorHandlerConfig, flattenOpenAPI, getOpenAPIOperations, + BodySpec, + ResponseSpec, }; export type WhookHandlers = { [name: string]: WhookHandler }; diff --git a/packages/whook-http-router/src/utils.ts b/packages/whook-http-router/src/utils.ts index 293ff824..d9e8fa3c 100644 --- a/packages/whook-http-router/src/utils.ts +++ b/packages/whook-http-router/src/utils.ts @@ -3,7 +3,7 @@ import YError from 'yerror'; import { OpenAPIV3 } from 'openapi-types'; import { WhookOperation } from '@whook/http-transaction'; -const OPEN_API_METHODS = [ +export const OPEN_API_METHODS = [ 'options', 'head', 'get', diff --git a/packages/whook/src/index.ts b/packages/whook/src/index.ts index 54810c16..ad8017d9 100644 --- a/packages/whook/src/index.ts +++ b/packages/whook/src/index.ts @@ -11,6 +11,7 @@ import { ProcessServiceConfig, } from 'common-services'; import initHTTPRouter, { + OPEN_API_METHODS, DEFAULT_ERROR_URI, DEFAULT_ERRORS_DESCRIPTORS, DEFAULT_DEFAULT_ERROR_CODE, @@ -113,6 +114,7 @@ export { WhookHandler, WhookHandlerFunction, WhookWrapper, + OPEN_API_METHODS, HTTPTransactionConfig, HTTPTransactionService, HTTPRouterConfig,