├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode └── settings.json ├── LICENSE.md ├── README.md ├── _templates ├── insert │ └── packageJson │ │ └── packageJson.ejs └── make │ ├── genericFunctions │ ├── genericFunctions.ejs │ └── index.js │ ├── regularNodeFile │ ├── index.js │ └── regularNodeFile.ejs │ ├── resourceDescription │ ├── index.js │ └── resourceDescription.ejs │ └── resourceIndex │ ├── index.ejs │ └── index.js ├── customSpecSchema.json ├── docs ├── custom-spec-syntax.md ├── icons8-product-documents-64.png ├── logo.png └── screenshot.png ├── package-lock.json ├── package.json ├── src ├── config │ └── index.ts ├── input │ ├── .gitkeep │ ├── custom │ │ ├── .gitkeep │ │ └── copper.yaml │ └── openApi │ │ ├── .gitkeep │ │ └── lichess.json ├── output │ └── .gitkeep ├── scripts │ ├── clear.ts │ ├── dev.ts │ ├── generate.ts │ ├── map.ts │ ├── place.ts │ └── render.ts ├── services │ ├── CustomSpecAdjuster.ts │ ├── CustomSpecStager.ts │ ├── NodeCodeGenerator.ts │ ├── OpenApiStager.ts │ ├── OutputPlacer.ts │ ├── PackageJsonGenerator.ts │ ├── Prompter.ts │ ├── TemplateBuilder.ts │ ├── TemplateHelper.ts │ └── templating │ │ ├── ApiCallBuilder.ts │ │ ├── BranchBuilder.ts │ │ ├── DividerBuilder.ts │ │ ├── ImportsBuilder.ts │ │ └── ResourceBuilder.ts ├── types │ ├── augmentations.d.ts │ ├── params.d.ts │ ├── printer.d.ts │ └── utils.d.ts └── utils │ ├── FilePrinter.ts │ └── TreeRenderer.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | 4 | src/input/* 5 | src/input/openApi/* 6 | !src/input/openApi/ 7 | !src/input/openApi/.gitkeep 8 | 9 | src/input/custom/* 10 | !src/input/custom/ 11 | !src/input/custom/.gitkeep 12 | !src/input/custom/copper.yaml 13 | 14 | src/output/* 15 | !src/output/.gitkeep 16 | !src/input/openApi/lichess.json 17 | 18 | *.todo 19 | *.txt 20 | 21 | .DS_store -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | src/input/*.json 2 | src/output/**/*.ts 3 | *.yaml -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": false, 4 | "tabWidth": 2, 5 | "trailingComma": "es5" 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[typescript]": { 3 | "editor.formatOnSave": true, 4 | "editor.defaultFormatter": "esbenp.prettier-vscode" 5 | }, 6 | "[json]": { 7 | "editor.formatOnSave": true, 8 | "editor.defaultFormatter": "esbenp.prettier-vscode" 9 | }, 10 | "[yaml]": { 11 | "editor.formatOnSave": true, 12 | "editor.defaultFormatter": "esbenp.prettier-vscode" 13 | }, 14 | 15 | "search.exclude": { 16 | "dist": true, 17 | "node_modules": true 18 | }, 19 | "files.exclude": { 20 | "node_modules": true 21 | }, 22 | "typescript.tsdk": "node_modules\\typescript\\lib", 23 | 24 | "yaml.schemas": { 25 | "customSpecSchema.json": "mySpec.yaml" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | © 2021 Iván Ovejero 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Nodemaker 3 |

4 | 5 |

6 |

Nodebuilder

7 |

8 | 9 |

10 | Build n8n nodes from OpenAPI specs and custom API mappings
11 | by Iván Ovejero 12 |

13 | 14 |

15 | Installation • 16 | Operation • 17 | Custom Spec Syntax 18 |

19 | 20 |

21 | 22 | 23 | 24 |

25 | 26 |
27 | 28 | **Nodebuilder** is a utility to generate n8n node files from 29 | 30 | - [OpenAPI specs in JSON and YAML](https://github.com/OAI/OpenAPI-Specification) and 31 | - [custom API mappings in YAML](#yaml). 32 | 33 | Developed to automate the node creation process for: 34 | 35 | - `*.node.ts`, logic for a regular node, 36 | - `GenericFunctions.ts`, helper functions, and 37 | - `*Description.ts`, parameter descriptions. 38 | 39 | ## Installation 40 | 41 | ```sh 42 | $ git clone https://github.com/ivov/nodebuilder.git 43 | $ cd nodebuilder && npm i 44 | ``` 45 | 46 | ## Operation 47 | 48 | ### OpenAPI 49 | 50 | 1. Place the input file in `src/input/openApi/` 51 | 2. Run `npm run generate` 52 | 3. Inspect `src/output/` 53 | 54 | Notes: 55 | - OpenAPI parsing may fail at undetected edge cases. If your spec cannot be parsed, please open an issue. 56 | - OpenAPI parsing needs to be adjusted to respect n8n's resources-and-operations format. Work in progress. 57 | 58 | ### YAML 59 | 60 | 1. Write a YAML file in `src/input/custom/` 61 | 2. Run `npm run generate` 62 | 3. Inspect `src/output/` 63 | 64 | For a full description of how to describe an API in YAML, refer to [this explanation](https://github.com/ivov/nodebuilder/blob/main/docs/yaml-mapping.md). 65 | 66 | For a full example of an API description in YAML, refer to [`copper.yaml`](https://github.com/ivov/nodebuilder/blob/main/src/input/custom/copper.yaml). 67 | 68 | ### Placement 69 | 70 | Run `npm run place` to place the output files in: 71 | 72 | - an n8n clone dir (located alongside the nodebuilder dir), or 73 | - the default custom nodes dir at `~/.n8n/custom`. 74 | 75 | ## Pending 76 | 77 | **OpenAPI:** 78 | - Add intermediate step to structure the result. 79 | - Add support for more content types. 80 | 81 | **YAML:** 82 | - Add support for `multiOptions` 83 | 84 | **Generator:** 85 | - Add resource loaders to the TypeScript generator. 86 | - Generate `*.credentials.ts` 87 | 88 | **Extras:** 89 | - Implement testing with [`git.js`](https://github.com/steveukx/git-js) 90 | - Explore integration with [VSCode YAML](https://github.com/redhat-developer/vscode-yaml) 91 | 92 | ## Author 93 | 94 | © 2021 [Iván Ovejero](https://github.com/ivov) 95 | 96 | ## License 97 | 98 | Distributed under the MIT License. See [LICENSE.md](LICENSE.md). 99 | -------------------------------------------------------------------------------- /_templates/insert/packageJson/packageJson.ejs: -------------------------------------------------------------------------------- 1 | --- 2 | inject: true 3 | nodeSpot: <%= nodeSpot %> 4 | serviceName: <%= serviceName %> 5 | to: src/output/package.json 6 | before: \s+"dist\/nodes\/<%= nodeSpot %>.node.js", 7 | --- 8 | "dist/nodes/<%= serviceName %>/<%= serviceName %>.node.js", -------------------------------------------------------------------------------- /_templates/make/genericFunctions/genericFunctions.ejs: -------------------------------------------------------------------------------- 1 | --- 2 | to: src/output/GenericFunctions.ts 3 | --- 4 | <%_ const helper = new Helper(); _%> 5 | <%_ const serviceName = helper.camelCase(metaParams.serviceName); _%> 6 | <%_ const credentialsName = helper.getCredentialsString(metaParams.serviceName, metaParams.authType); _%> 7 | import { 8 | IExecuteFunctions, 9 | } from 'n8n-core'; 10 | 11 | import { 12 | IDataObject, 13 | NodeApiError, 14 | NodeOperationError, 15 | } from 'n8n-workflow'; 16 | 17 | import { 18 | OptionsWithUri, 19 | } from 'request'; 20 | 21 | export async function <%= serviceName %>ApiRequest( 22 | this: IExecuteFunctions, 23 | method: string, 24 | endpoint: string, 25 | body: IDataObject = {}, 26 | qs: IDataObject = {}, 27 | uri?: string, 28 | ) { 29 | const options: OptionsWithUri = { 30 | headers: {}, 31 | method, 32 | body, 33 | qs, 34 | uri: uri ?? `<%= metaParams.apiUrl; %>${endpoint}`, 35 | json: true, 36 | }; 37 | 38 | <%_ if (metaParams.authType !== "None") { _%> 39 | const credentials = await this.getCredentials('<%= credentialsName %>'); 40 | 41 | if (credentials === undefined) { 42 | throw new NodeOperationError(this.getNode(), 'No credentials got returned!'); 43 | } 44 | <%_ } _%> 45 | 46 | if (!Object.keys(body).length) { 47 | delete options.body; 48 | } 49 | 50 | if (!Object.keys(qs).length) { 51 | delete options.qs; 52 | } 53 | 54 | try { 55 | <%_ if (metaParams.authType === "OAuth2") { _%> 56 | return await this.helpers.requestOAuth2.call(this, '<%= credentialsName %>', options); 57 | <%_ } else { _%> 58 | return await this.helpers.request(options); 59 | <%_ } _%> 60 | } catch (error) { 61 | throw new NodeApiError(this.getNode(), error); 62 | } 63 | } 64 | 65 | export async function <%= serviceName %>ApiRequestAllItems( 66 | this: IExecuteFunctions, 67 | method: string, 68 | endpoint: string, 69 | body: IDataObject = {}, 70 | qs: IDataObject = {}, 71 | ) { 72 | const returnData: IDataObject[] = []; 73 | let responseData: any; 74 | 75 | do { 76 | responseData = await <%= serviceName %>ApiRequest.call(this, method, endpoint, body, qs); 77 | // USERTASK: Get next page 78 | returnData.push(...responseData); 79 | } while ( 80 | true // USERTASK: Add condition for total not yet reached 81 | ); 82 | 83 | return returnData; 84 | } 85 | 86 | export async function handleListing( 87 | this: IExecuteFunctions, 88 | method: string, 89 | endpoint: string, 90 | body: IDataObject = {}, 91 | qs: IDataObject = {}, 92 | ) { 93 | const returnAll = this.getNodeParameter('returnAll', 0) as boolean; 94 | 95 | if (returnAll) { 96 | return await <%= serviceName %>ApiRequestAllItems.call(this, method, endpoint, body, qs); 97 | } 98 | 99 | const responseData = await <%= serviceName %>ApiRequestAllItems.call(this, method, endpoint, body, qs); 100 | const limit = this.getNodeParameter('limit', 0) as number; 101 | 102 | return responseData.slice(0, limit); 103 | } 104 | -------------------------------------------------------------------------------- /_templates/make/genericFunctions/index.js: -------------------------------------------------------------------------------- 1 | const nodegenParams = require("../../../src/input/_nodegenParams.json"); 2 | const { Helper } = require("../../../dist/services/TemplateHelper"); 3 | 4 | module.exports = { 5 | params: () => ({ ...nodegenParams, Helper }) 6 | }; 7 | -------------------------------------------------------------------------------- /_templates/make/regularNodeFile/index.js: -------------------------------------------------------------------------------- 1 | const nodegenParams = require("../../../src/input/_nodegenParams.json"); 2 | const { Helper } = require("../../../dist/services/TemplateHelper"); 3 | const { Builder } = require("../../../dist/services/TemplateBuilder"); 4 | const { pascalCase } = require("change-case"); // separately for file path generation 5 | 6 | module.exports = { 7 | params: () => ({ ...nodegenParams, Helper, Builder, pascalCase }) 8 | }; 9 | -------------------------------------------------------------------------------- /_templates/make/regularNodeFile/regularNodeFile.ejs: -------------------------------------------------------------------------------- 1 | --- 2 | to: src/output/<%= pascalCase(metaParams.serviceName) %>.node.ts 3 | --- 4 | <%_ const builder = new Builder(mainParams, metaParams); _%> 5 | <%_ const helper = new Helper(); _%> 6 | import { 7 | IExecuteFunctions, 8 | } from 'n8n-core'; 9 | 10 | import { 11 | IDataObject, 12 | INodeExecutionData, 13 | INodeType, 14 | INodeTypeDescription, 15 | } from 'n8n-workflow'; 16 | 17 | import { 18 | <%= builder.genericFunctionsImports(); %> 19 | } from './GenericFunctions'; 20 | 21 | import { 22 | <%_ builder.resourceNames.forEach((resourceName) => { _%> 23 | <%= helper.camelCase(resourceName) %>Fields, 24 | <%= helper.camelCase(resourceName) %>Operations, 25 | <%_ }); _%> 26 | } from './descriptions'; 27 | 28 | export class <%= helper.pascalCase(metaParams.serviceName); %> implements INodeType { 29 | description: INodeTypeDescription = { 30 | displayName: '<%= metaParams.serviceName %>', 31 | name: '<%= helper.camelCase(metaParams.serviceName) %>', 32 | icon: 'file:<%= helper.camelCase(metaParams.serviceName) %>.png', 33 | group: ['transform'], 34 | version: 1, 35 | subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', 36 | description: 'Consume the <%= metaParams.serviceName %> API', 37 | defaults: { 38 | name: '<%= metaParams.serviceName %>', 39 | color: '<%= metaParams.nodeColor %>', 40 | }, 41 | inputs: ['main'], 42 | outputs: ['main'], 43 | properties: [ 44 | { 45 | displayName: 'Resource', 46 | name: 'resource', 47 | type: 'options', 48 | options: [ 49 | <%_ builder.resourceNames.forEach((resourceName) => { _%> 50 | { 51 | name: '<%= helper.titleCase(resourceName); %>', 52 | value: '<%= helper.camelCase(resourceName); %>', 53 | }, 54 | <%_ }); _%><%#_ end resourceNames loop _%> 55 | ], 56 | default: '<%= builder.resourceNames[0].toLowerCase() %>', 57 | }, 58 | <%_ builder.resourceNames.forEach((resourceName) => { _%> 59 | ...<%= helper.camelCase(resourceName) %>Operations, 60 | ...<%= helper.camelCase(resourceName) %>Fields, 61 | <%_ }); _%><%#_ end resourceNames loop _%> 62 | ], <%# end properties key of object at description class field %> 63 | }; <%# end description class field %> 64 | 65 | async execute(this: IExecuteFunctions): Promise { 66 | const items = this.getInputData(); 67 | const returnData: IDataObject[] = []; 68 | 69 | const resource = this.getNodeParameter('resource', 0) as string; 70 | const operation = this.getNodeParameter('operation', 0) as string; 71 | 72 | let responseData; 73 | 74 | for (let i = 0; i < items.length; i++) { 75 | <%_ builder.resourceTuples.forEach(([resourceName, operationsArray]) => { %> 76 | <%- builder.resourceBranch(resourceName); %> 77 | 78 | <%= builder.resourceDivider(helper.camelCase(resourceName)); %> 79 | 80 | <%_ operationsArray.forEach(operation => { _%> 81 | <%- builder.operationBranch(resourceName, operation); %> 82 | 83 | <%= builder.operationDivider(helper.camelCase(resourceName), operation.operationId, operation.operationUrl); %> 84 | 85 | <%- builder.apiCall(operation); _%> 86 | 87 | <%- builder.operationError(resourceName, operation, { enabled: false }); _%> 88 | 89 | <%_ }); _%> <%#_ end operationsArray loop _%> 90 | <%- builder.resourceError(resourceName, { enabled: false }); _%> 91 | <%_ }); _%> <%#_ end resourceTuples loop _%> 92 | 93 | 94 | Array.isArray(responseData) 95 | ? returnData.push(...responseData) 96 | : returnData.push(responseData); 97 | 98 | } 99 | 100 | return [this.helpers.returnJsonArray(returnData)]; 101 | } 102 | } <%# end class %> -------------------------------------------------------------------------------- /_templates/make/resourceDescription/index.js: -------------------------------------------------------------------------------- 1 | const resource = require("../../../src/input/_resource.json"); 2 | const nodegenParams = require("../../../src/input/_nodegenParams.json"); 3 | const { Helper } = require("../../../dist/services/TemplateHelper"); 4 | const { Builder } = require("../../../dist/services/TemplateBuilder"); 5 | const { pascalCase } = require("change-case"); // separately for file path generation 6 | 7 | module.exports = { 8 | params: () => ({ ...resource, Helper, Builder, pascalCase, ...nodegenParams }), 9 | }; 10 | -------------------------------------------------------------------------------- /_templates/make/resourceDescription/resourceDescription.ejs: -------------------------------------------------------------------------------- 1 | --- 2 | to: src/output/descriptions/<%= pascalCase(resourceName) %>Description.ts 3 | --- 4 | <%_ const helper = new Helper(); _%> 5 | <%_ const builder = new Builder(mainParams, metaParams); _%> 6 | import { 7 | INodeProperties, 8 | } from 'n8n-workflow'; 9 | 10 | export const <%= helper.camelCase(resourceName) %>Operations: INodeProperties[] = [ 11 | { 12 | displayName: 'Operation', 13 | name: 'operation', 14 | type: 'options', 15 | displayOptions: { 16 | show: { 17 | resource: [ 18 | '<%= resourceName %>', 19 | ], 20 | }, 21 | }, 22 | options: [ 23 | <%- builder.operationsOptions(operationsArray); %> 24 | ], 25 | default: '<%= operationsArray[0].operationId %>', 26 | }, 27 | ]; 28 | 29 | export const <%= helper.camelCase(resourceName) %>Fields: INodeProperties[] = [ 30 | <%_ operationsArray.forEach(operation => { _%> 31 | <%_ if (operation.parameters) { _%> 32 | <%= builder.resourceDescriptionDivider(resourceName, operation.operationId); %> 33 | <%_ } _%> 34 | <%_/** 35 | * ********************** 36 | * PATH PARAMS & QS PARAMS 37 | * ********************** 38 | */_%> 39 | <%_ if (operation.parameters) { _%> 40 | <%_ operation.parameters.forEach(param => { _%> 41 | { 42 | displayName: '<%= helper.titleCase(param.name); %>', 43 | name: '<%= param.name %>', 44 | description: '<%- param.description; %>', 45 | type: '<%= helper.adjustType(param.schema.type, param.name); %>', 46 | <%_ if (helper.hasMinMax(param.schema)) { _%> 47 | typeOptions: { 48 | minValue: <%= param.schema.minimum %>, 49 | maxValue: <%= param.schema.maximum %> 50 | }, 51 | <%_ } _%> 52 | <%_ if (param.required) { _%> 53 | required: true, 54 | <%_ } _%> 55 | <%_ if (param.schema.type === 'options') { _%> 56 | options: [ 57 | <%_ param.schema.options.forEach(option => { _%> 58 | { 59 | name: '<%= helper.titleCase(option) %>', 60 | value: '<%= option %>', 61 | }, 62 | <%_ }); _%> 63 | ], 64 | <%_ } _%> 65 | default: <%- helper.getDefault(param.schema); %>, 66 | displayOptions: { 67 | show: { 68 | resource: [ 69 | '<%= resourceName %>', 70 | ], 71 | operation: [ 72 | '<%= operation.operationId; %>', 73 | ], 74 | }, 75 | }, 76 | }, 77 | <%_ }); _%> <%#_ end parameters loop _%> 78 | <%_ } _%> <%#_ end parameters if _%> 79 | <%_/** 80 | * ********************** 81 | * REQUEST BODY ITEMS 82 | * ********************** 83 | */_%> 84 | <%_ if (operation.requestBody) { _%> 85 | <%_ operation.requestBody.forEach((rbItem) => { _%> 86 | <%_ if (rbItem.required) { _%> 87 | <%_ Object.keys(rbItem.content).forEach((mimeType) => { _%> 88 | <%_ if (mimeType === "application/x-www-form-urlencoded") { _%> 89 | <%_ const properties = rbItem.content[mimeType].schema.properties; _%> 90 | <%_ Object.entries(properties).forEach(([property, value]) => { _%> 91 | { 92 | displayName: '<%= helper.titleCase(property); %>', 93 | name: '<%= helper.camelCase(property) %>', 94 | <%_ if (value.description) { _%> 95 | description: '<%- helper.escape(value.description); %>', 96 | <%_ } _%> 97 | type: '<%= helper.adjustType(value.type, property); %>', 98 | <%_ if (helper.hasMinMax(value)) { _%> 99 | typeOptions: { 100 | minValue: <%= value.minimum %>, 101 | maxValue: <%= value.maximum %> 102 | }, 103 | <%_ } _%> 104 | required: true, 105 | default: <%- helper.getDefault(value); %>, 106 | <%_ if (value.type === 'options') { _%> 107 | options: [ 108 | <%_ value.options.forEach(option => { _%> 109 | { 110 | name: '<%= helper.titleCase(option) %>', 111 | value: '<%= option %>', 112 | }, 113 | <%_ }); _%> 114 | ], 115 | <%_ } _%> 116 | displayOptions: { 117 | show: { 118 | resource: [ 119 | '<%= resourceName; %>', 120 | ], 121 | operation: [ 122 | '<%= operation.operationId; %>', 123 | ], 124 | }, 125 | }, 126 | }, 127 | <%_ }); _%> <%#_ end properties loop _%> 128 | <%_ } else if (mimeType === "text/plain") { _%> 129 | { 130 | displayName: '<%= helper.titleCase(rbItem.textPlainProperty); %>', 131 | name: '<%= helper.camelCase(rbItem.textPlainProperty); %>', 132 | <%_ if (rbItem.description) { _%> 133 | description: '<%- helper.escape(rbItem.description); %>', 134 | <%_ } _%> 135 | type: 'string', 136 | required: true, 137 | default: '', 138 | displayOptions: { 139 | show: { 140 | resource: [ 141 | '<%= resourceName; %>', 142 | ], 143 | operation: [ 144 | '<%= operation.operationId; %>', 145 | ], 146 | }, 147 | }, 148 | }, 149 | <%_ } _%> <%#_ end mimeType if _%> 150 | <%_ }); _%> <%#_ end mimeType loop _%> 151 | <%_ } else if (!rbItem.required) { _%> 152 | { 153 | displayName: '<%= rbItem.name %>', 154 | name: '<%= helper.camelCase(rbItem.name) %>', 155 | type: 'collection', 156 | placeholder: '<%= helper.getPlaceholder(rbItem.name); %>', 157 | default: {}, 158 | displayOptions: { 159 | show: { 160 | resource: [ 161 | '<%= resourceName; %>', 162 | ], 163 | operation: [ 164 | '<%= operation.operationId; %>', 165 | ], 166 | }, 167 | }, 168 | options: [ 169 | <%_ Object.keys(rbItem.content).forEach((mimeType) => { _%> 170 | <%_ if (mimeType === "application/x-www-form-urlencoded") { _%> 171 | <%_ const schema = rbItem.content[mimeType].schema; _%> 172 | <%_ Object.entries(schema.properties).forEach(([key, value]) => { _%> 173 | <%_ if (value.type === 'string' || value.type === 'number' || value.type === 'boolean') { _%> 174 | <%_/** 175 | * non-nested field inside extraFields 176 | */_%> 177 | { 178 | displayName: '<%= helper.titleCase(key); %>', 179 | name: '<%= key %>', 180 | type: '<%= helper.adjustType(value.type, key); %>', 181 | default: <%- helper.getDefault(value); %>, 182 | <%_ if (value.description) { _%> 183 | description: '<%- helper.escape(value.description) %>', 184 | <%_ } _%> 185 | }, 186 | <%_ } else if (value.type === 'loadOptions') { _%> 187 | { 188 | displayName: '<%= helper.titleCase(key); %>', 189 | name: '<%= key %>', 190 | type: 'options', 191 | default: '', 192 | typeOptions: { 193 | loadOptionsMethod: 'get<%= helper.pascalCase(key) %>s', 194 | }, 195 | description: '<%= value.description %>', 196 | }, 197 | <%_ } else if (value.type === 'options') { _%> 198 | { 199 | displayName: '<%= helper.titleCase(key); %>', 200 | name: '<%= key %>', 201 | type: 'options', 202 | default: '<%= value.default %>', 203 | <%_ if (value.description) { _%> 204 | description: '<%- helper.escape(value.description) %>', 205 | <%_ } _%> 206 | options: [ 207 | <%_ value.options.forEach(option => { _%> 208 | { 209 | name: '<%= helper.titleCase(option) %>', 210 | value: '<%= option %>', 211 | }, 212 | <%_ }); _%> 213 | ], 214 | }, 215 | <%_ } else if (Array.isArray(value) && value.length > 1) { _%> 216 | <%_/** 217 | * options (dropdown) inside extraFields 218 | * TODO: Is this branch working or is this now covered by the previous branch? 219 | */_%> 220 | { 221 | displayName: '<%= helper.titleCase(key); %>', 222 | name: '<%= key %>', 223 | type: 'options', 224 | default: '<%= Object.keys(value[0])[0] %>', 225 | options: [ 226 | <%_ value.forEach(suboption => { _%> 227 | <%_ Object.keys(suboption).forEach(subKey => { _%> 228 | { 229 | name: '<%= helper.titleCase(subKey) %>', 230 | value: '<%= subKey %>', 231 | }, 232 | <%_ }); _%> 233 | <%_ }); _%> 234 | ], 235 | }, 236 | <%_ } else if (Array.isArray(value) && value.length === 1) { _%> 237 | <%_/** 238 | * fixedCollection with multipleValues inside extraFields 239 | */_%> 240 | { 241 | displayName: '<%= helper.titleCase(key); %>', 242 | name: '<%= key %>', 243 | type: 'fixedCollection', 244 | default: {}, 245 | placeholder: 'Add <%= helper.titleCase(key + " Field") %>', 246 | typeOptions: { 247 | multipleValues: true, 248 | }, 249 | options: [ 250 | { 251 | displayName: '<%= helper.titleCase(key + " Fields") %>', 252 | name: '<%= helper.addFieldsSuffix(key) %>', 253 | values: [ 254 | <%_ value.forEach(i => { _%> 255 | <%_ Object.keys(i).forEach(subKey => { _%> 256 | { 257 | displayName: '<%= helper.titleCase(subKey) %>', 258 | name: '<%= subKey %>', 259 | type: 'string', 260 | default: '', 261 | }, 262 | <%_ }); _%> 263 | <%_ }); _%> 264 | ], 265 | }, 266 | ], 267 | }, 268 | <%_ } else if (!value.type && !Array.isArray(value)) { _%> 269 | <%_/** 270 | * fixedCollection without multipleValues inside extraFields 271 | */_%> 272 | { 273 | displayName: '<%= helper.titleCase(key); %>', 274 | name: '<%= key %>', 275 | type: 'fixedCollection', 276 | default: {}, 277 | placeholder: 'Add <%= helper.titleCase(key + " Field") %>', 278 | options: [ 279 | { 280 | displayName: '<%= helper.titleCase(key + " Fields") %>', 281 | name: '<%= helper.addFieldsSuffix(key) %>', 282 | values: [ 283 | <%_ Object.keys(value).forEach(subKey => { _%> 284 | { 285 | displayName: '<%= helper.titleCase(subKey) %>', 286 | name: '<%= subKey %>', 287 | type: 'string', 288 | default: '', 289 | }, 290 | <%_ }); _%> 291 | ], 292 | }, 293 | ], 294 | }, 295 | <%_ } _%> 296 | <%_ }); _%> <%#_ end properties loop _%> 297 | <%_ } _%> <%#_ end mimeType if _%> 298 | <%_ }); _%> <%#_ end mimeType loop _%> 299 | ], 300 | }, 301 | <%_ } _%> <%#_ end rbItem.required if _%> 302 | <%_ }); _%> <%#_ end rbItem loop _%> 303 | <%_ } _%> <%#_ end requestBody if _%> 304 | <%_ if (operation.operationId === 'getAll') { _%> 305 | <%- builder.getAllAdditions(resourceName, operation.operationId); %> 306 | <%_ } _%> 307 | <%_ }); _%> <%#_ end operations loop _%> 308 | ]; 309 | -------------------------------------------------------------------------------- /_templates/make/resourceIndex/index.ejs: -------------------------------------------------------------------------------- 1 | --- 2 | to: src/output/descriptions/index.ts 3 | --- 4 | <%_ resourceNames.forEach(resourceName => { _%> 5 | export * from './<%= resourceName %>Description'; 6 | <%_ }); _%> 7 | -------------------------------------------------------------------------------- /_templates/make/resourceIndex/index.js: -------------------------------------------------------------------------------- 1 | const nodegenParams = require("../../../src/input/_nodegenParams.json"); 2 | const { pascalCase } = require("change-case"); 3 | 4 | const resourceNames = Object.keys(nodegenParams.mainParams).map(resourceName => pascalCase(resourceName)); 5 | 6 | module.exports = { 7 | params: () => ({ resourceNames }) 8 | }; 9 | -------------------------------------------------------------------------------- /customSpecSchema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "Custom Spec Schema", 4 | "description": "JSON schema for custom API mapping for nodebuilder", 5 | "type": "object", 6 | "additionalProperties": false, 7 | "properties": { 8 | "metaParams": { 9 | "type": "object", 10 | "properties": { 11 | "apiUrl": { 12 | "type": "string", 13 | "description": "Base URL of the API" 14 | }, 15 | "authType": { 16 | "type": "string", 17 | "description": "abc", 18 | "oneOf": [ 19 | { 20 | "const": "OAuth2" 21 | }, 22 | { 23 | "const": "ApiKey" 24 | }, 25 | { 26 | "const": "None" 27 | } 28 | ] 29 | }, 30 | "serviceName": { 31 | "type": "string", 32 | "description": "abc" 33 | }, 34 | "nodeColor": { 35 | "type": "string", 36 | "description": "abc" 37 | } 38 | } 39 | }, 40 | 41 | "mainParams": { 42 | "type": "object", 43 | "properties": { 44 | "/": {} 45 | }, 46 | "patternProperties": { 47 | "^.+$": { 48 | "type": "array", 49 | "description": "Resource", 50 | "items": { 51 | "type": "object", 52 | "properties": { 53 | "endpoint": { 54 | "type": "string" 55 | }, 56 | "operationId": { 57 | "type": "string" 58 | }, 59 | "requestMethod": { 60 | "type": "string", 61 | "oneOf": [ 62 | { 63 | "const": "GET" 64 | }, 65 | { 66 | "const": "POST" 67 | }, 68 | { 69 | "const": "PUT" 70 | }, 71 | { 72 | "const": "DELETE" 73 | } 74 | ] 75 | } 76 | } 77 | } 78 | } 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /docs/custom-spec-syntax.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

6 |

Custom Spec Synax

7 |

8 | 9 |

10 | Syntax to describe an API in YAML to generate an n8n node 11 |

12 | 13 |
14 | 15 | Nodebuilder can generate an n8n node from a custom spec, i.e. a shorthand YAML description of an API. 16 | 17 | Install [YAML Support](https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml) for VSCode. 18 | 19 | ## API-level keys 20 | 21 | The YAML file must have two top-level keys: `metaParams` and `mainParams` 22 | 23 | `metaParams` contains four required properties of the API itself: 24 | 25 | ```yaml 26 | metaParams: 27 | apiUrl: https://api.myservice.com/ # base API URL 28 | authType: OAuth2 # one of "OAuth2", "ApiKey", or "None" 29 | serviceName: MyService # properly cased service name 30 | nodeColor: \#ff2564 # brand hex color, escaped by / 31 | ``` 32 | 33 | Used in the node class `description`: 34 | 35 | ```ts 36 | export class MyService implements INodeType { 37 | description: INodeTypeDescription = { 38 | displayName: 'MyService', 39 | name: 'myService', 40 | icon: 'file:myService.png', 41 | group: ['transform'], 42 | version: 1, 43 | subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', 44 | description: 'Consume the MyService API', 45 | defaults: { 46 | name: 'MyService', 47 | color: '#ff2564', 48 | }, 49 | inputs: ['main'], 50 | outputs: ['main'], 51 | properties: [ 52 | // ... 53 | ``` 54 | 55 | `mainParams` contains one or more resources as keys, each pointing to an array of one or more operations: 56 | 57 | ```yaml 58 | mainParams: 59 | company: 60 | - endpoint: /companies 61 | operationId: getAll 62 | operationUrl: https://myservice.com/api/get-all-companies 63 | requestMethod: GET 64 | - endpoint: /companies 65 | operationId: create 66 | operationUrl: https://myservice.com/api/create-a-company 67 | requestMethod: POST 68 | employee: 69 | - endpoint: /employees 70 | operationId: create 71 | operationUrl: https://myservice.com/api/create-an-employee 72 | requestMethod: POST 73 | ``` 74 | 75 | **Notes** 76 | 77 | - If `endpoint` contains a bracketed string `{...}`, this will be parsed as a path parameter. Therefore, no need to create a parameter for any path parameters. 78 | 79 | ```yaml 80 | - endpoint: /companies/{companyId} 81 | operationId: get 82 | requestMethod: GET 83 | ``` 84 | 85 | ## Operation-level keys 86 | 87 | Each operation in the array has the following keys. 88 | 89 | Required keys: 90 | 91 | - `endpoint`, the third party's API endpoint to call, 92 | - `operationId`, the ID of the operation in the n8n node, and 93 | - `requestMethod`, the HTTP method to use for the call. 94 | 95 | Optional keys: 96 | 97 | - `operationUrl`, a link to documentation on the operation, 98 | - `requiredFields`, fields that are needed for a call to succeed, and 99 | - optional fields, fields that are not needed for a call to succeed: 100 | - `additionalFields`, optional fields in general, 101 | - `filters`, optional fields for listing operations, and 102 | - `updateFields`, optional fields for updating operations. 103 | 104 | Note that the "required" and "optional" keys refer to nodebuilder args, whereas `requiredFields` and "optional fields" refer to fields that may or may not be needed by the API for a call to succeed. 105 | 106 | ## Field-level keys 107 | 108 | Field-level keys are contains for the params to be sent in the call. Each field-level property must contain one single key, either `queryString` or `requestBody`, based on the request method. Refer to the API documentation. 109 | 110 | ```yaml 111 | - endpoint: /companies 112 | operationId: create 113 | requestMethod: POST 114 | requiredFields: 115 | requestBody: 116 | # ... 117 | ``` 118 | 119 | Inside `queryString` or `requestBody`, 120 | 121 | - each key must be a param, cased per the API documentation. 122 | - each value must specify its type: 123 | - `string`, `number`, `boolean` and `dateTime` for simple values, e.g. `is_active`, 124 | - `options` for individual options in a dropdown, e.g. `classification`, 125 | - `loadOptions` for options to be loaded remotely, e.g. `accounts`, 126 | - an object for a `fixedCollection` with a single set of fields, e.g. `address`, 127 | - an array of objects for a `fixedCollection` with a multiple sets of fields, e.g. `phone_numbers`, 128 | 129 | ```yaml 130 | updateFields: 131 | requestBody: 132 | description: string|Arbitrary text to describe the company 133 | employees: number|Number of the employees at the company 134 | is_active: boolean=true|Whether the company's record is active 135 | founded_at: dateTime|Date when the company was created 136 | classification: options=Corporation|Legal classification of the company 137 | - LLC 138 | - Corporation 139 | accounts: loadOptions|Accounts owned by the company 140 | address: 141 | street: string 142 | city: string 143 | state: string 144 | postal_code: string 145 | country: string 146 | phone_numbers: 147 | - number: string 148 | category: string 149 | ``` 150 | 151 | **Notes** 152 | 153 | - Optionally, specify a description with a pipe `|` after the type, e.g. `employees`. 154 | - Optionally, specify a default with an equals sign `=` and the default after the type e.g. `is_active`. If the default is unspecified, a `string` or `dateTime` param defaults to `''`, a `number` param defaults to `0`, a `boolean` param defaults to `false`, and an `options` param defaults to the zeroth item. 155 | -------------------------------------------------------------------------------- /docs/icons8-product-documents-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivov/nodebuilder/af0494499696403af23655a17a92d7fdfc08a8b0/docs/icons8-product-documents-64.png -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivov/nodebuilder/af0494499696403af23655a17a92d7fdfc08a8b0/docs/logo.png -------------------------------------------------------------------------------- /docs/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivov/nodebuilder/af0494499696403af23655a17a92d7fdfc08a8b0/docs/screenshot.png -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodebuilder", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@apidevtools/json-schema-ref-parser": { 8 | "version": "9.0.7", 9 | "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.7.tgz", 10 | "integrity": "sha512-QdwOGF1+eeyFh+17v2Tz626WX0nucd1iKOm6JUTUvCZdbolblCOOQCxGrQPY0f7jEhn36PiAWqZnsC2r5vmUWg==", 11 | "requires": { 12 | "@jsdevtools/ono": "^7.1.3", 13 | "call-me-maybe": "^1.0.1", 14 | "js-yaml": "^3.13.1" 15 | }, 16 | "dependencies": { 17 | "argparse": { 18 | "version": "1.0.10", 19 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", 20 | "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", 21 | "requires": { 22 | "sprintf-js": "~1.0.2" 23 | } 24 | }, 25 | "js-yaml": { 26 | "version": "3.14.1", 27 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", 28 | "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", 29 | "requires": { 30 | "argparse": "^1.0.7", 31 | "esprima": "^4.0.0" 32 | } 33 | } 34 | } 35 | }, 36 | "@apidevtools/openapi-schemas": { 37 | "version": "2.0.4", 38 | "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.0.4.tgz", 39 | "integrity": "sha512-ob5c4UiaMYkb24pNhvfSABShAwpREvUGCkqjiz/BX9gKZ32y/S22M+ALIHftTAuv9KsFVSpVdIDzi9ZzFh5TCA==" 40 | }, 41 | "@apidevtools/swagger-cli": { 42 | "version": "4.0.4", 43 | "resolved": "https://registry.npmjs.org/@apidevtools/swagger-cli/-/swagger-cli-4.0.4.tgz", 44 | "integrity": "sha512-hdDT3B6GLVovCsRZYDi3+wMcB1HfetTU20l2DC8zD3iFRNMC6QNAZG5fo/6PYeHWBEv7ri4MvnlKodhNB0nt7g==", 45 | "requires": { 46 | "@apidevtools/swagger-parser": "^10.0.1", 47 | "chalk": "^4.1.0", 48 | "js-yaml": "^3.14.0", 49 | "yargs": "^15.4.1" 50 | }, 51 | "dependencies": { 52 | "argparse": { 53 | "version": "1.0.10", 54 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", 55 | "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", 56 | "requires": { 57 | "sprintf-js": "~1.0.2" 58 | } 59 | }, 60 | "js-yaml": { 61 | "version": "3.14.1", 62 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", 63 | "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", 64 | "requires": { 65 | "argparse": "^1.0.7", 66 | "esprima": "^4.0.0" 67 | } 68 | } 69 | } 70 | }, 71 | "@apidevtools/swagger-methods": { 72 | "version": "3.0.2", 73 | "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", 74 | "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==" 75 | }, 76 | "@apidevtools/swagger-parser": { 77 | "version": "10.0.2", 78 | "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.2.tgz", 79 | "integrity": "sha512-JFxcEyp8RlNHgBCE98nwuTkZT6eNFPc1aosWV6wPcQph72TSEEu1k3baJD4/x1qznU+JiDdz8F5pTwabZh+Dhg==", 80 | "requires": { 81 | "@apidevtools/json-schema-ref-parser": "^9.0.6", 82 | "@apidevtools/openapi-schemas": "^2.0.4", 83 | "@apidevtools/swagger-methods": "^3.0.2", 84 | "@jsdevtools/ono": "^7.1.3", 85 | "call-me-maybe": "^1.0.1", 86 | "z-schema": "^4.2.3" 87 | } 88 | }, 89 | "@dsherret/to-absolute-glob": { 90 | "version": "2.0.2", 91 | "resolved": "https://registry.npmjs.org/@dsherret/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", 92 | "integrity": "sha1-H2R13IvZdM6gei2vOGSzF7HdMyw=", 93 | "requires": { 94 | "is-absolute": "^1.0.0", 95 | "is-negated-glob": "^1.0.0" 96 | } 97 | }, 98 | "@jsdevtools/ono": { 99 | "version": "7.1.3", 100 | "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", 101 | "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==" 102 | }, 103 | "@nodelib/fs.scandir": { 104 | "version": "2.1.4", 105 | "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz", 106 | "integrity": "sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA==", 107 | "requires": { 108 | "@nodelib/fs.stat": "2.0.4", 109 | "run-parallel": "^1.1.9" 110 | } 111 | }, 112 | "@nodelib/fs.stat": { 113 | "version": "2.0.4", 114 | "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz", 115 | "integrity": "sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q==" 116 | }, 117 | "@nodelib/fs.walk": { 118 | "version": "1.2.6", 119 | "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz", 120 | "integrity": "sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow==", 121 | "requires": { 122 | "@nodelib/fs.scandir": "2.1.4", 123 | "fastq": "^1.6.0" 124 | } 125 | }, 126 | "@ts-morph/common": { 127 | "version": "0.7.2", 128 | "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.7.2.tgz", 129 | "integrity": "sha512-XyUPLf1UHtteP5C5FEgVJqgIEOcmaSEoJyU/jQ1gTBKlz/lb1Uss4ix+D2e5qRwPFiBMqM/jwJpna0yVDE5V/g==", 130 | "requires": { 131 | "@dsherret/to-absolute-glob": "^2.0.2", 132 | "fast-glob": "^3.2.4", 133 | "is-negated-glob": "^1.0.0", 134 | "mkdirp": "^1.0.4", 135 | "multimatch": "^5.0.0", 136 | "typescript": "~4.1.2" 137 | } 138 | }, 139 | "@types/inquirer": { 140 | "version": "7.3.1", 141 | "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-7.3.1.tgz", 142 | "integrity": "sha512-osD38QVIfcdgsPCT0V3lD7eH0OFurX71Jft18bZrsVQWVRt6TuxRzlr0GJLrxoHZR2V5ph7/qP8se/dcnI7o0g==", 143 | "dev": true, 144 | "requires": { 145 | "@types/through": "*", 146 | "rxjs": "^6.4.0" 147 | } 148 | }, 149 | "@types/js-yaml": { 150 | "version": "4.0.0", 151 | "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.0.tgz", 152 | "integrity": "sha512-4vlpCM5KPCL5CfGmTbpjwVKbISRYhduEJvvUWsH5EB7QInhEj94XPZ3ts/9FPiLZFqYO0xoW4ZL8z2AabTGgJA==", 153 | "dev": true 154 | }, 155 | "@types/jsonpath": { 156 | "version": "0.2.0", 157 | "resolved": "https://registry.npmjs.org/@types/jsonpath/-/jsonpath-0.2.0.tgz", 158 | "integrity": "sha512-v7qlPA0VpKUlEdhghbDqRoKMxFB3h3Ch688TApBJ6v+XLDdvWCGLJIYiPKGZnS6MAOie+IorCfNYVHOPIHSWwQ==", 159 | "dev": true 160 | }, 161 | "@types/minimatch": { 162 | "version": "3.0.3", 163 | "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", 164 | "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==" 165 | }, 166 | "@types/node": { 167 | "version": "14.14.16", 168 | "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.16.tgz", 169 | "integrity": "sha512-naXYePhweTi+BMv11TgioE2/FXU4fSl29HAH1ffxVciNsH3rYXjNP2yM8wqmSm7jS20gM8TIklKiTen+1iVncw==" 170 | }, 171 | "@types/pluralize": { 172 | "version": "0.0.29", 173 | "resolved": "https://registry.npmjs.org/@types/pluralize/-/pluralize-0.0.29.tgz", 174 | "integrity": "sha512-BYOID+l2Aco2nBik+iYS4SZX0Lf20KPILP5RGmM1IgzdwNdTs0eebiFriOPcej1sX9mLnSoiNte5zcFxssgpGA==", 175 | "dev": true 176 | }, 177 | "@types/through": { 178 | "version": "0.0.30", 179 | "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.30.tgz", 180 | "integrity": "sha512-FvnCJljyxhPM3gkRgWmxmDZyAQSiBQQWLI0A0VFL0K7W1oRUrPJSqNO0NvTnLkBcotdlp3lKvaT0JrnyRDkzOg==", 181 | "dev": true, 182 | "requires": { 183 | "@types/node": "*" 184 | } 185 | }, 186 | "@types/underscore": { 187 | "version": "1.10.24", 188 | "resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.10.24.tgz", 189 | "integrity": "sha512-T3NQD8hXNW2sRsSbLNjF/aBo18MyJlbw0lSpQHB/eZZtScPdexN4HSa8cByYwTw9Wy7KuOFr81mlDQcQQaZ79w==", 190 | "dev": true 191 | }, 192 | "ansi-colors": { 193 | "version": "4.1.1", 194 | "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", 195 | "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==" 196 | }, 197 | "ansi-escapes": { 198 | "version": "4.3.2", 199 | "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", 200 | "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", 201 | "requires": { 202 | "type-fest": "^0.21.3" 203 | } 204 | }, 205 | "ansi-regex": { 206 | "version": "5.0.0", 207 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", 208 | "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" 209 | }, 210 | "ansi-styles": { 211 | "version": "4.3.0", 212 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 213 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 214 | "requires": { 215 | "color-convert": "^2.0.1" 216 | } 217 | }, 218 | "argparse": { 219 | "version": "2.0.1", 220 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", 221 | "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" 222 | }, 223 | "array-differ": { 224 | "version": "3.0.0", 225 | "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz", 226 | "integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==" 227 | }, 228 | "array-union": { 229 | "version": "2.1.0", 230 | "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", 231 | "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==" 232 | }, 233 | "arrify": { 234 | "version": "2.0.1", 235 | "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", 236 | "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==" 237 | }, 238 | "async": { 239 | "version": "0.9.2", 240 | "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", 241 | "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=" 242 | }, 243 | "at-least-node": { 244 | "version": "1.0.0", 245 | "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", 246 | "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==" 247 | }, 248 | "axios": { 249 | "version": "0.21.1", 250 | "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", 251 | "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", 252 | "requires": { 253 | "follow-redirects": "^1.10.0" 254 | } 255 | }, 256 | "balanced-match": { 257 | "version": "1.0.0", 258 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 259 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" 260 | }, 261 | "brace-expansion": { 262 | "version": "1.1.11", 263 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 264 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 265 | "requires": { 266 | "balanced-match": "^1.0.0", 267 | "concat-map": "0.0.1" 268 | } 269 | }, 270 | "braces": { 271 | "version": "3.0.2", 272 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", 273 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", 274 | "requires": { 275 | "fill-range": "^7.0.1" 276 | } 277 | }, 278 | "call-me-maybe": { 279 | "version": "1.0.1", 280 | "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", 281 | "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=" 282 | }, 283 | "camel-case": { 284 | "version": "4.1.2", 285 | "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", 286 | "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", 287 | "requires": { 288 | "pascal-case": "^3.1.2", 289 | "tslib": "^2.0.3" 290 | } 291 | }, 292 | "camelcase": { 293 | "version": "5.3.1", 294 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", 295 | "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" 296 | }, 297 | "capital-case": { 298 | "version": "1.0.4", 299 | "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz", 300 | "integrity": "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==", 301 | "requires": { 302 | "no-case": "^3.0.4", 303 | "tslib": "^2.0.3", 304 | "upper-case-first": "^2.0.2" 305 | } 306 | }, 307 | "chalk": { 308 | "version": "4.1.0", 309 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", 310 | "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", 311 | "requires": { 312 | "ansi-styles": "^4.1.0", 313 | "supports-color": "^7.1.0" 314 | } 315 | }, 316 | "change-case": { 317 | "version": "4.1.2", 318 | "resolved": "https://registry.npmjs.org/change-case/-/change-case-4.1.2.tgz", 319 | "integrity": "sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==", 320 | "requires": { 321 | "camel-case": "^4.1.2", 322 | "capital-case": "^1.0.4", 323 | "constant-case": "^3.0.4", 324 | "dot-case": "^3.0.4", 325 | "header-case": "^2.0.4", 326 | "no-case": "^3.0.4", 327 | "param-case": "^3.0.4", 328 | "pascal-case": "^3.1.2", 329 | "path-case": "^3.0.4", 330 | "sentence-case": "^3.0.4", 331 | "snake-case": "^3.0.4", 332 | "tslib": "^2.0.3" 333 | } 334 | }, 335 | "chardet": { 336 | "version": "0.7.0", 337 | "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", 338 | "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" 339 | }, 340 | "cli-cursor": { 341 | "version": "3.1.0", 342 | "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", 343 | "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", 344 | "requires": { 345 | "restore-cursor": "^3.1.0" 346 | } 347 | }, 348 | "cli-width": { 349 | "version": "3.0.0", 350 | "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", 351 | "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==" 352 | }, 353 | "cliui": { 354 | "version": "6.0.0", 355 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", 356 | "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", 357 | "requires": { 358 | "string-width": "^4.2.0", 359 | "strip-ansi": "^6.0.0", 360 | "wrap-ansi": "^6.2.0" 361 | } 362 | }, 363 | "code-block-writer": { 364 | "version": "10.1.1", 365 | "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-10.1.1.tgz", 366 | "integrity": "sha512-67ueh2IRGst/51p0n6FvPrnRjAGHY5F8xdjkgrYE7DDzpJe6qA07RYQ9VcoUeo5ATOjSOiWpSL3SWBRRbempMw==" 367 | }, 368 | "color-convert": { 369 | "version": "2.0.1", 370 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 371 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 372 | "requires": { 373 | "color-name": "~1.1.4" 374 | } 375 | }, 376 | "color-name": { 377 | "version": "1.1.4", 378 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 379 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 380 | }, 381 | "commander": { 382 | "version": "2.20.3", 383 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", 384 | "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", 385 | "optional": true 386 | }, 387 | "concat-map": { 388 | "version": "0.0.1", 389 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 390 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" 391 | }, 392 | "constant-case": { 393 | "version": "3.0.4", 394 | "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-3.0.4.tgz", 395 | "integrity": "sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==", 396 | "requires": { 397 | "no-case": "^3.0.4", 398 | "tslib": "^2.0.3", 399 | "upper-case": "^2.0.2" 400 | } 401 | }, 402 | "cross-spawn": { 403 | "version": "7.0.3", 404 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", 405 | "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", 406 | "requires": { 407 | "path-key": "^3.1.0", 408 | "shebang-command": "^2.0.0", 409 | "which": "^2.0.1" 410 | } 411 | }, 412 | "decamelize": { 413 | "version": "1.2.0", 414 | "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", 415 | "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" 416 | }, 417 | "dot-case": { 418 | "version": "3.0.4", 419 | "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", 420 | "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", 421 | "requires": { 422 | "no-case": "^3.0.4", 423 | "tslib": "^2.0.3" 424 | } 425 | }, 426 | "ejs": { 427 | "version": "3.1.5", 428 | "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.5.tgz", 429 | "integrity": "sha512-dldq3ZfFtgVTJMLjOe+/3sROTzALlL9E34V4/sDtUd/KlBSS0s6U1/+WPE1B4sj9CXHJpL1M6rhNJnc9Wbal9w==", 430 | "requires": { 431 | "jake": "^10.6.1" 432 | } 433 | }, 434 | "emoji-regex": { 435 | "version": "8.0.0", 436 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 437 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" 438 | }, 439 | "end-of-stream": { 440 | "version": "1.4.4", 441 | "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", 442 | "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", 443 | "requires": { 444 | "once": "^1.4.0" 445 | } 446 | }, 447 | "enquirer": { 448 | "version": "2.3.6", 449 | "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", 450 | "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", 451 | "requires": { 452 | "ansi-colors": "^4.1.1" 453 | } 454 | }, 455 | "escape-string-regexp": { 456 | "version": "1.0.5", 457 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 458 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" 459 | }, 460 | "esprima": { 461 | "version": "4.0.1", 462 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", 463 | "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" 464 | }, 465 | "execa": { 466 | "version": "4.1.0", 467 | "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", 468 | "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", 469 | "requires": { 470 | "cross-spawn": "^7.0.0", 471 | "get-stream": "^5.0.0", 472 | "human-signals": "^1.1.1", 473 | "is-stream": "^2.0.0", 474 | "merge-stream": "^2.0.0", 475 | "npm-run-path": "^4.0.0", 476 | "onetime": "^5.1.0", 477 | "signal-exit": "^3.0.2", 478 | "strip-final-newline": "^2.0.0" 479 | } 480 | }, 481 | "external-editor": { 482 | "version": "3.1.0", 483 | "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", 484 | "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", 485 | "requires": { 486 | "chardet": "^0.7.0", 487 | "iconv-lite": "^0.4.24", 488 | "tmp": "^0.0.33" 489 | } 490 | }, 491 | "fast-glob": { 492 | "version": "3.2.4", 493 | "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.4.tgz", 494 | "integrity": "sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ==", 495 | "requires": { 496 | "@nodelib/fs.stat": "^2.0.2", 497 | "@nodelib/fs.walk": "^1.2.3", 498 | "glob-parent": "^5.1.0", 499 | "merge2": "^1.3.0", 500 | "micromatch": "^4.0.2", 501 | "picomatch": "^2.2.1" 502 | } 503 | }, 504 | "fastq": { 505 | "version": "1.10.0", 506 | "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.10.0.tgz", 507 | "integrity": "sha512-NL2Qc5L3iQEsyYzweq7qfgy5OtXCmGzGvhElGEd/SoFWEMOEczNh5s5ocaF01HDetxz+p8ecjNPA6cZxxIHmzA==", 508 | "requires": { 509 | "reusify": "^1.0.4" 510 | } 511 | }, 512 | "figures": { 513 | "version": "3.2.0", 514 | "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", 515 | "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", 516 | "requires": { 517 | "escape-string-regexp": "^1.0.5" 518 | } 519 | }, 520 | "filelist": { 521 | "version": "1.0.1", 522 | "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.1.tgz", 523 | "integrity": "sha512-8zSK6Nu0DQIC08mUC46sWGXi+q3GGpKydAG36k+JDba6VRpkevvOWUW5a/PhShij4+vHT9M+ghgG7eM+a9JDUQ==", 524 | "requires": { 525 | "minimatch": "^3.0.4" 526 | } 527 | }, 528 | "fill-range": { 529 | "version": "7.0.1", 530 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", 531 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", 532 | "requires": { 533 | "to-regex-range": "^5.0.1" 534 | } 535 | }, 536 | "find-up": { 537 | "version": "4.1.0", 538 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", 539 | "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", 540 | "requires": { 541 | "locate-path": "^5.0.0", 542 | "path-exists": "^4.0.0" 543 | } 544 | }, 545 | "follow-redirects": { 546 | "version": "1.14.1", 547 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.1.tgz", 548 | "integrity": "sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==" 549 | }, 550 | "front-matter": { 551 | "version": "4.0.2", 552 | "resolved": "https://registry.npmjs.org/front-matter/-/front-matter-4.0.2.tgz", 553 | "integrity": "sha512-I8ZuJ/qG92NWX8i5x1Y8qyj3vizhXS31OxjKDu3LKP+7/qBgfIKValiZIEwoVoJKUHlhWtYrktkxV1XsX+pPlg==", 554 | "requires": { 555 | "js-yaml": "^3.13.1" 556 | }, 557 | "dependencies": { 558 | "argparse": { 559 | "version": "1.0.10", 560 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", 561 | "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", 562 | "requires": { 563 | "sprintf-js": "~1.0.2" 564 | } 565 | }, 566 | "js-yaml": { 567 | "version": "3.14.1", 568 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", 569 | "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", 570 | "requires": { 571 | "argparse": "^1.0.7", 572 | "esprima": "^4.0.0" 573 | } 574 | } 575 | } 576 | }, 577 | "fs-extra": { 578 | "version": "9.0.1", 579 | "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz", 580 | "integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==", 581 | "requires": { 582 | "at-least-node": "^1.0.0", 583 | "graceful-fs": "^4.2.0", 584 | "jsonfile": "^6.0.1", 585 | "universalify": "^1.0.0" 586 | } 587 | }, 588 | "get-caller-file": { 589 | "version": "2.0.5", 590 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 591 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" 592 | }, 593 | "get-stream": { 594 | "version": "5.2.0", 595 | "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", 596 | "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", 597 | "requires": { 598 | "pump": "^3.0.0" 599 | } 600 | }, 601 | "glob-parent": { 602 | "version": "5.1.1", 603 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", 604 | "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", 605 | "requires": { 606 | "is-glob": "^4.0.1" 607 | } 608 | }, 609 | "graceful-fs": { 610 | "version": "4.2.4", 611 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", 612 | "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" 613 | }, 614 | "has-flag": { 615 | "version": "4.0.0", 616 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 617 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" 618 | }, 619 | "header-case": { 620 | "version": "2.0.4", 621 | "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.4.tgz", 622 | "integrity": "sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==", 623 | "requires": { 624 | "capital-case": "^1.0.4", 625 | "tslib": "^2.0.3" 626 | } 627 | }, 628 | "human-signals": { 629 | "version": "1.1.1", 630 | "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", 631 | "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==" 632 | }, 633 | "hygen": { 634 | "version": "6.0.4", 635 | "resolved": "https://registry.npmjs.org/hygen/-/hygen-6.0.4.tgz", 636 | "integrity": "sha512-FzYMYnhSfJc3i/fLi6GqpGVNQSCvAQ2eKL8oA59iJ8c50/BjFujjMYW0nB+Vl7fGwb2OPbyROb1vIBWPBG+rFg==", 637 | "requires": { 638 | "@types/node": "^14.0.14", 639 | "chalk": "^4.1.0", 640 | "change-case": "^3.1.0", 641 | "ejs": "^3.1.3", 642 | "enquirer": "^2.3.6", 643 | "execa": "^4.0.2", 644 | "front-matter": "^4.0.2", 645 | "fs-extra": "^9.0.1", 646 | "ignore-walk": "^3.0.3", 647 | "inflection": "^1.12.0", 648 | "yargs-parser": "^18.1.3" 649 | }, 650 | "dependencies": { 651 | "camel-case": { 652 | "version": "3.0.0", 653 | "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", 654 | "integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=", 655 | "requires": { 656 | "no-case": "^2.2.0", 657 | "upper-case": "^1.1.1" 658 | } 659 | }, 660 | "change-case": { 661 | "version": "3.1.0", 662 | "resolved": "https://registry.npmjs.org/change-case/-/change-case-3.1.0.tgz", 663 | "integrity": "sha512-2AZp7uJZbYEzRPsFoa+ijKdvp9zsrnnt6+yFokfwEpeJm0xuJDVoxiRCAaTzyJND8GJkofo2IcKWaUZ/OECVzw==", 664 | "requires": { 665 | "camel-case": "^3.0.0", 666 | "constant-case": "^2.0.0", 667 | "dot-case": "^2.1.0", 668 | "header-case": "^1.0.0", 669 | "is-lower-case": "^1.1.0", 670 | "is-upper-case": "^1.1.0", 671 | "lower-case": "^1.1.1", 672 | "lower-case-first": "^1.0.0", 673 | "no-case": "^2.3.2", 674 | "param-case": "^2.1.0", 675 | "pascal-case": "^2.0.0", 676 | "path-case": "^2.1.0", 677 | "sentence-case": "^2.1.0", 678 | "snake-case": "^2.1.0", 679 | "swap-case": "^1.1.0", 680 | "title-case": "^2.1.0", 681 | "upper-case": "^1.1.1", 682 | "upper-case-first": "^1.1.0" 683 | }, 684 | "dependencies": { 685 | "title-case": { 686 | "version": "2.1.1", 687 | "resolved": "https://registry.npmjs.org/title-case/-/title-case-2.1.1.tgz", 688 | "integrity": "sha1-PhJyFtpY0rxb7PE3q5Ha46fNj6o=", 689 | "requires": { 690 | "no-case": "^2.2.0", 691 | "upper-case": "^1.0.3" 692 | } 693 | } 694 | } 695 | }, 696 | "constant-case": { 697 | "version": "2.0.0", 698 | "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-2.0.0.tgz", 699 | "integrity": "sha1-QXV2TTidP6nI7NKRhu1gBSQ7akY=", 700 | "requires": { 701 | "snake-case": "^2.1.0", 702 | "upper-case": "^1.1.1" 703 | } 704 | }, 705 | "dot-case": { 706 | "version": "2.1.1", 707 | "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-2.1.1.tgz", 708 | "integrity": "sha1-NNzzf1Co6TwrO8qLt/uRVcfaO+4=", 709 | "requires": { 710 | "no-case": "^2.2.0" 711 | } 712 | }, 713 | "header-case": { 714 | "version": "1.0.1", 715 | "resolved": "https://registry.npmjs.org/header-case/-/header-case-1.0.1.tgz", 716 | "integrity": "sha1-lTWXMZfBRLCWE81l0xfvGZY70C0=", 717 | "requires": { 718 | "no-case": "^2.2.0", 719 | "upper-case": "^1.1.3" 720 | } 721 | }, 722 | "lower-case": { 723 | "version": "1.1.4", 724 | "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", 725 | "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=" 726 | }, 727 | "no-case": { 728 | "version": "2.3.2", 729 | "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", 730 | "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", 731 | "requires": { 732 | "lower-case": "^1.1.1" 733 | } 734 | }, 735 | "param-case": { 736 | "version": "2.1.1", 737 | "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz", 738 | "integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=", 739 | "requires": { 740 | "no-case": "^2.2.0" 741 | } 742 | }, 743 | "pascal-case": { 744 | "version": "2.0.1", 745 | "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-2.0.1.tgz", 746 | "integrity": "sha1-LVeNNFX2YNpl7KGO+VtODekSdh4=", 747 | "requires": { 748 | "camel-case": "^3.0.0", 749 | "upper-case-first": "^1.1.0" 750 | } 751 | }, 752 | "path-case": { 753 | "version": "2.1.1", 754 | "resolved": "https://registry.npmjs.org/path-case/-/path-case-2.1.1.tgz", 755 | "integrity": "sha1-lLgDfDctP+KQbkZbtF4l0ibo7qU=", 756 | "requires": { 757 | "no-case": "^2.2.0" 758 | } 759 | }, 760 | "sentence-case": { 761 | "version": "2.1.1", 762 | "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-2.1.1.tgz", 763 | "integrity": "sha1-H24t2jnBaL+S0T+G1KkYkz9mftQ=", 764 | "requires": { 765 | "no-case": "^2.2.0", 766 | "upper-case-first": "^1.1.2" 767 | } 768 | }, 769 | "snake-case": { 770 | "version": "2.1.0", 771 | "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-2.1.0.tgz", 772 | "integrity": "sha1-Qb2xtz8w7GagTU4srRt2OH1NbZ8=", 773 | "requires": { 774 | "no-case": "^2.2.0" 775 | } 776 | }, 777 | "upper-case": { 778 | "version": "1.1.3", 779 | "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", 780 | "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=" 781 | }, 782 | "upper-case-first": { 783 | "version": "1.1.2", 784 | "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-1.1.2.tgz", 785 | "integrity": "sha1-XXm+3P8UQZUY/S7bCgUHybaFkRU=", 786 | "requires": { 787 | "upper-case": "^1.1.1" 788 | } 789 | } 790 | } 791 | }, 792 | "iconv-lite": { 793 | "version": "0.4.24", 794 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 795 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 796 | "requires": { 797 | "safer-buffer": ">= 2.1.2 < 3" 798 | } 799 | }, 800 | "ignore-walk": { 801 | "version": "3.0.3", 802 | "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", 803 | "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", 804 | "requires": { 805 | "minimatch": "^3.0.4" 806 | } 807 | }, 808 | "inflection": { 809 | "version": "1.12.0", 810 | "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.12.0.tgz", 811 | "integrity": "sha1-ogCTVlbW9fa8TcdQLhrstwMihBY=" 812 | }, 813 | "inquirer": { 814 | "version": "8.0.0", 815 | "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.0.0.tgz", 816 | "integrity": "sha512-ON8pEJPPCdyjxj+cxsYRe6XfCJepTxANdNnTebsTuQgXpRyZRRT9t4dJwjRubgmvn20CLSEnozRUayXyM9VTXA==", 817 | "requires": { 818 | "ansi-escapes": "^4.2.1", 819 | "chalk": "^4.1.0", 820 | "cli-cursor": "^3.1.0", 821 | "cli-width": "^3.0.0", 822 | "external-editor": "^3.0.3", 823 | "figures": "^3.0.0", 824 | "lodash": "^4.17.21", 825 | "mute-stream": "0.0.8", 826 | "run-async": "^2.4.0", 827 | "rxjs": "^6.6.6", 828 | "string-width": "^4.1.0", 829 | "strip-ansi": "^6.0.0", 830 | "through": "^2.3.6" 831 | } 832 | }, 833 | "is-absolute": { 834 | "version": "1.0.0", 835 | "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", 836 | "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", 837 | "requires": { 838 | "is-relative": "^1.0.0", 839 | "is-windows": "^1.0.1" 840 | } 841 | }, 842 | "is-extglob": { 843 | "version": "2.1.1", 844 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 845 | "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" 846 | }, 847 | "is-fullwidth-code-point": { 848 | "version": "3.0.0", 849 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 850 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" 851 | }, 852 | "is-glob": { 853 | "version": "4.0.1", 854 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", 855 | "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", 856 | "requires": { 857 | "is-extglob": "^2.1.1" 858 | } 859 | }, 860 | "is-lower-case": { 861 | "version": "1.1.3", 862 | "resolved": "https://registry.npmjs.org/is-lower-case/-/is-lower-case-1.1.3.tgz", 863 | "integrity": "sha1-fhR75HaNxGbbO/shzGCzHmrWk5M=", 864 | "requires": { 865 | "lower-case": "^1.1.0" 866 | }, 867 | "dependencies": { 868 | "lower-case": { 869 | "version": "1.1.4", 870 | "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", 871 | "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=" 872 | } 873 | } 874 | }, 875 | "is-negated-glob": { 876 | "version": "1.0.0", 877 | "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", 878 | "integrity": "sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI=" 879 | }, 880 | "is-number": { 881 | "version": "7.0.0", 882 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 883 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" 884 | }, 885 | "is-relative": { 886 | "version": "1.0.0", 887 | "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", 888 | "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", 889 | "requires": { 890 | "is-unc-path": "^1.0.0" 891 | } 892 | }, 893 | "is-stream": { 894 | "version": "2.0.0", 895 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", 896 | "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==" 897 | }, 898 | "is-unc-path": { 899 | "version": "1.0.0", 900 | "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", 901 | "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", 902 | "requires": { 903 | "unc-path-regex": "^0.1.2" 904 | } 905 | }, 906 | "is-upper-case": { 907 | "version": "1.1.2", 908 | "resolved": "https://registry.npmjs.org/is-upper-case/-/is-upper-case-1.1.2.tgz", 909 | "integrity": "sha1-jQsfp+eTOh5YSDYA7H2WYcuvdW8=", 910 | "requires": { 911 | "upper-case": "^1.1.0" 912 | }, 913 | "dependencies": { 914 | "upper-case": { 915 | "version": "1.1.3", 916 | "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", 917 | "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=" 918 | } 919 | } 920 | }, 921 | "is-windows": { 922 | "version": "1.0.2", 923 | "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", 924 | "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" 925 | }, 926 | "isexe": { 927 | "version": "2.0.0", 928 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 929 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" 930 | }, 931 | "jake": { 932 | "version": "10.8.2", 933 | "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.2.tgz", 934 | "integrity": "sha512-eLpKyrfG3mzvGE2Du8VoPbeSkRry093+tyNjdYaBbJS9v17knImYGNXQCUV0gLxQtF82m3E8iRb/wdSQZLoq7A==", 935 | "requires": { 936 | "async": "0.9.x", 937 | "chalk": "^2.4.2", 938 | "filelist": "^1.0.1", 939 | "minimatch": "^3.0.4" 940 | }, 941 | "dependencies": { 942 | "ansi-styles": { 943 | "version": "3.2.1", 944 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 945 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 946 | "requires": { 947 | "color-convert": "^1.9.0" 948 | } 949 | }, 950 | "chalk": { 951 | "version": "2.4.2", 952 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 953 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 954 | "requires": { 955 | "ansi-styles": "^3.2.1", 956 | "escape-string-regexp": "^1.0.5", 957 | "supports-color": "^5.3.0" 958 | } 959 | }, 960 | "color-convert": { 961 | "version": "1.9.3", 962 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 963 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 964 | "requires": { 965 | "color-name": "1.1.3" 966 | } 967 | }, 968 | "color-name": { 969 | "version": "1.1.3", 970 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 971 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" 972 | }, 973 | "has-flag": { 974 | "version": "3.0.0", 975 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 976 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" 977 | }, 978 | "supports-color": { 979 | "version": "5.5.0", 980 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 981 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 982 | "requires": { 983 | "has-flag": "^3.0.0" 984 | } 985 | } 986 | } 987 | }, 988 | "js-yaml": { 989 | "version": "4.0.0", 990 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.0.0.tgz", 991 | "integrity": "sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q==", 992 | "requires": { 993 | "argparse": "^2.0.1" 994 | } 995 | }, 996 | "jsonfile": { 997 | "version": "6.1.0", 998 | "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", 999 | "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", 1000 | "requires": { 1001 | "graceful-fs": "^4.1.6", 1002 | "universalify": "^2.0.0" 1003 | }, 1004 | "dependencies": { 1005 | "universalify": { 1006 | "version": "2.0.0", 1007 | "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", 1008 | "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" 1009 | } 1010 | } 1011 | }, 1012 | "jsonpath-plus": { 1013 | "version": "4.0.0", 1014 | "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-4.0.0.tgz", 1015 | "integrity": "sha512-e0Jtg4KAzDJKKwzbLaUtinCn0RZseWBVRTRGihSpvFlM3wTR7ExSp+PTdeTsDrLNJUe7L7JYJe8mblHX5SCT6A==" 1016 | }, 1017 | "locate-path": { 1018 | "version": "5.0.0", 1019 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", 1020 | "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", 1021 | "requires": { 1022 | "p-locate": "^4.1.0" 1023 | } 1024 | }, 1025 | "lodash": { 1026 | "version": "4.17.21", 1027 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", 1028 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" 1029 | }, 1030 | "lodash.get": { 1031 | "version": "4.4.2", 1032 | "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", 1033 | "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" 1034 | }, 1035 | "lodash.isequal": { 1036 | "version": "4.5.0", 1037 | "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", 1038 | "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" 1039 | }, 1040 | "lower-case": { 1041 | "version": "2.0.2", 1042 | "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", 1043 | "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", 1044 | "requires": { 1045 | "tslib": "^2.0.3" 1046 | } 1047 | }, 1048 | "lower-case-first": { 1049 | "version": "1.0.2", 1050 | "resolved": "https://registry.npmjs.org/lower-case-first/-/lower-case-first-1.0.2.tgz", 1051 | "integrity": "sha1-5dp8JvKacHO+AtUrrJmA5ZIq36E=", 1052 | "requires": { 1053 | "lower-case": "^1.1.2" 1054 | }, 1055 | "dependencies": { 1056 | "lower-case": { 1057 | "version": "1.1.4", 1058 | "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", 1059 | "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=" 1060 | } 1061 | } 1062 | }, 1063 | "merge-stream": { 1064 | "version": "2.0.0", 1065 | "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", 1066 | "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" 1067 | }, 1068 | "merge2": { 1069 | "version": "1.4.1", 1070 | "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", 1071 | "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" 1072 | }, 1073 | "micromatch": { 1074 | "version": "4.0.2", 1075 | "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", 1076 | "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", 1077 | "requires": { 1078 | "braces": "^3.0.1", 1079 | "picomatch": "^2.0.5" 1080 | } 1081 | }, 1082 | "mimic-fn": { 1083 | "version": "2.1.0", 1084 | "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", 1085 | "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" 1086 | }, 1087 | "minimatch": { 1088 | "version": "3.0.4", 1089 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 1090 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 1091 | "requires": { 1092 | "brace-expansion": "^1.1.7" 1093 | } 1094 | }, 1095 | "mkdirp": { 1096 | "version": "1.0.4", 1097 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", 1098 | "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" 1099 | }, 1100 | "multimatch": { 1101 | "version": "5.0.0", 1102 | "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-5.0.0.tgz", 1103 | "integrity": "sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==", 1104 | "requires": { 1105 | "@types/minimatch": "^3.0.3", 1106 | "array-differ": "^3.0.0", 1107 | "array-union": "^2.1.0", 1108 | "arrify": "^2.0.1", 1109 | "minimatch": "^3.0.4" 1110 | } 1111 | }, 1112 | "mute-stream": { 1113 | "version": "0.0.8", 1114 | "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", 1115 | "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" 1116 | }, 1117 | "no-case": { 1118 | "version": "3.0.4", 1119 | "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", 1120 | "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", 1121 | "requires": { 1122 | "lower-case": "^2.0.2", 1123 | "tslib": "^2.0.3" 1124 | } 1125 | }, 1126 | "npm-run-path": { 1127 | "version": "4.0.1", 1128 | "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", 1129 | "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", 1130 | "requires": { 1131 | "path-key": "^3.0.0" 1132 | } 1133 | }, 1134 | "object-treeify": { 1135 | "version": "1.1.31", 1136 | "resolved": "https://registry.npmjs.org/object-treeify/-/object-treeify-1.1.31.tgz", 1137 | "integrity": "sha512-kt2UuyHDTH+J6w0pv2c+3uuEApGuwgfjWogbqPWAvk4nOM/T3No0SzDtp6CuJ/XBUy//nFNuerb8ms7CqjD9Tw==" 1138 | }, 1139 | "once": { 1140 | "version": "1.4.0", 1141 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 1142 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 1143 | "requires": { 1144 | "wrappy": "1" 1145 | } 1146 | }, 1147 | "onetime": { 1148 | "version": "5.1.2", 1149 | "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", 1150 | "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", 1151 | "requires": { 1152 | "mimic-fn": "^2.1.0" 1153 | } 1154 | }, 1155 | "os-tmpdir": { 1156 | "version": "1.0.2", 1157 | "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", 1158 | "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" 1159 | }, 1160 | "p-limit": { 1161 | "version": "2.3.0", 1162 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", 1163 | "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", 1164 | "requires": { 1165 | "p-try": "^2.0.0" 1166 | } 1167 | }, 1168 | "p-locate": { 1169 | "version": "4.1.0", 1170 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", 1171 | "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", 1172 | "requires": { 1173 | "p-limit": "^2.2.0" 1174 | } 1175 | }, 1176 | "p-try": { 1177 | "version": "2.2.0", 1178 | "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", 1179 | "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" 1180 | }, 1181 | "param-case": { 1182 | "version": "3.0.4", 1183 | "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", 1184 | "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", 1185 | "requires": { 1186 | "dot-case": "^3.0.4", 1187 | "tslib": "^2.0.3" 1188 | } 1189 | }, 1190 | "pascal-case": { 1191 | "version": "3.1.2", 1192 | "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", 1193 | "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", 1194 | "requires": { 1195 | "no-case": "^3.0.4", 1196 | "tslib": "^2.0.3" 1197 | } 1198 | }, 1199 | "path-case": { 1200 | "version": "3.0.4", 1201 | "resolved": "https://registry.npmjs.org/path-case/-/path-case-3.0.4.tgz", 1202 | "integrity": "sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==", 1203 | "requires": { 1204 | "dot-case": "^3.0.4", 1205 | "tslib": "^2.0.3" 1206 | } 1207 | }, 1208 | "path-exists": { 1209 | "version": "4.0.0", 1210 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", 1211 | "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" 1212 | }, 1213 | "path-key": { 1214 | "version": "3.1.1", 1215 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 1216 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" 1217 | }, 1218 | "picomatch": { 1219 | "version": "2.2.2", 1220 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", 1221 | "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==" 1222 | }, 1223 | "pluralize": { 1224 | "version": "8.0.0", 1225 | "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", 1226 | "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==" 1227 | }, 1228 | "prettier": { 1229 | "version": "2.2.1", 1230 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.2.1.tgz", 1231 | "integrity": "sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==", 1232 | "dev": true 1233 | }, 1234 | "pump": { 1235 | "version": "3.0.0", 1236 | "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", 1237 | "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", 1238 | "requires": { 1239 | "end-of-stream": "^1.1.0", 1240 | "once": "^1.3.1" 1241 | } 1242 | }, 1243 | "require-directory": { 1244 | "version": "2.1.1", 1245 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 1246 | "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" 1247 | }, 1248 | "require-main-filename": { 1249 | "version": "2.0.0", 1250 | "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", 1251 | "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" 1252 | }, 1253 | "restore-cursor": { 1254 | "version": "3.1.0", 1255 | "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", 1256 | "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", 1257 | "requires": { 1258 | "onetime": "^5.1.0", 1259 | "signal-exit": "^3.0.2" 1260 | } 1261 | }, 1262 | "reusify": { 1263 | "version": "1.0.4", 1264 | "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", 1265 | "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" 1266 | }, 1267 | "run-async": { 1268 | "version": "2.4.1", 1269 | "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", 1270 | "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==" 1271 | }, 1272 | "run-parallel": { 1273 | "version": "1.1.10", 1274 | "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.10.tgz", 1275 | "integrity": "sha512-zb/1OuZ6flOlH6tQyMPUrE3x3Ulxjlo9WIVXR4yVYi4H9UXQaeIsPbLn2R3O3vQCnDKkAl2qHiuocKKX4Tz/Sw==" 1276 | }, 1277 | "rxjs": { 1278 | "version": "6.6.7", 1279 | "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", 1280 | "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", 1281 | "requires": { 1282 | "tslib": "^1.9.0" 1283 | }, 1284 | "dependencies": { 1285 | "tslib": { 1286 | "version": "1.14.1", 1287 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", 1288 | "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" 1289 | } 1290 | } 1291 | }, 1292 | "safer-buffer": { 1293 | "version": "2.1.2", 1294 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 1295 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 1296 | }, 1297 | "sentence-case": { 1298 | "version": "3.0.4", 1299 | "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.4.tgz", 1300 | "integrity": "sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==", 1301 | "requires": { 1302 | "no-case": "^3.0.4", 1303 | "tslib": "^2.0.3", 1304 | "upper-case-first": "^2.0.2" 1305 | } 1306 | }, 1307 | "set-blocking": { 1308 | "version": "2.0.0", 1309 | "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", 1310 | "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" 1311 | }, 1312 | "shebang-command": { 1313 | "version": "2.0.0", 1314 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 1315 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 1316 | "requires": { 1317 | "shebang-regex": "^3.0.0" 1318 | } 1319 | }, 1320 | "shebang-regex": { 1321 | "version": "3.0.0", 1322 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 1323 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" 1324 | }, 1325 | "signal-exit": { 1326 | "version": "3.0.3", 1327 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", 1328 | "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" 1329 | }, 1330 | "snake-case": { 1331 | "version": "3.0.4", 1332 | "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", 1333 | "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", 1334 | "requires": { 1335 | "dot-case": "^3.0.4", 1336 | "tslib": "^2.0.3" 1337 | } 1338 | }, 1339 | "sprintf-js": { 1340 | "version": "1.0.3", 1341 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", 1342 | "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" 1343 | }, 1344 | "string-width": { 1345 | "version": "4.2.0", 1346 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", 1347 | "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", 1348 | "requires": { 1349 | "emoji-regex": "^8.0.0", 1350 | "is-fullwidth-code-point": "^3.0.0", 1351 | "strip-ansi": "^6.0.0" 1352 | } 1353 | }, 1354 | "strip-ansi": { 1355 | "version": "6.0.0", 1356 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", 1357 | "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", 1358 | "requires": { 1359 | "ansi-regex": "^5.0.0" 1360 | } 1361 | }, 1362 | "strip-final-newline": { 1363 | "version": "2.0.0", 1364 | "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", 1365 | "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==" 1366 | }, 1367 | "supports-color": { 1368 | "version": "7.2.0", 1369 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 1370 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 1371 | "requires": { 1372 | "has-flag": "^4.0.0" 1373 | } 1374 | }, 1375 | "swap-case": { 1376 | "version": "1.1.2", 1377 | "resolved": "https://registry.npmjs.org/swap-case/-/swap-case-1.1.2.tgz", 1378 | "integrity": "sha1-w5IDpFhzhfrTyFCgvRvK+ggZdOM=", 1379 | "requires": { 1380 | "lower-case": "^1.1.1", 1381 | "upper-case": "^1.1.1" 1382 | }, 1383 | "dependencies": { 1384 | "lower-case": { 1385 | "version": "1.1.4", 1386 | "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", 1387 | "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=" 1388 | }, 1389 | "upper-case": { 1390 | "version": "1.1.3", 1391 | "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", 1392 | "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=" 1393 | } 1394 | } 1395 | }, 1396 | "through": { 1397 | "version": "2.3.8", 1398 | "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", 1399 | "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" 1400 | }, 1401 | "title-case": { 1402 | "version": "3.0.3", 1403 | "resolved": "https://registry.npmjs.org/title-case/-/title-case-3.0.3.tgz", 1404 | "integrity": "sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA==", 1405 | "requires": { 1406 | "tslib": "^2.0.3" 1407 | } 1408 | }, 1409 | "tmp": { 1410 | "version": "0.0.33", 1411 | "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", 1412 | "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", 1413 | "requires": { 1414 | "os-tmpdir": "~1.0.2" 1415 | } 1416 | }, 1417 | "to-regex-range": { 1418 | "version": "5.0.1", 1419 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 1420 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 1421 | "requires": { 1422 | "is-number": "^7.0.0" 1423 | } 1424 | }, 1425 | "ts-morph": { 1426 | "version": "9.1.0", 1427 | "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-9.1.0.tgz", 1428 | "integrity": "sha512-sei4u651MBenr27sD6qLDXN3gZ4thiX71E3qV7SuVtDas0uvK2LtgZkIYUf9DKm/fLJ6AB/+yhRJ1vpEBJgy7Q==", 1429 | "requires": { 1430 | "@dsherret/to-absolute-glob": "^2.0.2", 1431 | "@ts-morph/common": "~0.7.0", 1432 | "code-block-writer": "^10.1.1" 1433 | } 1434 | }, 1435 | "tslib": { 1436 | "version": "2.0.3", 1437 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", 1438 | "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" 1439 | }, 1440 | "type-fest": { 1441 | "version": "0.21.3", 1442 | "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", 1443 | "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==" 1444 | }, 1445 | "typescript": { 1446 | "version": "4.1.3", 1447 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz", 1448 | "integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==" 1449 | }, 1450 | "unc-path-regex": { 1451 | "version": "0.1.2", 1452 | "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", 1453 | "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=" 1454 | }, 1455 | "underscore": { 1456 | "version": "1.12.0", 1457 | "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.0.tgz", 1458 | "integrity": "sha512-21rQzss/XPMjolTiIezSu3JAjgagXKROtNrYFEOWK109qY1Uv2tVjPTZ1ci2HgvQDA16gHYSthQIJfB+XId/rQ==" 1459 | }, 1460 | "universalify": { 1461 | "version": "1.0.0", 1462 | "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz", 1463 | "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==" 1464 | }, 1465 | "upper-case": { 1466 | "version": "2.0.2", 1467 | "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-2.0.2.tgz", 1468 | "integrity": "sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==", 1469 | "requires": { 1470 | "tslib": "^2.0.3" 1471 | } 1472 | }, 1473 | "upper-case-first": { 1474 | "version": "2.0.2", 1475 | "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.2.tgz", 1476 | "integrity": "sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==", 1477 | "requires": { 1478 | "tslib": "^2.0.3" 1479 | } 1480 | }, 1481 | "validator": { 1482 | "version": "12.2.0", 1483 | "resolved": "https://registry.npmjs.org/validator/-/validator-12.2.0.tgz", 1484 | "integrity": "sha512-jJfE/DW6tIK1Ek8nCfNFqt8Wb3nzMoAbocBF6/Icgg1ZFSBpObdnwVY2jQj6qUqzhx5jc71fpvBWyLGO7Xl+nQ==" 1485 | }, 1486 | "which": { 1487 | "version": "2.0.2", 1488 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 1489 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 1490 | "requires": { 1491 | "isexe": "^2.0.0" 1492 | } 1493 | }, 1494 | "which-module": { 1495 | "version": "2.0.0", 1496 | "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", 1497 | "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" 1498 | }, 1499 | "wrap-ansi": { 1500 | "version": "6.2.0", 1501 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", 1502 | "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", 1503 | "requires": { 1504 | "ansi-styles": "^4.0.0", 1505 | "string-width": "^4.1.0", 1506 | "strip-ansi": "^6.0.0" 1507 | } 1508 | }, 1509 | "wrappy": { 1510 | "version": "1.0.2", 1511 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1512 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 1513 | }, 1514 | "y18n": { 1515 | "version": "4.0.1", 1516 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", 1517 | "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==" 1518 | }, 1519 | "yargs": { 1520 | "version": "15.4.1", 1521 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", 1522 | "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", 1523 | "requires": { 1524 | "cliui": "^6.0.0", 1525 | "decamelize": "^1.2.0", 1526 | "find-up": "^4.1.0", 1527 | "get-caller-file": "^2.0.1", 1528 | "require-directory": "^2.1.1", 1529 | "require-main-filename": "^2.0.0", 1530 | "set-blocking": "^2.0.0", 1531 | "string-width": "^4.2.0", 1532 | "which-module": "^2.0.0", 1533 | "y18n": "^4.0.0", 1534 | "yargs-parser": "^18.1.2" 1535 | } 1536 | }, 1537 | "yargs-parser": { 1538 | "version": "18.1.3", 1539 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", 1540 | "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", 1541 | "requires": { 1542 | "camelcase": "^5.0.0", 1543 | "decamelize": "^1.2.0" 1544 | } 1545 | }, 1546 | "z-schema": { 1547 | "version": "4.2.3", 1548 | "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-4.2.3.tgz", 1549 | "integrity": "sha512-zkvK/9TC6p38IwcrbnT3ul9in1UX4cm1y/VZSs4GHKIiDCrlafc+YQBgQBUdDXLAoZHf2qvQ7gJJOo6yT1LH6A==", 1550 | "requires": { 1551 | "commander": "^2.7.1", 1552 | "lodash.get": "^4.4.2", 1553 | "lodash.isequal": "^4.5.0", 1554 | "validator": "^12.0.0" 1555 | } 1556 | } 1557 | } 1558 | } 1559 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodebuilder", 3 | "version": "1.0.0", 4 | "description": "Build n8n nodes from OpenAPI specs and YAML files", 5 | "main": "index.js", 6 | "scripts": { 7 | "generate": "tsc && node dist/scripts/generate.js", 8 | "dev": "tsc && node dist/scripts/dev.js", 9 | "render": "node dist/scripts/render.js", 10 | "map": "node dist/scripts/map.js", 11 | "clear": "node dist/scripts/clear.js", 12 | "place": "node dist/scripts/place.js" 13 | }, 14 | "keywords": [ 15 | "n8n" 16 | ], 17 | "author": "Iván Ovejero", 18 | "license": "MIT", 19 | "devDependencies": { 20 | "@types/inquirer": "^7.3.1", 21 | "@types/js-yaml": "^4.0.0", 22 | "@types/jsonpath": "^0.2.0", 23 | "@types/node": "^14.14.16", 24 | "@types/pluralize": "0.0.29", 25 | "@types/underscore": "^1.10.24", 26 | "prettier": "^2.2.1", 27 | "typescript": "^4.1.3" 28 | }, 29 | "dependencies": { 30 | "@apidevtools/swagger-cli": "^4.0.4", 31 | "axios": "^0.21.1", 32 | "change-case": "^4.1.2", 33 | "hygen": "^6.0.4", 34 | "inquirer": "^8.0.0", 35 | "js-yaml": "^4.0.0", 36 | "jsonpath-plus": "^4.0.0", 37 | "object-treeify": "^1.1.31", 38 | "pluralize": "^8.0.0", 39 | "title-case": "^3.0.3", 40 | "ts-morph": "^9.1.0", 41 | "underscore": "^1.12.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/config/index.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | 3 | export const inputDir = path.join("src", "input"); 4 | export const customInputDir = path.join(inputDir, "custom"); 5 | export const openApiInputDir = path.join(inputDir, "openApi"); 6 | 7 | export const outputDir = path.join("src", "output"); 8 | export const descriptionsOutputDir = path.join(outputDir, "descriptions"); 9 | 10 | export const hygen = path.join("node_modules", "hygen", "dist", "bin.js"); 11 | export const swagger = path.join( 12 | "node_modules", 13 | "@apidevtools", 14 | "swagger-cli", 15 | "bin", 16 | "swagger-cli.js" 17 | ); 18 | -------------------------------------------------------------------------------- /src/input/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivov/nodebuilder/af0494499696403af23655a17a92d7fdfc08a8b0/src/input/.gitkeep -------------------------------------------------------------------------------- /src/input/custom/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivov/nodebuilder/af0494499696403af23655a17a92d7fdfc08a8b0/src/input/custom/.gitkeep -------------------------------------------------------------------------------- /src/input/custom/copper.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | metaParams: 3 | apiUrl: https://api.prosperworks.com/developer_api/v1/ 4 | authType: OAuth2 5 | serviceName: Copper 6 | nodeColor: \#ff2564 7 | mainParams: 8 | 9 | company: 10 | - endpoint: /companies 11 | operationId: create 12 | operationUrl: https://developer.copper.com/companies/create-a-new-company.html 13 | requestMethod: POST 14 | requiredFields: 15 | requestBody: 16 | name: string|Name of the company to create. 17 | additionalFields: 18 | requestBody: 19 | email_domain: string 20 | details: string|Description of the company to create. 21 | address: 22 | street: string 23 | city: string 24 | state: string 25 | postal_code: string 26 | country: string 27 | phone_numbers: 28 | - number: string 29 | category: string 30 | - endpoint: /companies/{companyId} 31 | operationId: delete 32 | operationUrl: https://developer.copper.com/companies/delete-a-company.html 33 | requestMethod: DELETE 34 | - endpoint: /companies/{companyId} 35 | operationId: get 36 | operationUrl: https://developer.copper.com/companies/fetch-a-company-by-id.html 37 | requestMethod: GET 38 | - endpoint: /companies/search 39 | operationId: getAll 40 | requestMethod: POST 41 | operationUrl: https://developer.copper.com/companies/list-companies-search.html 42 | filters: 43 | requestBody: 44 | name: string|Name of the company to filter by. 45 | country: string|Country of the company to filter by. 46 | - endpoint: /companies/{companyId} 47 | operationId: update 48 | operationUrl: https://developer.copper.com/companies/update-a-company.html 49 | requestMethod: PUT 50 | updateFields: 51 | requestBody: 52 | name: string|Name to set for the company. 53 | address: 54 | street: string 55 | city: string 56 | state: string 57 | postal_code: string 58 | country: string 59 | details: string|Description to set for the company. 60 | phone_numbers: 61 | - number: string 62 | category: string 63 | 64 | lead: 65 | - endpoint: /leads 66 | operationId: create 67 | operationUrl: https://developer.copper.com/leads/create-a-new-lead.html 68 | requestMethod: POST 69 | requiredFields: 70 | requestBody: 71 | name: string|Name of the lead to create. 72 | additionalFields: 73 | requestBody: 74 | address: 75 | street: string 76 | city: string 77 | state: string 78 | postal_code: string 79 | country: string 80 | email: 81 | email: string 82 | category: string 83 | phone_numbers: 84 | - number: string 85 | category: string 86 | - endpoint: /leads/{leadId} 87 | operationId: delete 88 | operationUrl: https://developer.copper.com/leads/delete-a-lead.html 89 | requestMethod: DELETE 90 | - endpoint: /leads/{leadId} 91 | operationId: get 92 | operationUrl: https://developer.copper.com/leads/fetch-a-lead-by-id.html 93 | requestMethod: GET 94 | - endpoint: /leads/search 95 | operationId: getAll 96 | requestMethod: POST 97 | filters: 98 | requestBody: 99 | name: string|Name of the lead to filter by. 100 | country: string|Name of the country to filter by. 101 | - endpoint: /leads/{leadId} 102 | operationId: update 103 | operationUrl: https://developer.copper.com/leads/update-a-lead.html 104 | requestMethod: PUT 105 | updateFields: 106 | requestBody: 107 | name: string|Name to set for the lead. 108 | address: 109 | street: string 110 | city: string 111 | state: string 112 | postal_code: string 113 | country: string 114 | email: 115 | email: string 116 | category: string 117 | phone_numbers: 118 | - number: string 119 | category: string 120 | details: string|Description to set for the lead. 121 | 122 | opportunity: 123 | - endpoint: /opportunities 124 | operationId: create 125 | operationUrl: https://developer.copper.com/opportunities/create-a-new-opportunity.html 126 | requestMethod: POST 127 | requiredFields: 128 | requestBody: 129 | name: string|Name of the opportunity to create. 130 | additionalFields: 131 | requestBody: 132 | primary_contact_id: string|ID of the person who is the primary contact for this opportunity. 133 | customer_source_id: string|ID of the primary company associated with this opportunity. 134 | - endpoint: /opportunities/{opportunityId} 135 | operationId: delete 136 | operationUrl: https://developer.copper.com/opportunities/delete-an-opportunity.html 137 | requestMethod: DELETE 138 | - endpoint: /opportunities/{opportunityId} 139 | operationId: get 140 | operationUrl: https://developer.copper.com/opportunities/fetch-an-opportunity-by-id.html 141 | requestMethod: GET 142 | - endpoint: /opportunities/search 143 | operationId: getAll 144 | operationUrl: https://developer.copper.com/opportunities/list-opportunities-search.html 145 | requestMethod: POST 146 | filters: 147 | requestBody: 148 | customer_source_ids: string|Comma-separated IDs of the customer sources to filter by. 149 | company_ids: string|Comma-separated IDs of the primary companies to filter by. 150 | - endpoint: /opportunities/{opportunityId} 151 | operationId: update 152 | operationUrl: https://developer.copper.com/opportunities/update-an-opportunity.html 153 | requestMethod: PUT 154 | updateFields: 155 | requestBody: 156 | name: string|Name to set for the opportunity. 157 | primary_contact_id: string|ID of the primary company associated with this opportunity. 158 | customer_source_id: string|ID of the customer source that generated this opportunity. 159 | 160 | person: 161 | - endpoint: /people 162 | operationId: create 163 | operationUrl: https://developer.copper.com/people/create-a-new-person.html 164 | requestMethod: POST 165 | requiredFields: 166 | requestBody: 167 | name: string|Name of the person to create. 168 | additionalFields: 169 | requestBody: 170 | emails: string|Comma-separated list of emails to set for the person. 171 | phone_numbers: 172 | - number: string 173 | category: string 174 | address: 175 | street: string 176 | city: string 177 | state: string 178 | postal_code: string 179 | country: string 180 | email_domain: string 181 | details: string|Description of the person to create. 182 | - endpoint: /people/{personId} 183 | operationId: delete 184 | operationUrl: https://developer.copper.com/people/delete-a-person.html 185 | requestMethod: DELETE 186 | - endpoint: /people/{personId} 187 | operationId: get 188 | operationUrl: https://developer.copper.com/people/fetch-a-person-by-id.html 189 | requestMethod: GET 190 | - endpoint: /people/search 191 | operationId: getAll 192 | requestMethod: POST 193 | filters: 194 | requestBody: 195 | name: string|Name of the person to filter by. 196 | - endpoint: /people/{personId} 197 | operationId: update 198 | operationUrl: https://developer.copper.com/people/update-a-person.html 199 | requestMethod: PUT 200 | updateFields: 201 | requestBody: 202 | name: string|Name to set for the person. 203 | emails: string|Comma-separated list of emails to set for the person. 204 | phone_numbers: 205 | - number: string 206 | category: string 207 | address: 208 | street: string 209 | city: string 210 | state: string 211 | postal_code: string 212 | country: string 213 | email_domain: string 214 | details: string|Description to set for the person. 215 | 216 | project: 217 | - endpoint: /projects 218 | operationId: create 219 | requestMethod: POST 220 | requiredFields: 221 | requestBody: 222 | name: string|Name of the project to create. 223 | additionalFields: 224 | requestBody: 225 | status: 226 | - Open: string|Project with open status. 227 | - Completed: string|Project with closed status. 228 | details: string|Description of the project to create. 229 | assignee_id: string|ID of the user who will own the project to create. 230 | - endpoint: /projects/{projectId} 231 | operationId: delete 232 | operationUrl: https://developer.copper.com/projects/delete-a-project.html 233 | requestMethod: DELETE 234 | - endpoint: /projects/{projectId} 235 | operationId: get 236 | operationUrl: https://developer.copper.com/projects/fetch-a-project-by-id.html 237 | requestMethod: GET 238 | - endpoint: /projects/search 239 | operationId: getAll 240 | operationUrl: https://developer.copper.com/projects/list-projects-search.html 241 | requestMethod: POST 242 | filters: 243 | requestBody: 244 | name: string|Name of the project to filter by. 245 | - endpoint: /projects/{projectId} 246 | operationId: update 247 | operationUrl: https://developer.copper.com/projects/update-a-project.html 248 | requestMethod: PUT 249 | updateFields: 250 | requestBody: 251 | name: string|Name to set for the project. 252 | status: 253 | - Open: string|Project with open status. 254 | - Completed: string|Project with closed status. 255 | details: string|Description to set for the project. 256 | assignee_id: string|ID of the user who will own the project. 257 | 258 | task: 259 | - endpoint: /tasks 260 | operationId: create 261 | operationUrl: https://developer.copper.com/tasks/create-a-new-task.html 262 | requestMethod: POST 263 | requiredFields: 264 | requestBody: 265 | name: string 266 | additionalFields: 267 | requestBody: 268 | assignee_id: string|ID of the user who will own the task to create. 269 | priority: string|Priority of the task to create. 270 | status: 271 | - Open: string|Project with open status. 272 | - Completed: string|Project with closed status. 273 | details: string|Description of the task to create. 274 | - endpoint: /tasks/{taskId} 275 | operationId: delete 276 | operationUrl: https://developer.copper.com/tasks/delete-a-task.html 277 | requestMethod: DELETE 278 | - endpoint: /tasks/{taskId} 279 | operationId: get 280 | operationUrl: https://developer.copper.com/tasks/fetch-a-task-by-id.html 281 | requestMethod: GET 282 | - endpoint: /tasks/search 283 | operationId: getAll 284 | operationUrl: https://developer.copper.com/tasks/list-tasks-search.html 285 | requestMethod: POST 286 | filters: 287 | requestBody: 288 | assignee_ids: string|Comma-separated IDs of assignee IDs to filter by. 289 | project_ids: string|Comma-separated IDs of project IDs to filter by. 290 | - endpoint: /tasks/{taskId} 291 | operationId: update 292 | operationUrl: https://developer.copper.com/tasks/update-a-task.html 293 | requestMethod: PUT 294 | updateFields: 295 | requestBody: 296 | name: string|Name to set for the task. 297 | assignee_id: string|ID of the user who will own the task. 298 | priority: string|Priority to set for the task. 299 | details: string|Description to set for the task. -------------------------------------------------------------------------------- /src/input/openApi/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivov/nodebuilder/af0494499696403af23655a17a92d7fdfc08a8b0/src/input/openApi/.gitkeep -------------------------------------------------------------------------------- /src/output/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivov/nodebuilder/af0494499696403af23655a17a92d7fdfc08a8b0/src/output/.gitkeep -------------------------------------------------------------------------------- /src/scripts/clear.ts: -------------------------------------------------------------------------------- 1 | import { outputDir } from "../config"; 2 | import fs from "fs"; 3 | import { join } from "path"; 4 | import { promisify } from "util"; 5 | 6 | const deleteDir = promisify(fs.rmdir); 7 | const deleteFile = promisify(fs.unlink); 8 | 9 | (async () => { 10 | const descriptionsDir = join(outputDir, "descriptions"); 11 | if (fs.existsSync(descriptionsDir)) { 12 | await deleteDir(descriptionsDir, { recursive: true }); 13 | } 14 | })(); 15 | 16 | const isFile = (file: string) => fs.lstatSync(join(outputDir, file)).isFile(); 17 | 18 | const filesToDelete = fs 19 | .readdirSync(outputDir) 20 | .filter(isFile) 21 | .filter((file) => file !== ".gitkeep"); 22 | 23 | filesToDelete.forEach(async (file) => { 24 | await deleteFile(join(outputDir, file)); 25 | }); 26 | -------------------------------------------------------------------------------- /src/scripts/dev.ts: -------------------------------------------------------------------------------- 1 | import NodeCodeGenerator from "../services/NodeCodeGenerator"; 2 | import CustomSpecStager from "../services/CustomSpecStager"; 3 | import CustomSpecAdjuster from "../services/CustomSpecAdjuster"; 4 | import FilePrinter from "../utils/FilePrinter"; 5 | import OpenApiStager from "../services/OpenApiStager"; 6 | import PackageJsonGenerator from "../services/PackageJsonGenerator"; 7 | 8 | // for quick testing only 9 | 10 | // const adjustedParams = new CustomSpecAdjuster("abc.yaml").run(); 11 | // const stagedParams = new CustomSpecStager(adjustedParams).run(); 12 | 13 | const stagedParams = new OpenApiStager("misp.json").run(); 14 | 15 | new FilePrinter(stagedParams).print({ format: "json" }); 16 | new NodeCodeGenerator(stagedParams.mainParams).run(); 17 | 18 | // new PackageJsonGenerator(stagedParamsFromYaml.metaParams).run(); 19 | -------------------------------------------------------------------------------- /src/scripts/generate.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from "child_process"; 2 | import NodeCodeGenerator from "../services/NodeCodeGenerator"; 3 | import OpenApiStager from "../services/OpenApiStager"; 4 | // import CustomSpecParser from "../services/CustomSpecParser"; 5 | import FilePrinter from "../utils/FilePrinter"; 6 | import Prompter from "../services/Prompter"; 7 | import CustomSpecStager from "../services/CustomSpecStager"; 8 | import CustomSpecAdjuster from "../services/CustomSpecAdjuster"; 9 | import { openApiInputDir, swagger } from "../config"; 10 | import path from "path"; 11 | 12 | (async () => { 13 | const prompter = new Prompter(); 14 | const sourceType = await prompter.askForSourceType(); 15 | let stagedParams; 16 | if (sourceType === "Custom API mapping in YAML") { 17 | const customFile = await prompter.askForCustomYamlFile(); 18 | // const parsedParams = new CustomSpecParser(customFile).run(); 19 | const adjustedParams = new CustomSpecAdjuster(customFile).run(); 20 | stagedParams = new CustomSpecStager(adjustedParams).run(); 21 | } else { 22 | let openApiFile = await prompter.askForOpenApiFile(); 23 | 24 | if (openApiFile.endsWith(".yaml")) { 25 | const source = path.join(openApiInputDir, openApiFile); 26 | const openApiFileName = openApiFile.replace(/.yaml/, ""); 27 | const target = path.join(openApiInputDir, `${openApiFileName}.json`); 28 | 29 | execSync(`node ${swagger} bundle -o ${target} ${source}`); 30 | 31 | openApiFile = `${openApiFileName}.json`; 32 | } 33 | 34 | stagedParams = new OpenApiStager(openApiFile).run(); 35 | } 36 | 37 | new FilePrinter(stagedParams).print({ format: "json" }); 38 | new NodeCodeGenerator(stagedParams.mainParams).run(); 39 | })(); 40 | -------------------------------------------------------------------------------- /src/scripts/map.ts: -------------------------------------------------------------------------------- 1 | // import { sortBy } from "underscore"; 2 | // import nodegenParams from "../input/_nodegenParams.json"; 3 | // import FilePrinter from "../utils/FilePrinter"; 4 | 5 | // // @ts-ignore 6 | // const { mainParams } = nodegenParams as NodegenParams; 7 | 8 | // const addIrregularMarkers = true; 9 | 10 | // const apiMap: ApiMap = {}; 11 | 12 | // const derivenodeOperation = (requestMethod: string, endpoint: string) => { 13 | // const hasBracket = (endpoint: string) => endpoint.split("").includes("}"); 14 | 15 | // if (requestMethod === "GET" && hasBracket(endpoint)) return "Get"; 16 | // if (requestMethod === "GET" && !hasBracket(endpoint)) return "Get All"; 17 | // if (requestMethod === "PUT") return "Update"; 18 | // if (requestMethod === "DELETE") return "Delete"; 19 | // if (requestMethod === "POST") return "Create"; 20 | 21 | // return "Unknown"; 22 | // }; 23 | 24 | // Object.entries(mainParams).forEach(([resource, operations]) => { 25 | // apiMap[resource] = operations.map(({ requestMethod, endpoint }) => { 26 | // const result: { [key: string]: any } = {}; 27 | 28 | // if (addIrregularMarkers && /}\//.test(endpoint)) 29 | // result["IRREGULAR START"] = "-".repeat(40); 30 | 31 | // result.nodeOperation = derivenodeOperation(requestMethod, endpoint); 32 | // result.requestMethod = requestMethod; 33 | // result.endpoint = endpoint; 34 | 35 | // if (addIrregularMarkers && /}\//.test(endpoint)) 36 | // result["IRREGULAR END"] = "-".repeat(40); 37 | 38 | // return result as ApiMapOperation; 39 | // }); 40 | // }); 41 | 42 | // Object.entries(apiMap).forEach(([resource, operations]) => { 43 | // apiMap[resource] = sortBy( 44 | // operations, 45 | // (operation: ApiMapOperation) => operation.nodeOperation 46 | // ); 47 | // }); 48 | 49 | // const printer = new FilePrinter(apiMap); 50 | // printer.print({ format: "json" }); 51 | // console.log("Successfully printed API map"); 52 | 53 | // // console.log(apiMap); 54 | -------------------------------------------------------------------------------- /src/scripts/place.ts: -------------------------------------------------------------------------------- 1 | import { OutputPlacer } from "../services/OutputPlacer"; 2 | import Prompter from "../services/Prompter"; 3 | 4 | new Prompter().askForPlacementTargetType().then((targetType) => { 5 | new OutputPlacer({ targetType }).run(); 6 | }); 7 | -------------------------------------------------------------------------------- /src/scripts/render.ts: -------------------------------------------------------------------------------- 1 | import json from "../input/openApi/lichess.json"; 2 | 3 | import TreeRenderer from "../utils/TreeRenderer"; 4 | import OpenApiStager from "../services/OpenApiStager"; 5 | import FilePrinter from "../utils/FilePrinter"; 6 | 7 | const nodegenParams = new OpenApiStager("lichess").run(); 8 | const treeViewer = new TreeRenderer(nodegenParams.mainParams, json); 9 | const treeview = treeViewer.run(); 10 | const printer = new FilePrinter(treeview); 11 | printer.print({ format: "txt" }); 12 | -------------------------------------------------------------------------------- /src/services/CustomSpecAdjuster.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import { load as toJsObject } from "js-yaml"; 4 | import { customInputDir } from "../config"; 5 | 6 | export default class CustomSpecAdjuster { 7 | private customSpecFileName: string; 8 | private mainParams: CustomSpecParams["mainParams"]; 9 | 10 | constructor(customSpecFileName: string) { 11 | this.customSpecFileName = customSpecFileName; 12 | } 13 | 14 | public run() { 15 | const parsedCustomSpec = this.parseCustomSpec(); 16 | 17 | this.mainParams = parsedCustomSpec.mainParams; 18 | 19 | this.mainParams = this.sortKeys(this.mainParams); 20 | this.separateKeys(this.mainParams); 21 | 22 | return { 23 | mainParams: this.mainParams, 24 | metaParams: parsedCustomSpec.metaParams, 25 | }; 26 | } 27 | 28 | public parseCustomSpec() { 29 | const filePath = path.join(customInputDir, this.customSpecFileName); 30 | const fileContent = fs.readFileSync(filePath, "utf-8"); 31 | return toJsObject(fileContent) as CustomSpecParams; 32 | } 33 | 34 | private sortKeys(value: any): any { 35 | if (this.cannotBeSorted(value)) return value; 36 | 37 | // alphabetize operations by operationId 38 | if (this.isOperationsArray(value)) { 39 | value; 40 | const sortedIds = value.map((i) => i.operationId).sort(); 41 | return sortedIds.map((id) => value.find((i) => i.operationId === id)); 42 | } 43 | 44 | // alphabetize object keys - recursive 45 | const sorted: { [key: string]: string | object } = {}; 46 | 47 | Object.keys(value) 48 | .sort() 49 | .forEach((key) => { 50 | sorted[key] = this.sortKeys(value[key]); 51 | }); 52 | 53 | return sorted; 54 | } 55 | 56 | private skipFields = [ 57 | "endpoint", 58 | "operationId", 59 | "requestMethod", 60 | "operationUrl", 61 | ]; 62 | 63 | private separateKeys(obj: any) { 64 | Object.keys(obj).forEach((key) => { 65 | if (this.skipFields.includes(key)) return; 66 | 67 | const value = obj[key]; 68 | 69 | if (this.isStringArray(value)) 70 | value.forEach((i: string) => (obj[key] = this.adjustSeparator(i))); 71 | 72 | if (this.isObjectArray(value)) 73 | value.forEach((i: object) => this.separateKeys(i)); 74 | 75 | if (this.isTraversableObject(value)) return this.separateKeys(value); 76 | 77 | obj[key] = this.adjustSeparator(value); 78 | }); 79 | } 80 | 81 | private adjustSeparator(value: string | object[]) { 82 | if (Array.isArray(value)) return value; 83 | 84 | if (value.startsWith("options") && !value.includes("|")) { 85 | const [type, ...items] = value.split("-").map((i) => i.trim()); 86 | 87 | return { 88 | type, 89 | options: items, 90 | default: items[0], 91 | }; 92 | } 93 | 94 | if (value.startsWith("options") && value.includes("|")) { 95 | const [type, rest] = value.split("|"); 96 | const [description, ...items] = rest.split("-").map((i) => i.trim()); 97 | const defaultOption = type.includes("=") ? type.split("=")[1] : items[0]; 98 | 99 | return { 100 | type, 101 | description, 102 | options: items, 103 | default: defaultOption, 104 | }; 105 | } 106 | 107 | if (!this.isSimpleType(value)) throw new Error("Unknown type: " + value); 108 | 109 | if (!value.includes("|")) { 110 | return { type: value, default: this.getDefaultValue(value) }; 111 | } 112 | 113 | if (value.includes("=|")) { 114 | const [type, description] = value.split("|"); 115 | const [typeValue, defaultValue] = type.split("="); 116 | return { type: typeValue, description, default: defaultValue }; 117 | } 118 | 119 | if (value.includes("|")) { 120 | const [type, description] = value.split("|"); 121 | return { type, description, default: this.getDefaultValue(type) }; 122 | } 123 | 124 | return { type: value, default: this.getDefaultValue(value) }; 125 | } 126 | 127 | // ---------------------------------- 128 | // utils 129 | // ---------------------------------- 130 | 131 | private getDefaultValue(type: string) { 132 | if (type === "boolean") return false; 133 | if (type === "number") return 0; 134 | 135 | return ""; 136 | } 137 | 138 | private cannotBeSorted(value: unknown) { 139 | return !value || typeof value !== "object"; 140 | } 141 | 142 | private isOperationsArray(value: unknown): value is CustomSpecOperation[] { 143 | return Array.isArray(value) && value[0].operationId; 144 | } 145 | 146 | private isObjectArray(value: unknown) { 147 | return ( 148 | Array.isArray(value) && !!value.length && typeof value[0] === "object" 149 | ); 150 | } 151 | 152 | private isStringArray(value: Array) { 153 | return ( 154 | Array.isArray(value) && !!value.length && typeof value[0] === "string" 155 | ); 156 | } 157 | 158 | private isTraversableObject(value: unknown) { 159 | return ( 160 | value && 161 | typeof value === "object" && 162 | !Array.isArray(value) && 163 | !!Object.keys(value).length 164 | ); 165 | } 166 | 167 | private isSimpleType(value: string) { 168 | const simpleTypes = [ 169 | "string", 170 | "number", 171 | "boolean", 172 | "loadOptions", 173 | "dateTime", 174 | ]; 175 | return simpleTypes.some((type) => value.includes(type)); 176 | } 177 | } 178 | 179 | // TODO: Review this, for sortKeys 180 | // if (Array.isArray(value)) { 181 | // const newArr = value.map((item) => this.sortKeys(item)); 182 | 183 | // // sort dropdown options 184 | // if (newArr.every((i) => Object.keys(i).length === 1)) { 185 | // const orderedKeys = value.map((i) => Object.keys(i)[0]).sort(); 186 | // return orderedKeys.map((key) => newArr.find((i) => i[key])); 187 | // } 188 | 189 | // return newArr.sort(); 190 | // } 191 | -------------------------------------------------------------------------------- /src/services/CustomSpecStager.ts: -------------------------------------------------------------------------------- 1 | import { snakeCase } from "change-case"; 2 | 3 | /** 4 | * Responsible for staging traversed params into nodegen params. 5 | * Staging params are for consumption by nodegen templates. 6 | */ 7 | export default class CustomSpecStager { 8 | private inputMainParams: CustomSpecParams["mainParams"]; 9 | private outputMetaParams: MetaParams; 10 | private outputMainParams: MainParams = {}; 11 | 12 | private outputOperation: Operation; 13 | private currentResource = ""; 14 | 15 | constructor(yamlNodegenParams: any) { 16 | this.inputMainParams = yamlNodegenParams.mainParams; 17 | this.outputMetaParams = yamlNodegenParams.metaParams; 18 | } 19 | 20 | public run(): NodegenParams { 21 | this.initializeMainParams(); 22 | 23 | this.loopOverInputOperations((inputOperation) => { 24 | // this.validateInputOperation(inputOperation); 25 | this.initializeOutputOperation(inputOperation); 26 | this.populateOutputOperation(inputOperation); 27 | }); 28 | 29 | this.unescapeNodeColorHash(); 30 | 31 | return { 32 | mainParams: this.outputMainParams, 33 | metaParams: this.outputMetaParams, 34 | }; 35 | } 36 | 37 | // ---------------------------------- 38 | // validators 39 | // ---------------------------------- 40 | 41 | validateInputOperation(operation: CustomSpecOperation) { 42 | const errors = []; 43 | 44 | // TODO: Rethink this check 45 | // if (operation.requestMethod === "POST" && !operation.requiredFields) { 46 | // const invalidOperation = JSON.stringify(operation, null, 2); 47 | // errors.push( 48 | // `POST request is missing required request body params:\n${invalidOperation}` 49 | // ); 50 | // } 51 | 52 | if (this.needsRouteParam(operation) && !operation.endpoint.includes("{")) { 53 | const invalidOperation = JSON.stringify(operation, null, 2); 54 | errors.push( 55 | `Operation is missing required route param:\n${invalidOperation}` 56 | ); 57 | } 58 | 59 | if (errors.length) { 60 | throw new Error(`Validation failed:\n${errors}`); 61 | } 62 | } 63 | 64 | // ---------------------------------- 65 | // initializers and populators 66 | // ---------------------------------- 67 | 68 | private initializeMainParams() { 69 | this.getResources().forEach((resource) => { 70 | this.outputMainParams[resource] = []; 71 | }); 72 | } 73 | 74 | private initializeOutputOperation({ 75 | endpoint, 76 | requestMethod, 77 | operationId, 78 | operationUrl, 79 | }: CustomSpecOperation) { 80 | this.outputOperation = { 81 | endpoint, 82 | requestMethod, 83 | operationId, 84 | }; 85 | 86 | this.outputOperation.description = this.getOperationDescription(); 87 | 88 | if (operationUrl) this.outputOperation.operationUrl = operationUrl; 89 | } 90 | 91 | private getOperationDescription() { 92 | const { operationId } = this.outputOperation; 93 | 94 | let adjustedResource = this.handleMultipleWords(this.currentResource); 95 | 96 | if (operationId === "getAll") return `Retrieve all ${adjustedResource}s`; 97 | 98 | const addArticle = (resource: string) => 99 | "aeiou".split("").includes(this.currentResource.charAt(0)) 100 | ? `an ${resource}` 101 | : `a ${resource}`; 102 | 103 | const capitalize = (resource: string) => 104 | resource.charAt(0).toUpperCase() + resource.slice(1); 105 | 106 | let adjustedCurrentResource = addArticle(adjustedResource); 107 | 108 | return `${capitalize(operationId)} ${adjustedCurrentResource}`; 109 | } 110 | 111 | private loopOverInputOperations( 112 | callback: (inputOperation: CustomSpecOperation) => void 113 | ) { 114 | this.getResources().forEach((resource) => { 115 | this.currentResource = resource; 116 | this.inputMainParams[resource].forEach(callback); 117 | }); 118 | } 119 | 120 | private populateOutputOperation(inputOperation: CustomSpecOperation) { 121 | const { 122 | requiredFields, 123 | additionalFields, 124 | filters, 125 | updateFields, 126 | } = inputOperation; 127 | 128 | // path params 129 | 130 | const outputPathParams = this.handlePathParams(inputOperation); 131 | 132 | if (outputPathParams) this.outputOperation.parameters = outputPathParams; 133 | 134 | // qs params (required) 135 | 136 | const outputQsParams = this.handleRequiredQsParams( 137 | requiredFields?.queryString, 138 | { 139 | required: true, 140 | } 141 | ); 142 | 143 | if (outputQsParams) this.outputOperation.parameters = outputQsParams; 144 | 145 | // qs params (extra) - additional fields 146 | 147 | const outputQsAddFields = this.stageQsExtraFields(additionalFields, { 148 | name: "Additional Fields", 149 | }); 150 | 151 | if (outputQsAddFields) 152 | this.outputOperation.additionalFields = outputQsAddFields; 153 | 154 | // qs params (extra) - filters 155 | 156 | const outputQsFilters = this.stageQsExtraFields(filters, { 157 | name: "Filters", 158 | }); 159 | 160 | if (this.outputOperation.parameters && outputQsFilters) 161 | this.outputOperation.parameters.push(...outputQsFilters.options); 162 | 163 | if (!this.outputOperation.parameters && outputQsFilters) 164 | this.outputOperation.parameters = outputQsFilters.options; 165 | 166 | // qs params (extra) - update fields 167 | 168 | const outputQsUpdateFields = this.stageQsExtraFields(updateFields, { 169 | name: "Update Fields", 170 | }); 171 | 172 | if (outputQsUpdateFields) 173 | this.outputOperation.updateFields = outputQsUpdateFields; 174 | 175 | // required body (required) 176 | 177 | const outputRequestBody = this.stageRequestBody( 178 | requiredFields?.requestBody, 179 | { 180 | required: true, 181 | name: "Standard", 182 | } 183 | ); 184 | 185 | this.outputOperation.requestBody = outputRequestBody ?? []; 186 | 187 | // required body (extra) 188 | 189 | this.handleRequestBodyExtraFields(additionalFields, { 190 | name: "Additional Fields", 191 | }); 192 | this.handleRequestBodyExtraFields(filters, { name: "Filters" }); 193 | this.handleRequestBodyExtraFields(updateFields, { name: "Update Fields" }); 194 | 195 | this.outputMainParams[this.currentResource].push(this.outputOperation); 196 | } 197 | 198 | // ---------------------------------- 199 | // handlers 200 | // ---------------------------------- 201 | 202 | /** 203 | * Handle path params (if any) by forwarding them for staging. 204 | */ 205 | private handlePathParams(inputOperation: CustomSpecOperation) { 206 | if (!inputOperation.endpoint.match(/\{/)) return null; 207 | 208 | const pathParams = inputOperation.endpoint.match(/(?<={)(.*?)(?=})/g); 209 | 210 | if (!pathParams) return null; 211 | 212 | return pathParams.map((pathParam) => 213 | this.stagePathParam(pathParam, inputOperation) 214 | ); 215 | } 216 | 217 | /** 218 | * Handle required query string params (if any) by forwarding them for staging. 219 | */ 220 | private handleRequiredQsParams( 221 | queryString: CustomSpecFieldContent | undefined, 222 | { required }: { required: true } 223 | ) { 224 | if (!queryString) return null; 225 | 226 | return Object.entries(queryString).map(([key, value]) => 227 | this.stageQsParam(key, value, { required }) 228 | ); 229 | } 230 | 231 | /** 232 | * Handle extra fields in request body (if any) by forwarding them for staging. 233 | */ 234 | private handleRequestBodyExtraFields( 235 | extraFields: CustomSpecFields | undefined, 236 | { 237 | name, 238 | }: { 239 | name: "Additional Fields" | "Filters" | "Update Fields"; 240 | } 241 | ) { 242 | const rbExtraFields = this.stageRequestBody(extraFields?.requestBody, { 243 | required: false, 244 | name, 245 | }); 246 | 247 | if (rbExtraFields && this.outputOperation.requestBody) { 248 | this.outputOperation.requestBody.push(...rbExtraFields); 249 | } 250 | } 251 | 252 | // ---------------------------------- 253 | // stagers 254 | // ---------------------------------- 255 | 256 | private stagePathParam( 257 | pathParam: string, 258 | { operationId }: CustomSpecOperation 259 | ) { 260 | const output: OperationParameter = { 261 | in: "path" as const, 262 | name: pathParam, 263 | schema: { 264 | type: "string", 265 | default: "", 266 | }, 267 | required: true, 268 | }; 269 | 270 | let description = `ID of the ${this.handleMultipleWords( 271 | this.currentResource 272 | )} to `; 273 | 274 | if ( 275 | operationId === "create" || 276 | operationId === "update" || 277 | operationId === "delete" 278 | ) { 279 | output.description = description + operationId; 280 | } else if (operationId === "get") { 281 | output.description = description + "retrieve"; 282 | } 283 | 284 | return output; 285 | } 286 | 287 | private stageQsExtraFields( 288 | extraFields: CustomSpecFields | undefined, 289 | { name }: { name: ExtraFieldName } 290 | ) { 291 | if (!extraFields) return null; 292 | 293 | const qsExtraFields = extraFields.queryString; 294 | 295 | if (!qsExtraFields) return null; 296 | 297 | const output: AdditionalFields = { 298 | name, 299 | type: "collection", 300 | description: "", 301 | default: {}, 302 | options: [], 303 | }; 304 | 305 | Object.entries(qsExtraFields).forEach(([key, value]) => 306 | output.options.push(this.stageQsParam(key, value, { required: false })) 307 | ); 308 | 309 | return output.options.length ? output : null; 310 | } 311 | 312 | private stageQsParam( 313 | key: string, 314 | value: ParamContent, 315 | { required }: { required: boolean } 316 | ) { 317 | const output: OperationParameter = { 318 | in: "query" as const, 319 | name: key, 320 | required, 321 | schema: { 322 | type: value.type, 323 | default: value.default, 324 | }, 325 | }; 326 | 327 | if (value.type === "options" && value.options) { 328 | output.schema.options = value.options; 329 | } 330 | 331 | if (value.description) { 332 | output.description = this.supplementLink(value.description); 333 | } 334 | 335 | return output; 336 | } 337 | 338 | public stageRequestBody( 339 | requestBody: CustomSpecFieldContent | undefined, 340 | { 341 | required, 342 | name, 343 | }: { 344 | required: boolean; 345 | name: "Standard" | ExtraFieldName; 346 | } 347 | ) { 348 | if (!requestBody) return null; 349 | 350 | const outputRequestBody: OperationRequestBody = { 351 | name, 352 | required, 353 | content: { 354 | // TODO: add `multipart/form-data` and `text/plain` 355 | "application/x-www-form-urlencoded": { 356 | schema: { 357 | type: "object", 358 | properties: {}, 359 | }, 360 | }, 361 | }, 362 | }; 363 | 364 | const formUrlEncoded = "application/x-www-form-urlencoded"; 365 | 366 | Object.entries(requestBody).forEach(([key, value]) => { 367 | const properties = 368 | outputRequestBody.content[formUrlEncoded]?.schema.properties; 369 | 370 | if (value.description) 371 | value.description = this.supplementLink(value.description); 372 | 373 | if (properties) { 374 | properties[key] = value; 375 | } 376 | }); 377 | 378 | outputRequestBody.content[ 379 | formUrlEncoded 380 | ]!.schema.properties = this.sortObject( 381 | outputRequestBody.content[formUrlEncoded]?.schema.properties 382 | ); 383 | 384 | return [outputRequestBody]; 385 | } 386 | 387 | // ---------------------------------- 388 | // utils 389 | // ---------------------------------- 390 | 391 | private handleMultipleWords(resource: string) { 392 | return snakeCase(resource).includes("_") 393 | ? snakeCase(resource).replace(/_/g, " ") 394 | : resource; 395 | } 396 | 397 | /** 398 | * TODO: Type properly 399 | */ 400 | private sortObject(obj: { [key: string]: any } | undefined) { 401 | if (!obj) return; 402 | 403 | return Object.keys(obj) 404 | .sort() 405 | .reduce((result, key) => { 406 | result[key] = obj[key]; 407 | return result; 408 | }, {}); 409 | } 410 | 411 | /** 412 | * Remove `\` from `#` in the node color in the meta params in the YAML file. 413 | */ 414 | private unescapeNodeColorHash() { 415 | this.outputMetaParams.nodeColor = this.outputMetaParams.nodeColor.replace( 416 | "\\#", 417 | "#" 418 | ); 419 | } 420 | 421 | /** 422 | * Return all the resource names of the API. 423 | */ 424 | private getResources() { 425 | return Object.keys(this.inputMainParams); 426 | } 427 | 428 | /** 429 | * Add `target="_blank"` to any link in a param description. 430 | */ 431 | private supplementLink(description: string) { 432 | if (description.includes("', '" target="_blank">'); 434 | } 435 | 436 | return description; 437 | } 438 | 439 | private needsRouteParam(operation: CustomSpecOperation) { 440 | return ( 441 | (operation.requestMethod === "GET" && 442 | operation.operationId !== "getAll") || 443 | operation.requestMethod === "DELETE" || 444 | operation.requestMethod === "PATCH" 445 | ); 446 | } 447 | } 448 | -------------------------------------------------------------------------------- /src/services/NodeCodeGenerator.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from "child_process"; 2 | import fs from "fs"; 3 | import path from "path"; 4 | 5 | import { descriptionsOutputDir, hygen, inputDir, outputDir } from "../config"; 6 | 7 | export default class NodeCodeGenerator { 8 | private mainParams: MainParams; 9 | private resourceJson = path.join(inputDir, "_resource.json"); 10 | 11 | constructor(mainParams: MainParams) { 12 | this.mainParams = mainParams; 13 | } 14 | 15 | public run() { 16 | this.validateDirs(); 17 | 18 | this.generateResourceDescriptions(); 19 | 20 | this.generateRegularNodeFile(); 21 | 22 | this.generateGenericFunctionsFile(); 23 | 24 | // if (this.metaParams.authType !== "None") { 25 | // this.generateCredentialsFile(); 26 | // } 27 | } 28 | 29 | private executeCommand(command: string) { 30 | try { 31 | execSync(`env HYGEN_OVERWRITE=1 node ${hygen} ${command}`); 32 | } catch (error) { 33 | console.log(error.stdout.toString()); 34 | console.log(error.message); 35 | } 36 | } 37 | 38 | private generateRegularNodeFile() { 39 | this.executeCommand("make regularNodeFile"); 40 | } 41 | 42 | private generateGenericFunctionsFile() { 43 | this.executeCommand("make genericFunctions"); 44 | } 45 | 46 | /**For every resource in main params, generate a resource JSON file, feeds it into 47 | * the Hygen template for code generation and deletes the resource JSON file.*/ 48 | private generateResourceDescriptions() { 49 | // TEMP: only first resource ----------------------------- 50 | // const firstResourceName = Object.keys(this.mainParams)[0]; 51 | // this.saveResourceJson( 52 | // firstResourceName, 53 | // this.mainParams[firstResourceName] 54 | // ); 55 | 56 | // this.executeCommand("make resourceDescription"); 57 | 58 | // const resourceNames = Object.keys(this.mainParams); 59 | // this.executeCommand(`make resourceIndex --resourceNames ${resourceNames}`); 60 | // unlinkSync(this.resourceJson); 61 | // TEMP ------------------------------------------- 62 | 63 | // FINAL VERSION: ALL RESOURCES 64 | const resourceNames = Object.keys(this.mainParams); 65 | Object.entries(this.mainParams).forEach( 66 | ([resourceName, operationsArray]) => { 67 | this.saveResourceJson(resourceName, operationsArray); 68 | this.executeCommand("make resourceDescription"); 69 | fs.unlinkSync(this.resourceJson); 70 | } 71 | ); 72 | this.executeCommand(`make resourceIndex --resourceNames ${resourceNames}`); 73 | } 74 | 75 | private validateDirs() { 76 | [inputDir, outputDir, descriptionsOutputDir].forEach((dir) => { 77 | if (!fs.existsSync(dir)) fs.mkdirSync(dir); 78 | }); 79 | } 80 | 81 | private saveResourceJson(resourceName: string, operationsArray: Operation[]) { 82 | fs.writeFileSync( 83 | this.resourceJson, 84 | JSON.stringify({ resourceName, operationsArray }, null, 2), 85 | "utf8" 86 | ); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/services/OpenApiStager.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from "child_process"; 2 | import path from "path"; 3 | import fs from "fs"; 4 | import { JSONPath as jsonQuery } from "jsonpath-plus"; 5 | import { titleCase } from "title-case"; 6 | import { camelCase } from "change-case"; 7 | import pluralize from "pluralize"; 8 | import { inputDir, openApiInputDir, swagger } from "../config"; 9 | 10 | export default class OpenApiStager { 11 | private readonly json: JsonObject & { paths: object }; 12 | private readonly serviceName: string; 13 | private currentEndpoint: string; 14 | private currentResource: string; 15 | 16 | constructor(serviceName: string) { 17 | this.serviceName = serviceName.replace(".json", ""); 18 | this.json = this.parseSpec(serviceName); 19 | } 20 | 21 | public run(): NodegenParams { 22 | return { 23 | metaParams: { 24 | apiUrl: this.getApiUrl(), 25 | authType: this.getAuthType(), 26 | serviceName: titleCase(this.serviceName), 27 | nodeColor: this.getNodeColor(), 28 | }, 29 | mainParams: this.getMainParams(), 30 | }; 31 | } 32 | 33 | /** 34 | * Replace `$ref` with its referenced value and parse the resulting JSON. 35 | * */ 36 | private parseSpec(serviceName: string) { 37 | const source = path.join(openApiInputDir, serviceName); 38 | const target = path.join(inputDir, "_deref.json"); 39 | 40 | execSync( 41 | `node ${swagger} bundle --dereference ${source} --outfile ${target}` 42 | ); 43 | 44 | return JSON.parse(fs.readFileSync(target).toString()); 45 | } 46 | 47 | private getApiUrl() { 48 | return jsonQuery({ json: this.json, path: "$.servers.*.url" })[0]; 49 | } 50 | 51 | // TODO: temp implementation 52 | private getNodeColor() { 53 | return "#ffffff"; 54 | } 55 | 56 | // TODO: temp implementation 57 | private getAuthType(): AuthType { 58 | return "OAuth2"; 59 | } 60 | 61 | private getMainParams() { 62 | let mainParams: MainParams = {}; 63 | 64 | for (const endpoint in this.json.paths) { 65 | this.currentEndpoint = endpoint; 66 | 67 | const resources = this.getResources(); 68 | const methods = this.extract("requestMethods"); 69 | 70 | resources.forEach((resource) => { 71 | methods.forEach((method) => { 72 | this.currentResource = resource; 73 | const operation = this.createOperation(method); 74 | mainParams[resource] = mainParams[resource] || []; // TODO: nullish-coalescing operator 75 | mainParams[resource].push(operation); 76 | }); 77 | 78 | mainParams[resource] = this.alphabetizeOperations(mainParams[resource]); 79 | }); 80 | } 81 | 82 | return this.alphabetizeResources(mainParams); 83 | } 84 | 85 | private getResources() { 86 | const resources = this.extract("tags").filter((r) => r !== "OAuth"); 87 | return [...new Set(resources)].map((r) => this.singularize(r)); 88 | } 89 | 90 | private processDescription() { 91 | const description = this.extract("description"); 92 | 93 | if (description) return this.escape(description); 94 | } 95 | 96 | private escape(description: string) { 97 | return description 98 | .replace(/\n/g, " ") 99 | .replace(/\s+/g, " ") 100 | .replace(/'/g, "\\'") 101 | .trim(); 102 | } 103 | 104 | private processRequestBody() { 105 | const requestBody = this.extract("requestBody"); 106 | 107 | if (!requestBody) return null; 108 | 109 | const urlEncoded = requestBody.content["application/x-www-form-urlencoded"]; 110 | const textPlain = requestBody.content["text/plain"]; 111 | 112 | if (urlEncoded) { 113 | this.sanitizeProperties(urlEncoded); 114 | } 115 | 116 | if (textPlain) { 117 | this.setTextPlainProperty(requestBody); 118 | } 119 | 120 | return [{ name: "Standard", ...requestBody } as const]; 121 | } 122 | 123 | private setTextPlainProperty(requestBody: OperationRequestBody) { 124 | requestBody.textPlainProperty = requestBody.description 125 | ?.split(" ")[0] 126 | .toLowerCase(); 127 | } 128 | 129 | private sanitizeProperties(urlEncoded: { schema: RequestBodySchema }) { 130 | const properties = Object.keys(urlEncoded.schema.properties); 131 | properties.forEach((property) => { 132 | const sanitizedProperty = camelCase(property.replace(".", " ")); 133 | 134 | urlEncoded.schema.properties[sanitizedProperty] = 135 | urlEncoded.schema.properties[property]; 136 | delete urlEncoded.schema.properties[property]; 137 | }); 138 | } 139 | 140 | private createOperation(requestMethod: string) { 141 | const operation: Operation = { 142 | endpoint: this.currentEndpoint, 143 | requestMethod: requestMethod.toUpperCase(), 144 | operationId: this.processOperationId(requestMethod), 145 | }; 146 | 147 | const parameters = this.processParameters(); 148 | const requestBody = this.processRequestBody(); 149 | const description = this.processDescription(); 150 | 151 | if (parameters.length) operation.parameters = parameters; 152 | if (requestBody?.length) operation.requestBody = requestBody; 153 | if (description) operation.description = description; 154 | 155 | return operation; 156 | } 157 | 158 | private processOperationId(requestMethod: string) { 159 | let extracted = this.extract("operationId"); 160 | 161 | if (!extracted) return this.getFallbackId(requestMethod); 162 | 163 | if (extracted.endsWith("ById")) return "get"; 164 | 165 | if (extracted.match(/get./)) { 166 | const words = this.camelCaseToSpaced(extracted).split(" "); 167 | const lastWord = words.slice(-1).join(""); 168 | return lastWord.endsWith("s") ? "getAll" : extracted; 169 | } 170 | 171 | if (extracted.startsWith("edit")) return "update"; 172 | 173 | if (extracted.startsWith("add")) return "create"; 174 | 175 | const surplusPart = this.currentResource.replace(" ", ""); 176 | const surplusRegex = new RegExp(surplusPart, "g"); 177 | 178 | return extracted.replace(surplusRegex, ""); 179 | } 180 | 181 | private processParameters() { 182 | const parameters = this.extract("parameters"); 183 | 184 | parameters.forEach((param) => { 185 | if (param.description) { 186 | param.description = this.escape(param.description); 187 | } 188 | 189 | // TODO: Type properly 190 | // @ts-ignore 191 | if ("oneOf" in param.schema && param.schema.oneOf) { 192 | // @ts-ignore 193 | param.schema = param.schema.oneOf[0]; 194 | } 195 | 196 | // TODO: Type properly 197 | // @ts-ignore 198 | if ("anyOf" in param.schema && param.schema.anyOf) { 199 | // @ts-ignore 200 | param.schema = param.schema.anyOf[0]; 201 | } 202 | }); 203 | 204 | return parameters.map((field) => 205 | field.required ? field : { ...field, required: false } 206 | ); 207 | } 208 | 209 | // ---------------------------------- 210 | // extractors 211 | // ---------------------------------- 212 | 213 | /**Extract the keys and values from the OpenAPI JSON based on the current endpoint. 214 | * Based on [JSON Path Plus](https://github.com/JSONPath-Plus/JSONPath). 215 | * 216 | * Note: The square brackets escape chars in the endpoint.*/ 217 | private extract(key: "description" | "operationId"): string | undefined; 218 | private extract(key: "tags" | "requestMethods"): string[]; 219 | private extract(key: "parameters"): OperationParameter[]; 220 | private extract(key: "requestBody"): OperationRequestBody | null; 221 | private extract(key: OpenApiKey) { 222 | const result = jsonQuery({ 223 | json: this.json, 224 | path: `$.paths.[${this.currentEndpoint}].${this.setEndOfPath(key)}`, 225 | }); 226 | 227 | if (key === "requestBody" && !result.length) return null; 228 | 229 | // always a one-element array, so remove nesting 230 | const hasExtraNesting = 231 | (key === "parameters" && result.length) || 232 | key === "description" || 233 | key === "operationId" || 234 | (key === "requestBody" && result.length); 235 | 236 | if (hasExtraNesting) return result[0]; 237 | 238 | return result; 239 | } 240 | 241 | /**Adjust the end of the JSON Path query based on the key. 242 | * ```json 243 | * $.[/endpoint]. *.tags.* resources 244 | * $.[/endpoint]. *~ request methods 245 | * $.[/endpoint]. *.otherKey 246 | * ``` 247 | * Note: `parameters` is kept in a nested array (instead of together with `tags`) 248 | * for the edge case where the endpoint has 2+ request methods. Otherwise, the 249 | * parameters for both methods become mixed together, causing duplication.*/ 250 | private setEndOfPath(key: OpenApiKey) { 251 | if (key === "tags") return `*.${key}.*`; 252 | if (key === "requestMethods") return `*~`; 253 | return `*.${key}`; 254 | } 255 | 256 | // ---------------------------------- 257 | // utils 258 | // ---------------------------------- 259 | 260 | private singularize(str: string) { 261 | return pluralize(str, 1); 262 | } 263 | 264 | private camelCaseToSpaced(str: string) { 265 | return str.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()); 266 | } 267 | 268 | private alphabetizeResources(obj: { [key: string]: any }) { 269 | const sorted: { [key: string]: any } = {}; 270 | 271 | Object.keys(obj) 272 | .sort() 273 | .forEach((key) => { 274 | sorted[key] = obj[key]; 275 | }); 276 | 277 | return sorted; 278 | } 279 | 280 | private alphabetizeOperations(operations: Operation[]) { 281 | return operations 282 | .map((o) => o.operationId) 283 | .sort() 284 | .map((id) => 285 | this.safeFind(operations, (o: Operation) => o.operationId === id) 286 | ); 287 | } 288 | 289 | private safeFind(arg: T[], cb: (arg: T) => boolean): T { 290 | const found = arg.find(cb); 291 | 292 | if (found === undefined || found === null) { 293 | throw new Error("Expected value is missing"); 294 | } 295 | 296 | return found; 297 | } 298 | 299 | private getFallbackId(requestMethod: string) { 300 | const hasBracket = this.currentEndpoint.split("").includes("}"); 301 | 302 | if (requestMethod === "get" && hasBracket) return "get"; 303 | if (requestMethod === "get" && !hasBracket) return "getAll"; 304 | if (requestMethod === "put") return "update"; 305 | if (requestMethod === "delete") return "delete"; 306 | if (requestMethod === "post") return "create"; 307 | 308 | return "UNNAMED"; 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /src/services/OutputPlacer.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import os from "os"; 4 | import { promisify } from "util"; 5 | import { outputDir } from "../config"; 6 | 7 | const place = promisify(fs.rename); 8 | const rmdir = promisify(fs.rmdir); 9 | 10 | export class OutputPlacer { 11 | private rootFilesToPlace = fs.readdirSync(outputDir); 12 | private outputPackageJsonPath = path.join(outputDir, "package.json"); 13 | private nodeTs: string; // *.node.ts 14 | 15 | private targetType: "clone" | "custom"; 16 | private targetDir: string; 17 | private n8nRootDir = path.join(__dirname, "..", "..", "..", "n8n"); 18 | private nodesBaseDir = path.join(this.n8nRootDir, "packages", "nodes-base"); 19 | 20 | constructor({ targetType }: { targetType: "clone" | "custom" }) { 21 | this.targetType = targetType; 22 | this.targetDir = 23 | targetType === "clone" 24 | ? path.join(this.nodesBaseDir, "nodes") 25 | : path.join(os.homedir(), ".n8n", "custom"); 26 | } 27 | 28 | public async run() { 29 | this.prePlacementCheck(); 30 | 31 | if (this.targetType === "clone") { 32 | await this.placePackageJson(); 33 | } 34 | 35 | await this.placeRootTsFiles(); 36 | await this.placeDescriptionsTsFiles(); 37 | 38 | await this.removeDescriptionsDir(); 39 | } 40 | 41 | private prePlacementCheck() { 42 | if (!fs.existsSync(this.outputPackageJsonPath)) 43 | throw new Error("No `package.json` file found in /output dir"); 44 | 45 | const nodeTs = this.rootFilesToPlace.find((f) => f.endsWith(".node.ts")); 46 | 47 | if (!nodeTs) throw new Error("No `*.node.ts` file found in /output dir"); 48 | 49 | this.nodeTs = nodeTs; 50 | 51 | const genericFunctionsTs = this.rootFilesToPlace.find( 52 | (f) => f === "GenericFunctions.ts" 53 | ); 54 | 55 | if (!genericFunctionsTs) 56 | throw new Error("No `GenericFunctions.ts` file found in /output dir"); 57 | } 58 | 59 | private async placePackageJson() { 60 | const dest = path.join(this.nodesBaseDir, "package.json"); 61 | await place(this.outputPackageJsonPath, dest); 62 | } 63 | 64 | private async placeRootTsFiles() { 65 | const rootTsFiles = this.rootFilesToPlace.filter((f) => f.endsWith(".ts")); 66 | 67 | if (!fs.existsSync(this.targetDir)) fs.mkdirSync(this.targetDir); 68 | 69 | const destDir = path.join( 70 | this.targetDir, 71 | this.nodeTs.replace(".node.ts", "") 72 | ); 73 | 74 | if (!fs.existsSync(destDir)) fs.mkdirSync(destDir); 75 | 76 | rootTsFiles.forEach(async (rootTsFile) => { 77 | const src = path.join(outputDir, rootTsFile); 78 | const dest = path.join(destDir, rootTsFile); 79 | await place(src, dest); 80 | }); 81 | } 82 | 83 | private async placeDescriptionsTsFiles() { 84 | if (!fs.existsSync(path.join(outputDir, "descriptions"))) return; 85 | 86 | const tsDescriptions = fs 87 | .readdirSync(path.join(outputDir, "descriptions")) 88 | .filter((f) => f.endsWith(".ts")); 89 | 90 | const destDir = path.join( 91 | this.targetDir, 92 | this.nodeTs.replace(".node.ts", ""), 93 | "descriptions" 94 | ); 95 | 96 | if (!fs.existsSync(destDir)) fs.mkdirSync(destDir); 97 | 98 | tsDescriptions.forEach(async (tsDescription) => { 99 | const src = path.join(outputDir, "descriptions", tsDescription); 100 | const dest = path.join(destDir, tsDescription); 101 | await place(src, dest); 102 | }); 103 | } 104 | 105 | private async removeDescriptionsDir() { 106 | await rmdir(path.join(outputDir, "descriptions")); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/services/PackageJsonGenerator.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import { execSync } from "child_process"; 4 | import axios from "axios"; 5 | import { hygen, outputDir } from "../config"; 6 | 7 | export default class PackageJsonGenerator { 8 | private metaParams: MetaParams; 9 | private outputPackageJsonPath = path.join(outputDir, "package.json"); 10 | private packageJson: PackageJson; 11 | private packageJsonUrl = 12 | "https://raw.githubusercontent.com/n8n-io/n8n/master/packages/nodes-base/package.json"; 13 | 14 | constructor(metaParams: MetaParams) { 15 | this.metaParams = metaParams; 16 | } 17 | 18 | public async run() { 19 | this.packageJson = await this.getPackageJson(); 20 | this.checkIfNodeExists(); 21 | this.storePackageJson(); 22 | this.insertNodeInPackageJson(); 23 | } 24 | 25 | private async getPackageJson() { 26 | return await axios 27 | .get(this.packageJsonUrl) 28 | .then((response) => response.data); 29 | } 30 | 31 | private checkIfNodeExists() { 32 | const newNodeName = this.metaParams.serviceName.replace(/\s/, ""); 33 | 34 | this.packageJson.n8n.nodes.forEach((nodePath) => { 35 | const existingNodeName = this.getNameFromNodePath(nodePath); 36 | 37 | if (newNodeName === existingNodeName) { 38 | throw new Error( 39 | "The node you are trying to create already exists in the n8n repo" 40 | ); 41 | } 42 | }); 43 | } 44 | 45 | private getNameFromNodePath(nodePath: string) { 46 | return nodePath.split("/").slice(-1)[0].replace(".node.js", ""); 47 | } 48 | 49 | private storePackageJson() { 50 | fs.writeFileSync( 51 | this.outputPackageJsonPath, 52 | JSON.stringify(this.packageJson, null, 2) 53 | ); 54 | } 55 | 56 | private insertNodeInPackageJson() { 57 | const serviceName = this.metaParams.serviceName.replace(/\s/, ""); 58 | const args = `--serviceName ${serviceName} --nodeSpot ${this.findNodeSpot()}`; 59 | 60 | execSync(`env HYGEN_OVERWRITE=1 node ${hygen} insert packageJson ${args}`); 61 | } 62 | 63 | /** 64 | * Find the node after which the new node is to be inserted in `package.json`. 65 | * Comparison with dir and name: `Lichess/Lichess` vs. `Lemlist/Lemlist` 66 | */ 67 | private findNodeSpot() { 68 | const newDirAndName = `${this.metaParams.serviceName}/${this.metaParams.serviceName}`; 69 | 70 | for (const nodePath of this.packageJson.n8n.nodes) { 71 | const dirAndName = nodePath.match(/dist\/nodes\/(.*)\.node\.js/)![1]; 72 | if (dirAndName < newDirAndName) continue; 73 | return dirAndName; 74 | } 75 | 76 | throw new Error( 77 | "No node path insertion spot found in `package.json` retrieved from n8n repo" 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/services/Prompter.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import { promisify } from "util"; 4 | 5 | import inquirer from "inquirer"; 6 | 7 | import { inputDir, openApiInputDir, customInputDir } from "../config"; 8 | 9 | const readDir = promisify(fs.readdir); 10 | 11 | export default class Prompter { 12 | constructor() { 13 | this.validateDirs(); 14 | } 15 | 16 | private validateDirs() { 17 | [inputDir, customInputDir, openApiInputDir].forEach((dir) => { 18 | if (!fs.existsSync(dir)) fs.mkdirSync(dir); 19 | }); 20 | } 21 | 22 | public async askForSourceType() { 23 | const { sourceType } = await inquirer.prompt<{ 24 | sourceType: "OpenAPI spec in YAML or JSON" | "Custom API mapping in YAML"; 25 | }>([ 26 | { 27 | name: "sourceType", 28 | type: "list", 29 | message: "Select the source.", 30 | choices: ["OpenAPI spec in YAML or JSON", "Custom API mapping in YAML"], 31 | }, 32 | ]); 33 | 34 | return sourceType; 35 | } 36 | 37 | public async askForCustomYamlFile() { 38 | const { yamlFile } = await inquirer.prompt<{ yamlFile: string }>([ 39 | { 40 | name: "yamlFile", 41 | type: "list", 42 | message: "Select the custom API mapping in YAML.", 43 | choices: await this.getInputFiles("custom"), 44 | }, 45 | ]); 46 | 47 | return yamlFile; 48 | } 49 | 50 | public async askForOpenApiFile() { 51 | const { openApiFile } = await inquirer.prompt<{ openApiFile: string }>([ 52 | { 53 | name: "openApiFile", 54 | type: "list", 55 | message: "Select the OpenAPI file in JSON or YAML.", 56 | choices: await this.getInputFiles("openApi"), 57 | }, 58 | ]); 59 | 60 | return openApiFile; 61 | } 62 | 63 | private async getInputFiles(sourceType: "custom" | "openApi") { 64 | const files = await readDir(path.join(inputDir, sourceType)); 65 | return files.filter((file) => file !== ".gitkeep"); 66 | } 67 | 68 | public async askForPlacementTargetType() { 69 | const { placementTargetType } = await inquirer.prompt<{ 70 | placementTargetType: "clone" | "custom"; 71 | }>([ 72 | { 73 | name: "placementTargetType", 74 | type: "list", 75 | message: "Select the target dir", 76 | choices: ["clone", "custom"], 77 | }, 78 | ]); 79 | 80 | return placementTargetType; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/services/TemplateBuilder.ts: -------------------------------------------------------------------------------- 1 | import { camelCase } from "change-case"; 2 | import ApiCallBuilder from "./templating/ApiCallBuilder"; 3 | import DividerBuilder from "./templating/DividerBuilder"; 4 | import BranchBuilder from "./templating/BranchBuilder"; 5 | import ImportsBuilder from "./templating/ImportsBuilder"; 6 | import ResourceBuilder from "./templating/ResourceBuilder"; 7 | 8 | export class Builder { 9 | resourceTuples: ResourceTuples; 10 | resourceNames: string[]; 11 | apiRequest: string; 12 | 13 | apiCallBuilder: ApiCallBuilder; 14 | branchBuilder: BranchBuilder; 15 | dividerBuilder: DividerBuilder; 16 | importsBuilder: ImportsBuilder; 17 | resourceBuilder: ResourceBuilder; 18 | 19 | constructor(mainParams: MainParams, { serviceName }: MetaParams) { 20 | this.resourceTuples = Object.entries(mainParams); 21 | this.resourceNames = Object.keys(mainParams); 22 | this.apiRequest = camelCase(serviceName) + "ApiRequest"; 23 | 24 | this.apiCallBuilder = new ApiCallBuilder(this.apiRequest); 25 | this.branchBuilder = new BranchBuilder(mainParams); 26 | this.dividerBuilder = new DividerBuilder(); 27 | this.importsBuilder = new ImportsBuilder(this.apiRequest, mainParams); 28 | this.resourceBuilder = new ResourceBuilder(); 29 | } 30 | 31 | // ApiCallBuilder 32 | 33 | apiCall(operation: Operation) { 34 | return this.apiCallBuilder.run(operation); 35 | } 36 | 37 | // BranchBuilder 38 | 39 | resourceBranch(resourceName: string) { 40 | return this.branchBuilder.resourceBranch(resourceName); 41 | } 42 | 43 | operationBranch(resourceName: string, operation: Operation) { 44 | return this.branchBuilder.operationBranch(resourceName, operation); 45 | } 46 | 47 | resourceError( 48 | resourceName: string, 49 | options: { enabled: boolean } = { enabled: false } 50 | ) { 51 | return this.branchBuilder.resourceError(resourceName, options); 52 | } 53 | 54 | operationError( 55 | resourceName: string, 56 | operation: Operation, 57 | options: { enabled: boolean } = { enabled: false } 58 | ) { 59 | return this.branchBuilder.operationError(resourceName, operation, options); 60 | } 61 | 62 | // ImportsBuilder 63 | 64 | genericFunctionsImports() { 65 | return this.importsBuilder.genericFunctionsImports(); 66 | } 67 | 68 | // DividerBuilder 69 | 70 | resourceDivider(resourceName: string) { 71 | return this.dividerBuilder.resourceDivider(resourceName); 72 | } 73 | 74 | operationDivider( 75 | resourceName: string, 76 | operationId: string, 77 | operationUrl: string 78 | ) { 79 | return this.dividerBuilder.operationDivider( 80 | resourceName, 81 | operationId, 82 | operationUrl 83 | ); 84 | } 85 | 86 | resourceDescriptionDivider(resourceName: string, operationId: string) { 87 | return this.dividerBuilder.resourceDescriptionDivider( 88 | resourceName, 89 | operationId 90 | ); 91 | } 92 | 93 | // ResourceBuilder 94 | 95 | operationsOptions(operations: Operation[]) { 96 | return this.resourceBuilder.operationsOptions(operations); 97 | } 98 | 99 | getAllAdditions(resourceName: string, operationId: string) { 100 | return this.resourceBuilder.getAllAdditions(resourceName, operationId); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/services/TemplateHelper.ts: -------------------------------------------------------------------------------- 1 | import { camelCase, capitalCase, pascalCase } from "change-case"; 2 | import { titleCase } from "title-case"; 3 | 4 | export class Helper { 5 | adjustType = (type: string, name: string) => { 6 | if (type === "integer") return "number"; 7 | if (name.toLowerCase().includes("date")) return "dateTime"; 8 | return type; 9 | }; 10 | 11 | camelCase = (str: string) => camelCase(str); 12 | 13 | capitalCase = (str: string) => capitalCase(str); 14 | 15 | escape = (str: string) => str.replace(/(\r)?\n/g, "
").replace(/'/g, "’"); 16 | 17 | getCredentialsString = (name: string, auth: AuthType) => 18 | this.camelCase(name) + (auth === "OAuth2" ? "OAuth2" : "") + "Api"; 19 | 20 | getDefault(arg: any) { 21 | if (arg.default) { 22 | if (arg.type === "boolean" || arg.type === "number") return arg.default; 23 | 24 | if (arg.type === "string" || arg.type === "options") 25 | return `'${arg.default}'`; 26 | 27 | // edge case: number type with string default (third-party error) 28 | if ( 29 | typeof arg.default === "string" && 30 | (arg.type === "number" || arg.type === "integer") 31 | ) { 32 | return 0; 33 | } 34 | } 35 | 36 | if ( 37 | arg.type === "string" || 38 | arg.type === "dateTime" || 39 | arg.type === "loadOptions" 40 | ) 41 | return "''"; 42 | if (arg.type === "number" || arg.type === "integer") return 0; 43 | if (arg.type === "boolean") return false; 44 | if (arg.type === "options") return `'${arg.options[0]}'`; 45 | 46 | return "''"; 47 | } 48 | 49 | getParams = (params: OperationParameter[], type: "query" | "path") => 50 | params.filter((p) => p.in === type).map((p) => p.name); 51 | 52 | hasMinMax = (arg: any) => arg.minimum && arg.maximum; 53 | 54 | pascalCase = (str: string) => pascalCase(str); 55 | 56 | titleCase = (str: string) => { 57 | const base = str.replace(/[._]/g, " ").trim(); 58 | 59 | return str.includes("_") 60 | ? titleCase(base).replace("Id", "ID") // for snake case 61 | : titleCase(base.replace("Id", " ID")); // for camel case 62 | }; 63 | 64 | toTemplateLiteral = (endpoint: string) => endpoint.replace(/{/g, "${"); 65 | 66 | getPlaceholder = (property: string) => { 67 | if (property === "Filters") return "Add Filter"; 68 | return "Add Field"; 69 | }; 70 | 71 | addFieldsSuffix = (key: string) => 72 | key.split("").includes("_") ? key + "_fields" : key + "Fields"; 73 | } 74 | -------------------------------------------------------------------------------- /src/services/templating/ApiCallBuilder.ts: -------------------------------------------------------------------------------- 1 | import { camelCase } from "change-case"; 2 | 3 | const LONG_ENDPOINT_CHARACTER_LENGTH = 20; 4 | 5 | export default class ApiCallBuilder { 6 | serviceApiRequest: string; 7 | lines: string[]; 8 | hasPathParam = false; 9 | hasQueryString = false; 10 | hasRequestBody = false; 11 | hasStandardRequestBody = false; 12 | isGetAll = false; 13 | hasLongEndpoint = false; 14 | 15 | constructor(serviceApiRequest: string) { 16 | this.serviceApiRequest = serviceApiRequest; 17 | return this; 18 | } 19 | 20 | run({ 21 | operationId, 22 | parameters, 23 | additionalFields, 24 | requestBody, 25 | requestMethod, 26 | endpoint, 27 | }: Operation) { 28 | this.resetState(); 29 | 30 | this.isGetAll = operationId === "getAll"; 31 | this.hasLongEndpoint = endpoint.length > LONG_ENDPOINT_CHARACTER_LENGTH; 32 | 33 | const pathParams = parameters?.filter(this.isPathParam); 34 | const qsParams = parameters?.filter(this.isQsParam); 35 | 36 | if (requestBody?.length) { 37 | this.hasRequestBody = true; 38 | this.requestBody(requestBody); 39 | } 40 | 41 | if (pathParams?.length) { 42 | this.hasPathParam = true; 43 | pathParams.forEach((p) => this.pathParam(p)); 44 | this.addNewLine(this.lines); 45 | } 46 | 47 | if (qsParams?.length) { 48 | this.hasQueryString = true; 49 | this.qs(qsParams); 50 | } 51 | 52 | if (additionalFields) { 53 | const qsOptions = additionalFields.options.filter(this.isQsParam); 54 | 55 | if (!this.hasQueryString) { 56 | this.lines.push("const qs = {} as IDataObject;"); 57 | } 58 | 59 | if (qsOptions.length) { 60 | this.hasQueryString = true; 61 | this.additionalFields("qs"); 62 | } 63 | } 64 | 65 | if (this.hasLongEndpoint) this.endpoint(endpoint); 66 | 67 | this.lines.push(this.callLine(requestMethod, endpoint)); 68 | 69 | return this.indentLines(); 70 | } 71 | 72 | resetState() { 73 | this.lines = []; 74 | this.hasPathParam = false; 75 | this.hasQueryString = false; 76 | this.hasRequestBody = false; 77 | this.hasStandardRequestBody = false; 78 | this.isGetAll = false; 79 | this.hasLongEndpoint = false; 80 | } 81 | 82 | indentLines() { 83 | return this.lines 84 | .map((line, index) => { 85 | if (index === 0) return line; 86 | return "\t".repeat(5) + line; 87 | }) 88 | .join("\n"); 89 | } 90 | 91 | // ------------------ path and qs parameters ------------------ 92 | 93 | pathParam({ name }: OperationParameter) { 94 | this.lines.push(`const ${name} = this.getNodeParameter('${name}', i);`); 95 | } 96 | 97 | qs(qsParams: OperationParameter[]) { 98 | const [requiredQsParams, extraQsParams] = this.partitionByRequired( 99 | qsParams 100 | ); 101 | 102 | // TODO: This is only for filters. Add variants for other extra fields. 103 | if (extraQsParams.length) { 104 | this.lines.push( 105 | "const qs = {} as IDataObject;", 106 | "const filters = this.getNodeParameter('filters', i) as IDataObject;\n", 107 | "if (Object.keys(filters).length) {", 108 | `\tObject.assign(qs, filters);`, 109 | "}\n" 110 | ); 111 | } 112 | 113 | if (requiredQsParams.length) { 114 | this.lines.push("const qs: IDataObject = {"); 115 | this.lines.push( 116 | ...requiredQsParams.map( 117 | (rqsp) => `\t${rqsp.name}: this.getNodeParameter('${rqsp.name}', i),` 118 | ) 119 | ); 120 | this.lines.push("};"); 121 | this.addNewLine(this.lines); 122 | } 123 | } 124 | 125 | // TODO: No longer used? 126 | // qsParam({ name }: OperationParameter) { 127 | // this.lines.push(`qs.${name} = this.getNodeParameter('${name}', i);`); 128 | // } 129 | 130 | isPathParam = (param: OperationParameter) => param.in === "path"; 131 | 132 | isQsParam = (param: OperationParameter) => param.in === "query"; 133 | 134 | // ------------------ request body ------------------ 135 | 136 | requestBody(rbArray: OperationRequestBody[]) { 137 | rbArray.forEach((rbItem) => { 138 | if (rbItem.name === "Standard") { 139 | this.hasStandardRequestBody = true; 140 | 141 | const rbItemNames = this.getRequestBodyItemNames(rbItem); 142 | 143 | if (rbItemNames === "text/plain") { 144 | this.lines.push( 145 | `const body = this.getNodeParameter('${rbItem.textPlainProperty}', i) as string;` 146 | ); 147 | return; 148 | } 149 | 150 | if (!rbItemNames) return; 151 | 152 | this.lines.push("const body = {"); 153 | 154 | this.lines.push( 155 | ...rbItemNames.map( 156 | (rbc) => `\t${rbc}: this.getNodeParameter('${camelCase(rbc)}', i),` 157 | ) 158 | ); 159 | 160 | this.lines.push("} as IDataObject;"); 161 | this.addNewLine(this.lines); 162 | } else if ( 163 | rbItem.name === "Additional Fields" || 164 | rbItem.name === "Filters" || 165 | rbItem.name === "Update Fields" 166 | ) { 167 | if (!this.hasStandardRequestBody) { 168 | this.lines.push("const body = {} as IDataObject;"); 169 | } 170 | 171 | const rbItemName = camelCase(rbItem.name); 172 | 173 | const rbItemNames = this.getRequestBodyItemNames(rbItem); 174 | 175 | if (!rbItemNames) return; 176 | 177 | this.lines.push( 178 | `const ${rbItemName} = this.getNodeParameter('${rbItemName}', i) as IDataObject;` 179 | ); 180 | this.addNewLine(this.lines); 181 | 182 | this.lines.push(`if (Object.keys(${rbItemName}).length) {`); 183 | this.lines.push(`\tObject.assign(body, ${rbItemName});`); 184 | this.lines.push("}"); 185 | 186 | this.addNewLine(this.lines); 187 | } 188 | }); 189 | } 190 | 191 | addNewLine = (array: string[]) => (array[array.length - 1] += "\n"); 192 | 193 | getRequestBodyItemNames(requestBodyItem: OperationRequestBody) { 194 | const formUrlEncoded = 195 | requestBodyItem.content["application/x-www-form-urlencoded"]; 196 | 197 | if (formUrlEncoded) { 198 | return Object.keys(formUrlEncoded.schema.properties); 199 | } 200 | 201 | const textPlainContent = requestBodyItem.content["text/plain"]; 202 | 203 | if (textPlainContent) return "text/plain"; 204 | 205 | return null; 206 | } 207 | 208 | // ------------------ additional fields ------------------- 209 | 210 | additionalFields(target: "body" | "qs") { 211 | this.lines.push( 212 | `const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;\n`, 213 | "if (Object.keys(additionalFields).length) {", 214 | `\tObject.assign(${target}, additionalFields);`, 215 | "}" 216 | ); 217 | 218 | this.addNewLine(this.lines); 219 | } 220 | 221 | adjustType = (type: string) => (type === "integer" ? "number" : type); 222 | 223 | // ------------------ endpoint ------------------ 224 | 225 | endpoint(endpoint: string) { 226 | this.lines.push( 227 | this.hasPathParam 228 | ? this.pathParamEndpoint(endpoint) 229 | : this.ordinaryEndpoint(endpoint) 230 | ); 231 | } 232 | 233 | pathParamEndpoint = (endpoint: string) => 234 | `const endpoint = \`${this.toTemplateLiteral(endpoint)}\`;`; 235 | 236 | ordinaryEndpoint = (endpoint: string) => `const endpoint = '${endpoint}';`; 237 | 238 | toTemplateLiteral = (endpoint: string) => endpoint.replace(/{/g, "${"); 239 | 240 | // ------------------ call ------------------------ 241 | 242 | callLine(requestMethod: string, endpoint = "") { 243 | const hasBracket = endpoint.split("").includes("}"); 244 | 245 | const endpointInsert = this.hasLongEndpoint 246 | ? "endpoint" 247 | : hasBracket 248 | ? `\`${this.toTemplateLiteral(endpoint)}\`` 249 | : `'${endpoint}'`; 250 | 251 | let call = this.isGetAll 252 | ? `responseData = await handleListing.call(this, '${requestMethod}', ${endpointInsert}` 253 | : `responseData = await ${this.serviceApiRequest}.call(this, '${requestMethod}', ${endpointInsert}`; 254 | 255 | if (this.hasRequestBody && this.hasQueryString) { 256 | call += ", body, qs);"; 257 | } else if (this.hasRequestBody && !this.hasQueryString) { 258 | call += ", body);"; 259 | } else if (!this.hasRequestBody && this.hasQueryString) { 260 | call += ", {}, qs);"; 261 | } else if (!this.hasRequestBody && !this.hasQueryString) { 262 | call += ");"; 263 | } 264 | 265 | return call; 266 | } 267 | 268 | // ------------------ utils ------------------------ 269 | 270 | private partition = (test: (op: OperationParameter) => boolean) => ( 271 | array: OperationParameter[] 272 | ) => { 273 | const pass: OperationParameter[] = [], 274 | fail: OperationParameter[] = []; 275 | array.forEach((item) => (test(item) ? pass : fail).push(item)); 276 | 277 | return [pass, fail]; 278 | }; 279 | 280 | private partitionByRequired = this.partition( 281 | (op: OperationParameter) => op.required ?? false 282 | ); 283 | } 284 | -------------------------------------------------------------------------------- /src/services/templating/BranchBuilder.ts: -------------------------------------------------------------------------------- 1 | import { camelCase } from "change-case"; 2 | 3 | export default class BranchBuilder { 4 | mainParams: MainParams; 5 | resourceTuples: ResourceTuples; 6 | resourceNames: string[]; 7 | 8 | isFirstResource = true; 9 | 10 | constructor(mainParams: MainParams) { 11 | this.mainParams = mainParams; 12 | this.resourceTuples = Object.entries(this.mainParams); 13 | this.resourceNames = this.resourceTuples.map((tuple) => tuple[0]); 14 | return this; 15 | } 16 | 17 | isFirst = (item: T, array: T[]) => array.indexOf(item) === 0; 18 | 19 | isLast = (item: T, array: T[]) => array.indexOf(item) + 1 === array.length; 20 | 21 | resourceBranch(resourceName: string) { 22 | const branch = `if (resource === '${camelCase(resourceName)}') {`; 23 | 24 | if (this.isFirstResource) { 25 | this.isFirstResource = false; 26 | return branch; 27 | } 28 | 29 | return "} else " + branch; 30 | } 31 | 32 | operationBranch(resourceName: string, operation: Operation) { 33 | const branch = `if (operation === '${camelCase(operation.operationId)}') {`; 34 | const prefix = "} else "; 35 | const isFirst = this.isFirst(operation, this.mainParams[resourceName]); 36 | 37 | return isFirst ? branch : prefix + branch; 38 | } 39 | 40 | resourceError(resourceName: string, { enabled }: { enabled: boolean }) { 41 | const isLast = this.isLast(resourceName, this.resourceNames); 42 | 43 | if (isLast && !enabled) return "\t}\n\n\t\t\t}"; // close operation and resource 44 | if (!enabled) return "\t}\n"; // close operation 45 | 46 | const resourceError = ` 47 | \t} else { 48 | \t\tthrow new Error(\`Unknown resource: \${resource}\`); 49 | \t}`; 50 | 51 | return isLast ? resourceError : null; 52 | } 53 | 54 | operationError( 55 | resourceName: string, 56 | operation: Operation, 57 | { enabled }: { enabled: boolean } 58 | ) { 59 | // if (!enabled) return "\t}\n"; 60 | if (!enabled) return null; 61 | 62 | const isLast = this.isLast(operation, this.mainParams[resourceName]); 63 | const operationError = ` 64 | \t\t} else { 65 | \t\t\tthrow new Error(\`Unknown operation: \${operation}\`); 66 | \t\t}`; 67 | 68 | return isLast ? operationError : null; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/services/templating/DividerBuilder.ts: -------------------------------------------------------------------------------- 1 | export default class DividerBuilder { 2 | isFirstOperation = true; 3 | 4 | /** Build a comment divider for a resource: 5 | * ``` 6 | * // ********************************************************************** 7 | * // user 8 | * // ********************************************************************** 9 | * ```*/ 10 | resourceDivider(resourceName: string) { 11 | const RESOURCE_DIVIDER_LENGTH = 70; 12 | 13 | const padLength = Math.floor( 14 | (RESOURCE_DIVIDER_LENGTH - resourceName.length) / 2 15 | ); 16 | 17 | const titleLine = "// " + " ".repeat(padLength) + resourceName; 18 | const dividerLine = "// " + "*".repeat(RESOURCE_DIVIDER_LENGTH); 19 | 20 | return [dividerLine, titleLine, dividerLine].join("\n" + "\t".repeat(4)); 21 | } 22 | 23 | /** Build a comment divider for an operation: 24 | * ``` 25 | * // ---------------------------------------- 26 | * // user: getUser 27 | * // ---------------------------------------- 28 | * 29 | * // https://api.service.com/api-docs-section 30 | * ```*/ 31 | operationDivider( 32 | resourceName: string, 33 | operationId: string, 34 | operationUrl: string 35 | ) { 36 | const operationDividerLines = this.dividerLines(resourceName, operationId); 37 | 38 | if (operationUrl) { 39 | operationDividerLines.push("\n" + "\t".repeat(5) + "// " + operationUrl); 40 | } 41 | 42 | return operationDividerLines.join("\n" + "\t".repeat(5)); 43 | } 44 | 45 | resourceDescriptionDivider(resourceName: string, operationId: string) { 46 | const divider = this.dividerLines( 47 | resourceName.charAt(0).toLowerCase() + resourceName.slice(1), 48 | operationId 49 | ).join("\n\t"); 50 | 51 | if (this.isFirstOperation) { 52 | this.isFirstOperation = false; 53 | return divider; 54 | } 55 | 56 | return "\n\t" + divider; 57 | } 58 | 59 | dividerLines(resourceName: string, operationId: string) { 60 | const OPERATION_DIVIDER_LENGTH = 40; 61 | 62 | const title = `${resourceName}: ${operationId}`; 63 | const padLengthCandidate = Math.floor( 64 | (OPERATION_DIVIDER_LENGTH - title.length) / 2 65 | ); 66 | const padLength = padLengthCandidate > 0 ? padLengthCandidate : 0; 67 | 68 | const titleLine = "// " + " ".repeat(padLength) + title; 69 | const dividerLine = "// " + "-".repeat(OPERATION_DIVIDER_LENGTH); 70 | 71 | return [dividerLine, titleLine, dividerLine]; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/services/templating/ImportsBuilder.ts: -------------------------------------------------------------------------------- 1 | export default class ImportsBuilder { 2 | serviceApiRequest: string; 3 | hasGetAll = false; 4 | 5 | constructor(serviceApiRequest: string, mainParams: MainParams) { 6 | this.serviceApiRequest = serviceApiRequest; 7 | this.checkIfGetAllExists(mainParams); 8 | } 9 | 10 | checkIfGetAllExists(mainParams: MainParams) { 11 | Object.values(mainParams).forEach((operationsBundle) => { 12 | for (const operation of operationsBundle) { 13 | if (operation.operationId === "getAll") { 14 | this.hasGetAll = true; 15 | break; 16 | } 17 | } 18 | }); 19 | } 20 | 21 | genericFunctionsImports() { 22 | return this.hasGetAll 23 | ? [this.serviceApiRequest, "handleListing"] 24 | .sort() 25 | .map((imported) => "\t" + imported + ",") 26 | .join("\n") 27 | : "\t" + this.serviceApiRequest + ","; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/services/templating/ResourceBuilder.ts: -------------------------------------------------------------------------------- 1 | import { camelCase, capitalCase } from "change-case"; 2 | 3 | export default class ResourceBuilder { 4 | lines: string[] = []; 5 | 6 | public operationsOptions(operations: Operation[]) { 7 | operations.sort((a, b) => a.operationId.localeCompare(b.operationId)); 8 | 9 | operations.forEach(({ operationId, description }, index) => { 10 | this.createLine("{", { tabs: !index ? 0 : 3 }); 11 | 12 | this.createLine(`name: '${capitalCase(operationId)}',`, { tabs: 4 }); 13 | this.createLine(`value: '${camelCase(operationId)}',`, { tabs: 4 }); 14 | if (description) { 15 | this.createLine(`description: '${description}',`, { tabs: 4 }); 16 | } 17 | 18 | this.createLine("},", { tabs: 3 }); 19 | }); 20 | 21 | return this.lines.join("\n"); 22 | } 23 | 24 | private createLine(line: string, { tabs } = { tabs: 0 }) { 25 | if (!tabs) { 26 | this.lines.push(line); 27 | return; 28 | } 29 | 30 | this.lines.push("\t".repeat(tabs) + line); 31 | } 32 | 33 | public getAllAdditions(resourceName: string, operationId: string) { 34 | return [ 35 | this.returnAll(resourceName, operationId), 36 | this.limit(resourceName, operationId), 37 | ].join("\n\t"); 38 | } 39 | 40 | private returnAll(resourceName: string, operationId: string) { 41 | const returnAll = ` 42 | { 43 | displayName: 'Return All', 44 | name: 'returnAll', 45 | type: 'boolean', 46 | default: false, 47 | description: 'Whether to return all results or only up to a given limit', 48 | displayOptions: { 49 | show: { 50 | resource: [ 51 | '${resourceName}', 52 | ], 53 | operation: [ 54 | '${operationId}', 55 | ], 56 | }, 57 | }, 58 | }, 59 | `; 60 | 61 | return this.adjustCodeToTemplate(returnAll); 62 | } 63 | 64 | private limit(resourceName: string, operationId: string) { 65 | const limit = ` 66 | { 67 | displayName: 'Limit', 68 | name: 'limit', 69 | type: 'number', 70 | default: 50, 71 | description: 'Max number of results to return', 72 | typeOptions: { 73 | minValue: 1, 74 | }, 75 | displayOptions: { 76 | show: { 77 | resource: [ 78 | '${resourceName}', 79 | ], 80 | operation: [ 81 | '${operationId}', 82 | ], 83 | returnAll: [ 84 | false, 85 | ], 86 | }, 87 | }, 88 | }, 89 | `; 90 | 91 | return this.adjustCodeToTemplate(limit); 92 | } 93 | 94 | private adjustCodeToTemplate(property: string) { 95 | return property 96 | .trimLeft() 97 | .replace(/^[ ]{2}/gm, "") 98 | .replace(/[ ]{2}/gm, "\t") 99 | .trimRight(); 100 | } 101 | 102 | private hasMinMax = (arg: any) => arg.minimum && arg.maximum; 103 | } 104 | -------------------------------------------------------------------------------- /src/types/augmentations.d.ts: -------------------------------------------------------------------------------- 1 | // ---------------------------------- 2 | // Module augmentations 3 | // ---------------------------------- 4 | 5 | declare module "object-treeify" { 6 | export default function treeify(jsObject: Object): string; 7 | } 8 | 9 | declare module "js-yaml" { 10 | export function load(json: string): JsonObject; 11 | } 12 | -------------------------------------------------------------------------------- /src/types/params.d.ts: -------------------------------------------------------------------------------- 1 | // ---------------------------------- 2 | // custom spec params 3 | // ---------------------------------- 4 | 5 | type CustomSpecParams = { 6 | metaParams: MetaParams; 7 | mainParams: { 8 | [resource: string]: CustomSpecOperation[]; 9 | }; 10 | }; 11 | 12 | type CustomSpecOperation = { 13 | endpoint: string; 14 | operationId: string; 15 | requestMethod: string; 16 | operationUrl?: string; 17 | requiredFields?: CustomSpecFields; 18 | additionalFields?: CustomSpecFields; 19 | filters?: CustomSpecFields; 20 | updateFields?: CustomSpecFields; 21 | }; 22 | 23 | type CustomSpecFields = { 24 | queryString?: CustomSpecFieldContent; 25 | requestBody?: CustomSpecFieldContent; 26 | }; 27 | 28 | type CustomSpecFieldContent = { [name: string]: ParamContent }; 29 | 30 | // ---------------------------------- 31 | // nodegen params 32 | // ---------------------------------- 33 | 34 | /** 35 | * Output of `OpenApiStager` and of `YamlStager`. 36 | */ 37 | type NodegenParams = { 38 | metaParams: MetaParams; 39 | mainParams: MainParams; 40 | }; 41 | 42 | type MetaParams = { 43 | serviceName: string; 44 | authType: AuthType; 45 | nodeColor: string; 46 | apiUrl: string; 47 | }; 48 | 49 | type AuthType = "None" | "ApiKey" | "OAuth2"; 50 | 51 | type MainParams = { 52 | [resource: string]: Operation[]; 53 | }; 54 | 55 | type Operation = { 56 | [key: string]: string | OperationParameter[]; 57 | endpoint: string; 58 | operationId: string; 59 | requestMethod: string; 60 | operationUrl?: string; 61 | description?: string; 62 | parameters?: OperationParameter[]; 63 | requestBody?: OperationRequestBody[]; 64 | } & TemplatingFields; 65 | 66 | type TemplatingFields = { 67 | additionalFields?: AdditionalFields; 68 | filters?: AdditionalFields; 69 | updateFields?: AdditionalFields; 70 | }; 71 | 72 | type OperationParameter = { 73 | in: "path" | "query" | "header"; 74 | name: string; 75 | description?: string; 76 | schema: PathQuerySchema; 77 | required?: boolean; 78 | example?: string; 79 | $ref?: string; // from OpenAPI 80 | }; 81 | 82 | type PathQuerySchema = { 83 | type: string; 84 | default: boolean | string | number; 85 | example?: string | number; 86 | minimum?: number; 87 | maximum?: number; 88 | options?: string[]; // from custom spec in YAML 89 | }; 90 | 91 | type OperationRequestBody = { 92 | content: { 93 | "application/x-www-form-urlencoded"?: { schema: RequestBodySchema }; 94 | "text/plain"?: { schema: RequestBodySchema }; 95 | }; 96 | name?: "Standard" | "Additional Fields" | "Filters" | "Update Fields"; 97 | description?: string; 98 | required?: boolean; 99 | textPlainProperty?: string; 100 | }; 101 | 102 | type RequestBodySchema = { 103 | type: string; 104 | required?: string[]; 105 | properties: { 106 | [propertyName: string]: ParamContent; 107 | }; 108 | }; 109 | 110 | type ParamContent = { 111 | type: 112 | | "string" 113 | | "number" 114 | | "boolean" 115 | | "options" 116 | | "collection" 117 | | "fixedCollection"; 118 | default?: any; // TODO: Type properly 119 | description?: string; 120 | options?: string[]; 121 | }; 122 | 123 | type AdditionalFields = { 124 | name: "Additional Fields" | "Filters" | "Update Fields"; 125 | type: "collection"; 126 | description: ""; 127 | default: {}; 128 | options: { 129 | in: "path" | "query" | "header"; 130 | name: string; 131 | schema: { 132 | type: string; 133 | default: string | boolean | number; 134 | }; 135 | description?: string; 136 | }[]; 137 | }; 138 | 139 | type ExtraFieldName = "Additional Fields" | "Filters" | "Update Fields"; 140 | 141 | /** 142 | * Utility type for template builder. 143 | */ 144 | type ResourceTuples = [string, Operation[]][]; 145 | 146 | // ---------------------------------- 147 | // OpenAPI keys 148 | // ---------------------------------- 149 | 150 | type OpenApiKey = StringArrayKey | StringKey | CustomObjectKey; 151 | 152 | type StringArrayKey = "tags" | "requestMethods"; 153 | 154 | type StringKey = "description" | "operationId"; 155 | 156 | type CustomObjectKey = "parameters" | "requestBody"; 157 | -------------------------------------------------------------------------------- /src/types/printer.d.ts: -------------------------------------------------------------------------------- 1 | // ---------------------------------- 2 | // Printer 3 | // ---------------------------------- 4 | 5 | type ApiMap = { 6 | [key: string]: ApiMapOperation[]; 7 | }; 8 | 9 | type ApiMapOperation = { 10 | ATTENTION?: string; 11 | nodeOperation: string; 12 | requestMethod; 13 | endpoint: string; 14 | IRREGULAR?: string; 15 | }; 16 | 17 | type TreeView = string; 18 | -------------------------------------------------------------------------------- /src/types/utils.d.ts: -------------------------------------------------------------------------------- 1 | // ---------------------------------- 2 | // JSON 3 | // ---------------------------------- 4 | 5 | type JsonValue = 6 | | string 7 | | number 8 | | boolean 9 | | null 10 | | Array 11 | | JsonObject; 12 | 13 | type JsonObject = { [key: string]: JsonValue }; 14 | 15 | // ---------------------------------- 16 | // n8n package.json 17 | // ---------------------------------- 18 | 19 | type PackageJson = { 20 | n8n: { 21 | nodes: string[]; 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /src/utils/FilePrinter.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from "child_process"; 2 | import fs from "fs"; 3 | import path from "path"; 4 | 5 | import { Project } from "ts-morph"; 6 | 7 | import { inputDir } from "../config"; 8 | 9 | /**Converts nodegen params into TypeScript or JSON, or treeview into TXT.*/ 10 | export default class FilePrinter { 11 | private readonly nodegenPath = path.join(inputDir, "_nodegenParams"); 12 | private readonly treeviewPath = path.join(inputDir, "_treeview"); 13 | private readonly apiMapPath = path.join(inputDir, "_apiMap"); 14 | 15 | constructor(private readonly printable: NodegenParams | TreeView | ApiMap) {} 16 | 17 | public print({ format }: { format: "json" | "ts" | "txt" }) { 18 | if (format === "json") this.printJson(); 19 | if (format === "ts") this.printTypeScript(); 20 | if (format === "txt") this.printTxt(); 21 | } 22 | 23 | private getJson() { 24 | return JSON.stringify(this.printable, null, 2); 25 | } 26 | 27 | private printJson() { 28 | if (isNodegenParams(this.printable)) { 29 | fs.writeFileSync(this.nodegenPath + ".json", this.getJson(), "utf8"); 30 | } else { 31 | fs.writeFileSync(this.apiMapPath + ".json", this.getJson(), "utf8"); 32 | } 33 | } 34 | 35 | private printTypeScript() { 36 | new Project() 37 | .createSourceFile( 38 | this.nodegenPath + ".ts", 39 | `export default ${this.getJson()}`, 40 | { overwrite: true } 41 | ) 42 | .saveSync(); 43 | 44 | execSync(`npx prettier --write ${this.nodegenPath + ".ts"}`); 45 | } 46 | 47 | private printTxt() { 48 | if (typeof this.printable !== "string") { 49 | throw new Error("This method is only allowed for printing a treeview."); 50 | } 51 | fs.writeFileSync(this.treeviewPath + ".txt", this.printable, "utf8"); 52 | } 53 | } 54 | 55 | function isNodegenParams(value: any): value is NodegenParams { 56 | return "metaParams" in value && "mainParams" in value; 57 | } 58 | 59 | export function printTranslation( 60 | yamlMainParams: CustomSpecParams["mainParams"] 61 | ) { 62 | console.log(yamlMainParams); 63 | fs.writeFileSync( 64 | "translation.json", 65 | JSON.stringify(yamlMainParams, null, 2), 66 | "utf8" 67 | ); 68 | } 69 | 70 | export function printStagedParams(mainParams: MainParams) { 71 | fs.writeFileSync( 72 | "stagedParams.json", 73 | JSON.stringify(mainParams, null, 2), 74 | "utf8" 75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /src/utils/TreeRenderer.ts: -------------------------------------------------------------------------------- 1 | import treeify from "object-treeify"; 2 | import { JSONPath as jsonQuery } from "jsonpath-plus"; 3 | 4 | export default class TreeRenderer { 5 | constructor( 6 | private readonly viewable: MainParams, 7 | private readonly json: any 8 | ) {} 9 | 10 | public run() { 11 | this.prepareTreeView(); 12 | return this.generateTreeView(); 13 | } 14 | 15 | /**Hide wordy descriptions and convert arrays into objects.*/ 16 | private prepareTreeView() { 17 | for (const [resource, operations] of Object.entries(this.viewable)) { 18 | operations.forEach((op) => { 19 | this.hideProperty(op, "description"); 20 | if (op.parameters) this.objectifyProperty(op, "parameters"); 21 | if (op.requestBody) this.objectifyProperty(op, "requestBody"); 22 | }); 23 | 24 | this.objectifyOperations(operations, resource); 25 | } 26 | } 27 | 28 | private hideProperty(object: Operation, property: string) { 29 | Object.defineProperty(object, property, { enumerable: false }); 30 | } 31 | 32 | /**Convert the array in `parameters` or `requestBody` into an object with numbered keys.*/ 33 | private objectifyProperty(operation: Operation, property: string) { 34 | operation[property] = Object.assign({}, operation[property]); 35 | } 36 | 37 | /**Convert the array of operations into an object with numbered keys.*/ 38 | private objectifyOperations(operations: Operation[], resource: string) { 39 | this.viewable[resource] = Object.assign({}, operations); 40 | } 41 | 42 | private generateTreeView() { 43 | return this.getTitle() + "\n" + treeify(this.viewable); 44 | } 45 | 46 | private getTitle() { 47 | return jsonQuery({ json: this.json, path: `$.servers.*.url` }); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "target": "es5", 5 | "lib": [ 6 | "es2019" 7 | ], 8 | "module": "commonjs", 9 | "resolveJsonModule": true, 10 | "outDir": "./dist", 11 | "esModuleInterop": true, 12 | "skipLibCheck": true, 13 | "strictPropertyInitialization": false, 14 | "downlevelIteration": true, 15 | }, 16 | "exclude": [ 17 | "src/input", 18 | "src/output" 19 | ] 20 | } 21 | --------------------------------------------------------------------------------