├── .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 |
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 |
--------------------------------------------------------------------------------