├── .github ├── FUNDING.yml └── workflows │ └── publish.yaml ├── .gitignore ├── .nvmrc ├── .pre-commit-config.yaml ├── LICENSE ├── README.md ├── jest.config.js ├── n8n.png ├── node.png ├── openapi.png ├── package-lock.json ├── package.json ├── src ├── N8NPropertiesBuilder.spec.ts ├── N8NPropertiesBuilder.ts ├── OperationParser.ts ├── OperationsCollector.ts ├── ResourceCollector.ts ├── ResourceParser.ts ├── index.ts ├── n8n │ ├── OptionsByResourceMap.ts │ ├── SchemaToINodeProperties.ts │ └── utils.ts └── openapi │ ├── OpenAPIVisitor.ts │ ├── OpenAPIWalker.ts │ ├── RefResolver.ts │ └── SchemaExample.ts ├── tests ├── petstore.spec.ts ├── samples │ ├── api.github.com.2022-11-28.json │ ├── chatwoot.json │ ├── gitlab.json │ ├── hcloud.json │ ├── payhawk.com.json │ ├── petstore.json │ └── waha.json └── smoke.spec.ts └── tsconfig.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: devlikeapro 5 | open_collective: # doks Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: [ ] 13 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish Package to npmjs 2 | on: 3 | release: 4 | types: [ published ] 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | permissions: 9 | contents: read 10 | id-token: write 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version: '22.x' 16 | registry-url: 'https://registry.npmjs.org' 17 | - run: npm ci 18 | - run: npm run test 19 | - run: npm run build 20 | - run: npm publish --provenance --access public 21 | env: 22 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | dist 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | *.lcov 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # Snowpack dependency directory (https://snowpack.dev/) 47 | web_modules/ 48 | 49 | # TypeScript cache 50 | *.tsbuildinfo 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Optional stylelint cache 59 | .stylelintcache 60 | 61 | # Microbundle cache 62 | .rpt2_cache/ 63 | .rts2_cache_cjs/ 64 | .rts2_cache_es/ 65 | .rts2_cache_umd/ 66 | 67 | # Optional REPL history 68 | .node_repl_history 69 | 70 | # Output of 'npm pack' 71 | *.tgz 72 | 73 | # Yarn Integrity file 74 | .yarn-integrity 75 | 76 | # dotenv environment variable files 77 | .env 78 | .env.development.local 79 | .env.test.local 80 | .env.production.local 81 | .env.local 82 | 83 | # parcel-bundler cache (https://parceljs.org/) 84 | .cache 85 | .parcel-cache 86 | 87 | # Next.js build output 88 | .next 89 | out 90 | 91 | # Nuxt.js build / generate output 92 | .nuxt 93 | dist 94 | 95 | # Gatsby files 96 | .cache/ 97 | # Comment in the public line in if your project uses Gatsby and not Next.js 98 | # https://nextjs.org/blog/next-9-1#public-directory-support 99 | # public 100 | 101 | # vuepress build output 102 | .vuepress/dist 103 | 104 | # vuepress v2.x temp and cache directory 105 | .temp 106 | .cache 107 | 108 | # Docusaurus cache and generated files 109 | .docusaurus 110 | 111 | # Serverless directories 112 | .serverless/ 113 | 114 | # FuseBox cache 115 | .fusebox/ 116 | 117 | # DynamoDB Local files 118 | .dynamodb/ 119 | 120 | # TernJS port file 121 | .tern-port 122 | 123 | # Stores VSCode versions used for testing VSCode extensions 124 | .vscode-test 125 | 126 | # yarn v2 127 | .yarn/cache 128 | .yarn/unplugged 129 | .yarn/build-state.yml 130 | .yarn/install-state.gz 131 | .pnp.* 132 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.17 2 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/Lucas-C/pre-commit-hooks-nodejs 3 | rev: v1.1.2 4 | hooks: 5 | - id: markdown-toc 6 | stages: [ pre-commit ] 7 | files: 8 | (?x)^( 9 | README.md$| 10 | )$ 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 devlikeapro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @devlikeapro/n8n-openapi-node 2 | 3 | 4 | ![openapi logo](openapi.png) 5 | ![n8n logo](n8n.png) 6 | 7 | Turn Your **OpenAPI** (**Swagger**) spec into a **n8n node**! 8 | 9 | [![npm version](https://img.shields.io/npm/v/@devlikeapro/n8n-openapi-node.svg)](https://www.npmjs.com/package/@devlikeapro/n8n-openapi-node) 10 | 11 | --- 12 | 13 | 14 | 15 | - [Quick Start](#quick-start) 16 | * [Installation](#installation) 17 | * [Usage](#usage) 18 | - [How it works](#how-it-works) 19 | * [Resource](#resource) 20 | * [Operation](#operation) 21 | * [Query Parameters](#query-parameters) 22 | * [Request Body](#request-body) 23 | * [Headers](#headers) 24 | - [Customization](#customization) 25 | * [Resource](#resource-1) 26 | * [Operation](#operation-1) 27 | * [Fields](#fields) 28 | - [Use Cases](#use-cases) 29 | - [FAQ](#faq) 30 | * [I have only OpenAPI v2 spec, what can I do?](#i-have-only-openapi-v2-spec-what-can-i-do) 31 | * [I have openapi.yaml spec, what can I do?](#i-have-openapiyaml-spec-what-can-i-do) 32 | * [How to set up credentials from OpenAPI v3 spec?](#how-to-set-up-credentials-from-openapi-v3-spec) 33 | * [Why it doesn't work with my OpenAPI spec?](#why-it-doesnt-work-with-my-openapi-spec) 34 | - [Support the project](#support-the-project) 35 | 36 | 37 | 38 | # Quick Start 39 | 40 | If you have OpenAPI specification - you can easily in few minutes create 41 | [your community node](https://docs.n8n.io/integrations/community-nodes/usage/) for n8n! 42 | 43 | It'll still require to create and publish `n8n-nodes-` npm package, 44 | but you can use this package to generate most of the code. 45 | 46 | 47 | 👉 We recommend using one of repo for the `n8n-nodes-` package: 48 | 49 | - https://github.com/devlikeapro/n8n-nodes-petstore - Petstore example generated from OpenAPI v3 spec 50 | - https://github.com/n8n-io/n8n-nodes-starter - Official n8n nodes starter template 51 | 52 | Find more real-world examples in [Use Cases](#use-cases) section. 53 | 54 | ![example](node.png) 55 | 56 | ## Installation 57 | 58 | Add `@devlikeapro/n8n-openapi-node` as dependency 59 | 60 | ```bash 61 | npm install @devlikeapro/n8n-openapi-node 62 | # OR 63 | pnpm add @devlikeapro/n8n-openapi-node 64 | # OR 65 | yarn add @devlikeapro/n8n-openapi-node 66 | ``` 67 | 68 | 69 | ## Usage 70 | 71 | 1. Add your `openapi.json` to `src/{NodeName}` folder 72 | (use **OpenAPI v3** and **json**, see [FAQ](#faq) if you don't have it) 73 | 74 | 2. Get your `Node.properties` from OpenAPI v3 spec: 75 | 76 | ```typescript 77 | import {INodeType, INodeTypeDescription, NodeConnectionType} from 'n8n-workflow'; 78 | import {N8NPropertiesBuilder, N8NPropertiesBuilderConfig} from '@devlikeapro/n8n-openapi-node'; 79 | import * as doc from './openapi.json'; // <=== Your OpenAPI v3 spec 80 | 81 | const config: N8NPropertiesBuilderConfig = {} 82 | const parser = new N8NPropertiesBuilder(doc, config); 83 | const properties = parser.build() 84 | 85 | export class Petstore implements INodeType { 86 | description: INodeTypeDescription = { 87 | displayName: 'Petstore', 88 | name: 'petstore', 89 | icon: 'file:petstore.svg', 90 | group: ['transform'], 91 | version: 1, 92 | subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', 93 | description: 'Interact with Petstore API', 94 | defaults: { 95 | name: 'Petstore', 96 | }, 97 | inputs: [NodeConnectionType.Main], 98 | outputs: [NodeConnectionType.Main], 99 | credentials: [ 100 | { 101 | name: 'petstoreApi', 102 | required: false, 103 | }, 104 | ], 105 | requestDefaults: { 106 | headers: { 107 | Accept: 'application/json', 108 | 'Content-Type': 'application/json', 109 | }, 110 | baseURL: '={{$credentials.url}}', 111 | }, 112 | properties: properties, // <==== HERE 113 | }; 114 | } 115 | ``` 116 | 117 | # How it works 118 | 119 | `N8NPropertiesBuilder` extracts few entities from OpenAPI v3 to your n8n community node: 120 | 121 | 1. **Resource** - a list of **Tags** from OpenAPI spec 122 | 2. **Operation** - a list of **Operations** from OpenAPI spec (aka **Actions** in n8n) 123 | 3. **Query Parameters** - a list of `operation.parameters` from OpenAPI spec 124 | 4. **Request Body** - a list of `operation.requestBody.content` from OpenAPI spec (only for `application/json`) 125 | 5. **Headers** - a list of `operation.parameters` from OpenAPI spec 126 | 127 | ## Resource 128 | 129 | By default, it get **Tags** from OpenAPI spec and converts them to **Resource** in n8n. 130 | 131 | ## Operation 132 | 133 | By default, it gets **Operations** from OpenAPI spec and converts them to **Actions** in n8n. 134 | 135 | ## Query Parameters 136 | 137 | It gets `operation.parameters` from OpenAPI spec and converts them to **Query Parameters** in n8n. 138 | 139 | ## Request Body 140 | 141 | It doesn't create the full structure of the request body, only the first level of properties. 142 | So if you have request body as 143 | 144 | ```json 145 | { 146 | "name": "string", 147 | "config": { 148 | "id": 0, 149 | "name": "string" 150 | } 151 | } 152 | ``` 153 | 154 | it creates 2 fields in n8n: 155 | 156 | - `name` - with default value `string` 157 | - `config` - with default value `{"id": 0, "name": "string"}` 158 | 159 | ## Headers 160 | 161 | It gets `operation.parameters` from OpenAPI spec and converts them to **Headers** in n8n. 162 | 163 | # Customization 164 | 165 | ## Resource 166 | 167 | You can override the way how to extract **Resource** from **OpenAPI Tag** defining your custom `IResourceParser`: 168 | 169 | ```typescript 170 | import {IResourceParser} from '@devlikeapro/n8n-openapi-node'; 171 | 172 | export class CustomResourceParser { 173 | CUSTOM_DESCRIPTION = { 174 | "cats": "Cats are cute", 175 | } 176 | 177 | name(tag: OpenAPIV3.TagObject): string { 178 | // Your custom logic here 179 | if (tag['X-Visible-Name']) { 180 | return tag['X-Visible-Name']; 181 | } 182 | return lodash.startCase(tag.name); 183 | } 184 | 185 | value(tag: Pick): string { 186 | // Remove all non-alphanumeric characters 187 | const name = tag.name.replace(/[^a-zA-Z0-9_-]/g, '') 188 | return lodash.startCase(name) 189 | } 190 | 191 | description(tag: OpenAPIV3.TagObject): string { 192 | // Your custom logic here 193 | return this.CUSTOM_DESCRIPTION[tag.name] || tag.description || ''; 194 | } 195 | } 196 | ``` 197 | 198 | Alternatively, you can use `DefaultResourceParser` and override only the methods you need. 199 | The default implementation you can find in [src/ResourceParser.ts](src/ResourceParser.ts) 200 | 201 | ```typescript 202 | import {OpenAPIV3} from 'openapi-types'; 203 | import * as lodash from 'lodash'; 204 | import {DefaultResourceParser} from '@devlikeapro/n8n-openapi-node'; 205 | 206 | export class CustomResourceParser extends DefaultResourceParser { 207 | value(tag: OpenAPIV3.TagObject): string { 208 | return lodash.startCase(tag.name.replace(/[^a-zA-Z0-9_-]/g, '')); 209 | } 210 | } 211 | ``` 212 | 213 | Then you use it in `N8NPropertiesBuilder` in `config.resource`: 214 | 215 | ```typescript 216 | import {N8NPropertiesBuilder, N8NPropertiesBuilderConfig} from '@devlikeapro/n8n-openapi-node'; 217 | import * as doc from './openapi.json'; 218 | 219 | import {CustomResourceParser} from './CustomResourceParser'; 220 | 221 | const config: N8NPropertiesBuilderConfig = { 222 | resource: new CustomResourceParser() 223 | } 224 | const parser = new N8NPropertiesBuilder(doc, config); 225 | const properties = parser.build() 226 | ``` 227 | 228 | Find real example in [@devlikeapro/n8n-nodes-waha](https://github.com/devlikeapro/n8n-nodes-waha) repository. 229 | 230 | ## Operation 231 | 232 | You can override the way how to extract **Operation** from **OpenAPI Operation** defining your custom 233 | `IOperationParser`: 234 | 235 | ```typescript 236 | import {IOperationParser} from '@devlikeapro/n8n-openapi-node'; 237 | 238 | export class CustomOperationParser implements IOperationParser { 239 | shouldSkip(operation: OpenAPIV3.OperationObject, context: OperationContext): boolean { 240 | // By default it skips operation.deprecated 241 | // But we can include all operations 242 | return false 243 | } 244 | 245 | name(operation: OpenAPIV3.OperationObject, context: OperationContext): string { 246 | if (operation['X-Visible-Name']) { 247 | return operation['X-Visible-Name']; 248 | } 249 | return lodash.startCase(operation.operationId) 250 | } 251 | 252 | value(operation: OpenAPIV3.OperationObject, context: OperationContext): string { 253 | return lodash.startCase(operation.operationId) 254 | } 255 | 256 | action(operation: OpenAPIV3.OperationObject, context: OperationContext): string { 257 | // How operation is displayed in n8n when you select your node (right form) 258 | return operation.summary || this.name(operation, context) 259 | } 260 | 261 | description(operation: OpenAPIV3.OperationObject, context: OperationContext): string { 262 | return operation.description || operation.summary || ''; 263 | } 264 | } 265 | ``` 266 | 267 | Alternatively, you can use `DefaultOperationParser` and override only the methods you need. 268 | The default implementation you can find in [src/OperationParser.ts](src/OperationParser.ts) 269 | 270 | ```typescript 271 | import {DefaultOperationParser} from '@devlikeapro/n8n-openapi-node'; 272 | 273 | export class CustomOperationParser extends DefaultOperationParser { 274 | name(operation: OpenAPIV3.OperationObject, context: OperationContext): string { 275 | // NestJS add operationId in format CatController_findOne 276 | let operationId: string = operation.operationId!!.split('_').slice(1).join('_'); 277 | if (!operationId) { 278 | operationId = operation.operationId as string; 279 | } 280 | return lodash.startCase(operationId); 281 | } 282 | } 283 | ``` 284 | 285 | Then you use it in `N8NPropertiesBuilder` in `config.operation`: 286 | 287 | ```typescript 288 | import {N8NPropertiesBuilder, N8NPropertiesBuilderConfig} from '@devlikeapro/n8n-openapi-node'; 289 | import * as doc from './openapi.json'; 290 | import {CustomOperationParser} from './CustomOperationParser'; 291 | 292 | const config: N8NPropertiesBuilderConfig = { 293 | operation: new CustomOperationParser() 294 | } 295 | const parser = new N8NPropertiesBuilder(doc, config); 296 | const properties = parser.build() 297 | ``` 298 | 299 | Find real example in [@devlikeapro/n8n-nodes-waha](https://github.com/devlikeapro/n8n-nodes-waha) repository. 300 | 301 | ## Fields 302 | 303 | You can override some values for fields at the end, when full `properties` are ready. 304 | 305 | Here's example how you can override `session` field value (which has `'default'` string default value) to more n8n 306 | suitable `=${$json.session}}`: 307 | 308 | ```typescript 309 | import {Override} from '@devlikeapro/n8n-openapi-node'; 310 | 311 | export const customDefaults: Override[] = [ 312 | { 313 | // Find field by fields matching 314 | find: { 315 | name: 'session', 316 | required: true, 317 | type: 'string', 318 | }, 319 | // Replace 'default' field value 320 | replace: { 321 | default: '={{ $json.session }}', 322 | }, 323 | }, 324 | ]; 325 | ``` 326 | 327 | Then you use it in `N8NPropertiesBuilder`: 328 | 329 | ```typescript 330 | 331 | import {N8NPropertiesBuilder, N8NPropertiesBuilderConfig} from '@devlikeapro/n8n-openapi-node'; 332 | import * as doc from './openapi.json'; 333 | import {customDefaults} from './customDefaults'; 334 | 335 | const parser = new N8NPropertiesBuilder(doc); 336 | const properties = parser.build(customDefaults); 337 | ``` 338 | 339 | Find real example in [@devlikeapro/n8n-nodes-waha](https://github.com/devlikeapro/n8n-nodes-waha) repository. 340 | 341 | # Use Cases 342 | 343 | Here's n8n community nodes generated from OpenAPI specifications you can use for reference: 344 | 345 | - [@devlikeapro/n8n-nodes-petstore](https://github.com/devlikeapro/n8n-nodes-petstore) - Petstore example generated from 346 | [Petstore openapi.json](https://github.com/OAI/OpenAPI-Specification/blob/main/examples/v3.0/petstore.yaml) 347 | - [@devlikeapro/n8n-nodes-chatwoot](https://github.com/devlikeapro/n8n-nodes-chatwoot) - ChatWoot n8n community node 348 | from 349 | [https://www.chatwoot.com/developers/api/](https://www.chatwoot.com/developers/api/). Defines credentials as well ( 350 | manually) 351 | - [@devlikeapro/n8n-nodes-waha](https://github.com/devlikeapro/n8n-nodes-waha) - **WAHA** - Self-hosted **WhatsApp HTTP 352 | API** you can run in a click! 353 | 354 | # FAQ 355 | 356 | ## I have only OpenAPI v2 spec, what can I do? 357 | 358 | Paste your OpenAPI 2.0 definition into https://editor.swagger.io and select **Edit > Convert to OpenAPI 3** from the 359 | menu. 360 | 361 | https://stackoverflow.com/a/59749691 362 | 363 | ## I have openapi.yaml spec, what can I do? 364 | 365 | Paste your yaml spec to https://editor.swagger.io and select **File > Save as JSON** from the menu. 366 | 367 | ## How to set up credentials from OpenAPI v3 spec? 368 | 369 | Right now you need to define it manually. 370 | Check [ChatWoot node](https://github.com/devlikeapro/n8n-nodes-chatwoot) 371 | for an example. 372 | 373 | ## Why it doesn't work with my OpenAPI spec? 374 | 375 | Open [a new issue](https://github.com/devlikeapro/n8n-openapi-node/issues) and please attach 376 | your openapi.json file and describe the problem (logs are helpful too). 377 | 378 | # Support the project 379 | 380 | You can support the project by donating a small amount to help us improve the project even more. 381 | 382 | - [https://patreon.com/devlikeapro](https://patreon.com/devlikeapro) 383 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testPathIgnorePatterns: ['node_modules/', "dist/"], 4 | }; 5 | -------------------------------------------------------------------------------- /n8n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devlikeapro/n8n-openapi-node/c08043b4d20ab32700e5e7b180ccea5f45dde127/n8n.png -------------------------------------------------------------------------------- /node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devlikeapro/n8n-openapi-node/c08043b4d20ab32700e5e7b180ccea5f45dde127/node.png -------------------------------------------------------------------------------- /openapi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devlikeapro/n8n-openapi-node/c08043b4d20ab32700e5e7b180ccea5f45dde127/openapi.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@devlikeapro/n8n-openapi-node", 3 | "version": "0.1.4", 4 | "description": "Turn OpenAPI specs into n8n node", 5 | "main": "dist/src/index.js", 6 | "module": "dist/src/index.js", 7 | "types": "dist/src/index.d.ts", 8 | "scripts": { 9 | "build": "tsc", 10 | "test": "jest" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/devlikeapro/n8n-openapi-node.git" 15 | }, 16 | "keywords": [ 17 | "n8n", 18 | "openapi" 19 | ], 20 | "author": "Devlikeapro", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/devlikeapro/n8n-openapi-node/issues" 24 | }, 25 | "exports": { 26 | ".": { 27 | "import": "./dist/src/index.js", 28 | "require": "./dist/src/index.js" 29 | } 30 | }, 31 | "files": [ 32 | "dist" 33 | ], 34 | "homepage": "https://github.com/devlikeapro/n8n-openapi-node#readme", 35 | "devDependencies": { 36 | "@types/jest": "^29.5.13", 37 | "@types/lodash": "^4.17.7", 38 | "jest": "^29.7.0", 39 | "n8n-core": "*", 40 | "n8n-workflow": "*", 41 | "ts-jest": "^29.2.5", 42 | "typescript": "^5.6.2" 43 | }, 44 | "dependencies": { 45 | "lodash": "^4.17.21", 46 | "openapi-types": "^12.1.3", 47 | "pino": "^9.4.0", 48 | "pino-pretty": "^11.2.2" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/N8NPropertiesBuilder.spec.ts: -------------------------------------------------------------------------------- 1 | import {N8NPropertiesBuilder, Override} from './N8NPropertiesBuilder'; 2 | 3 | import {BaseOperationsCollector} from "./OperationsCollector"; 4 | import {OpenAPIV3} from "openapi-types"; 5 | import {OperationContext} from "./openapi/OpenAPIVisitor"; 6 | import * as lodash from "lodash"; 7 | import {DefaultOperationParser} from "./OperationParser"; 8 | import {DefaultResourceParser} from "./ResourceParser"; 9 | 10 | export class CustomOperationParser extends DefaultOperationParser { 11 | name(operation: OpenAPIV3.OperationObject, context: OperationContext): string { 12 | let operationId: string = operation.operationId!!.split('_').slice(1).join('_'); 13 | if (!operationId) { 14 | operationId = operation.operationId as string 15 | } 16 | return lodash.startCase(operationId) 17 | } 18 | 19 | value(operation: OpenAPIV3.OperationObject, context: OperationContext): string { 20 | return this.name(operation, context) 21 | } 22 | 23 | action(operation: OpenAPIV3.OperationObject, context: OperationContext): string { 24 | return operation.summary || this.name(operation, context) 25 | } 26 | 27 | description(operation: OpenAPIV3.OperationObject, context: OperationContext): string { 28 | return operation.description || operation.summary || ''; 29 | } 30 | } 31 | 32 | export class CustomResourceParser extends DefaultResourceParser { 33 | value(tag: OpenAPIV3.TagObject): string { 34 | return lodash.startCase(tag.name.replace(/[^a-zA-Z0-9_-]/g, '')); 35 | } 36 | } 37 | 38 | test('query param - schema', () => { 39 | const paths = { 40 | '/api/entities': { 41 | get: { 42 | operationId: 'EntityController_list', 43 | summary: 'List all entities', 44 | parameters: [ 45 | { 46 | name: 'all', 47 | required: false, 48 | in: 'query', 49 | example: false, 50 | description: 'Boolean flag description', 51 | schema: { 52 | type: 'boolean', 53 | }, 54 | }, 55 | ], 56 | tags: ['🖥️ Entity'], 57 | }, 58 | }, 59 | }; 60 | 61 | const parser = new N8NPropertiesBuilder({paths}, { 62 | operation: new CustomOperationParser(), 63 | resource: new CustomResourceParser(), 64 | }); 65 | const result = parser.build() 66 | 67 | expect(result).toEqual([ 68 | { 69 | "default": "", 70 | "displayName": "Resource", 71 | "name": "resource", 72 | "noDataExpression": true, 73 | "options": [ 74 | { 75 | "description": "", 76 | "name": "🖥️ Entity", 77 | "value": "Entity" 78 | } 79 | ], 80 | "type": "options" 81 | }, 82 | { 83 | displayName: 'Operation', 84 | name: 'operation', 85 | type: 'options', 86 | noDataExpression: true, 87 | displayOptions: { 88 | show: { 89 | resource: ['Entity'], 90 | }, 91 | }, 92 | options: [ 93 | { 94 | name: 'List', 95 | value: 'List', 96 | action: 'List all entities', 97 | description: 'List all entities', 98 | routing: { 99 | request: { 100 | method: 'GET', 101 | url: '=/api/entities', 102 | }, 103 | }, 104 | }, 105 | ], 106 | // eslint-disable-next-line 107 | default: '', 108 | }, 109 | { 110 | displayName: 'GET /api/entities', 111 | default: '', 112 | displayOptions: { 113 | show: { 114 | operation: ['List'], 115 | resource: ['Entity'], 116 | }, 117 | }, 118 | name: 'operation', 119 | type: 'notice', 120 | typeOptions: { 121 | theme: 'info', 122 | }, 123 | }, 124 | { 125 | displayName: 'All', 126 | name: 'all', 127 | type: 'boolean', 128 | displayOptions: { 129 | show: { 130 | resource: ['Entity'], 131 | operation: ['List'], 132 | }, 133 | }, 134 | default: false, 135 | // eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether 136 | description: 'Boolean flag description', 137 | routing: { 138 | "send": { 139 | "property": "all", 140 | "propertyInDotNotation": false, 141 | "type": "query", 142 | "value": "={{ $value }}" 143 | } 144 | }, 145 | }, 146 | ]); 147 | }); 148 | 149 | test('query param - content', () => { 150 | const paths = { 151 | '/api/entities': { 152 | get: { 153 | operationId: 'EntityController_list', 154 | summary: 'List all entities', 155 | parameters: [ 156 | { 157 | name: 'filter', 158 | required: false, 159 | in: 'query', 160 | example: false, 161 | description: 'Filter description', 162 | content: { 163 | 'application/json': { 164 | schema: { 165 | $ref: '#/components/schemas/Entity', 166 | }, 167 | }, 168 | } 169 | }, 170 | ], 171 | tags: ['🖥️ Entity'], 172 | }, 173 | }, 174 | }; 175 | const components = { 176 | schemas: { 177 | Entity: { 178 | type: 'object', 179 | properties: { 180 | name: { 181 | type: 'string', 182 | maxLength: 54, 183 | example: 'default', 184 | description: 'Entity name', 185 | }, 186 | start: { 187 | type: 'boolean', 188 | description: 'Boolean flag description', 189 | example: true, 190 | default: true, 191 | }, 192 | config: { 193 | $ref: '#/components/schemas/EntityConfig', 194 | }, 195 | }, 196 | required: ['name'], 197 | }, 198 | EntityConfig: { 199 | type: 'object', 200 | properties: { 201 | foo: { 202 | type: 'string', 203 | example: 'bar', 204 | }, 205 | }, 206 | }, 207 | }, 208 | }; 209 | 210 | const parser = new N8NPropertiesBuilder({paths, components}, { 211 | operation: new CustomOperationParser(), 212 | resource: new CustomResourceParser(), 213 | }); 214 | const result = parser.build() 215 | 216 | expect(result).toEqual([ 217 | { 218 | "default": "", 219 | "displayName": "Resource", 220 | "name": "resource", 221 | "noDataExpression": true, 222 | "options": [ 223 | { 224 | "description": "", 225 | "name": "🖥️ Entity", 226 | "value": "Entity" 227 | } 228 | ], 229 | "type": "options" 230 | }, 231 | { 232 | "default": "", 233 | "displayName": "Operation", 234 | "displayOptions": { 235 | "show": { 236 | "resource": [ 237 | "Entity" 238 | ] 239 | } 240 | }, 241 | "name": "operation", 242 | "noDataExpression": true, 243 | "options": [ 244 | { 245 | "action": "List all entities", 246 | "description": "List all entities", 247 | "name": "List", 248 | "routing": { 249 | "request": { 250 | "method": "GET", 251 | "url": "=/api/entities" 252 | } 253 | }, 254 | "value": "List" 255 | } 256 | ], 257 | "type": "options" 258 | }, 259 | { 260 | "default": "", 261 | "displayName": "GET /api/entities", 262 | "displayOptions": { 263 | "show": { 264 | "operation": [ 265 | "List" 266 | ], 267 | "resource": [ 268 | "Entity" 269 | ] 270 | } 271 | }, 272 | "name": "operation", 273 | "type": "notice", 274 | "typeOptions": { 275 | "theme": "info" 276 | } 277 | }, 278 | { 279 | "default": false, 280 | "description": "Filter description", 281 | "displayName": "Filter", 282 | "displayOptions": { 283 | "show": { 284 | "operation": [ 285 | "List" 286 | ], 287 | "resource": [ 288 | "Entity" 289 | ] 290 | } 291 | }, 292 | "name": "filter", 293 | "routing": { 294 | "send": { 295 | "property": "filter", 296 | "propertyInDotNotation": false, 297 | "type": "query", 298 | "value": "={{ $value }}" 299 | } 300 | }, 301 | "type": "json" 302 | } 303 | ]); 304 | }); 305 | 306 | test('query param - dot in field name', () => { 307 | const paths = { 308 | '/api/entities': { 309 | get: { 310 | operationId: 'EntityController_list', 311 | summary: 'List all entities', 312 | parameters: [ 313 | { 314 | name: 'filter.entities.all', 315 | required: false, 316 | in: 'query', 317 | example: false, 318 | description: 'Boolean flag description', 319 | schema: { 320 | type: 'boolean', 321 | }, 322 | }, 323 | ], 324 | tags: ['🖥️ Entity'], 325 | }, 326 | }, 327 | }; 328 | 329 | const parser = new N8NPropertiesBuilder({paths}, { 330 | operation: new CustomOperationParser(), 331 | resource: new CustomResourceParser(), 332 | }); 333 | const result = parser.build() 334 | 335 | expect(result).toEqual([ 336 | { 337 | "default": "", 338 | "displayName": "Resource", 339 | "name": "resource", 340 | "noDataExpression": true, 341 | "options": [ 342 | { 343 | "description": "", 344 | "name": "🖥️ Entity", 345 | "value": "Entity" 346 | } 347 | ], 348 | "type": "options" 349 | }, 350 | { 351 | displayName: 'Operation', 352 | name: 'operation', 353 | type: 'options', 354 | noDataExpression: true, 355 | displayOptions: { 356 | show: { 357 | resource: ['Entity'], 358 | }, 359 | }, 360 | options: [ 361 | { 362 | name: 'List', 363 | value: 'List', 364 | action: 'List all entities', 365 | description: 'List all entities', 366 | routing: { 367 | request: { 368 | method: 'GET', 369 | url: '=/api/entities', 370 | }, 371 | }, 372 | }, 373 | ], 374 | // eslint-disable-next-line 375 | default: '', 376 | }, 377 | { 378 | displayName: 'GET /api/entities', 379 | default: '', 380 | displayOptions: { 381 | show: { 382 | operation: ['List'], 383 | resource: ['Entity'], 384 | }, 385 | }, 386 | name: 'operation', 387 | type: 'notice', 388 | typeOptions: { 389 | theme: 'info', 390 | }, 391 | }, 392 | { 393 | displayName: 'Filter Entities All', 394 | name: 'filter-entities-all', 395 | type: 'boolean', 396 | displayOptions: { 397 | show: { 398 | resource: ['Entity'], 399 | operation: ['List'], 400 | }, 401 | }, 402 | default: false, 403 | // eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether 404 | description: 'Boolean flag description', 405 | routing: { 406 | "send": { 407 | "property": "filter.entities.all", 408 | "propertyInDotNotation": false, 409 | "type": "query", 410 | "value": "={{ $value }}" 411 | } 412 | }, 413 | }, 414 | ]); 415 | }); 416 | 417 | test('path param', () => { 418 | const paths = { 419 | '/api/entities/{entity}': { 420 | get: { 421 | operationId: 'EntityController_get', 422 | summary: 'Get entity', 423 | parameters: [ 424 | { 425 | name: 'entity', 426 | required: true, 427 | in: 'path', 428 | schema: { 429 | default: 'default', 430 | }, 431 | description: 'Entity name', 432 | }, 433 | ], 434 | tags: ['🖥️ Entity'], 435 | }, 436 | }, 437 | }; 438 | 439 | const parser = new N8NPropertiesBuilder({paths}, { 440 | OperationsCollector: BaseOperationsCollector, 441 | operation: new CustomOperationParser(), 442 | resource: new CustomResourceParser(), 443 | }); 444 | const result = parser.build() 445 | expect(result).toEqual([ 446 | { 447 | "default": "", 448 | "displayName": "Resource", 449 | "name": "resource", 450 | "noDataExpression": true, 451 | "options": [ 452 | { 453 | "description": "", 454 | "name": "🖥️ Entity", 455 | "value": "Entity" 456 | } 457 | ], 458 | "type": "options" 459 | }, 460 | { 461 | displayName: 'Operation', 462 | name: 'operation', 463 | type: 'options', 464 | noDataExpression: true, 465 | displayOptions: { 466 | show: { 467 | resource: ['Entity'], 468 | }, 469 | }, 470 | options: [ 471 | { 472 | name: 'Get', 473 | value: 'Get', 474 | action: 'Get entity', 475 | description: 'Get entity', 476 | routing: { 477 | request: { 478 | method: 'GET', 479 | url: '=/api/entities/{{$parameter["entity"]}}', 480 | }, 481 | }, 482 | }, 483 | ], 484 | // eslint-disable-next-line 485 | default: '', 486 | }, 487 | { 488 | displayName: 'Entity', 489 | name: 'entity', 490 | type: 'string', 491 | displayOptions: { 492 | show: { 493 | resource: ['Entity'], 494 | operation: ['Get'], 495 | }, 496 | }, 497 | default: 'default', 498 | required: true, 499 | description: 'Entity name', 500 | }, 501 | ]); 502 | }); 503 | 504 | test('request body', () => { 505 | const paths = { 506 | '/api/entities': { 507 | post: { 508 | operationId: 'EntityController_create', 509 | summary: 'Create entity', 510 | requestBody: { 511 | content: { 512 | 'application/json': { 513 | schema: { 514 | $ref: '#/components/schemas/Entity', 515 | }, 516 | }, 517 | }, 518 | }, 519 | tags: ['🖥️ Entity'], 520 | }, 521 | }, 522 | }; 523 | const components = { 524 | schemas: { 525 | Entity: { 526 | type: 'object', 527 | properties: { 528 | name: { 529 | type: 'string', 530 | maxLength: 54, 531 | example: 'default', 532 | description: 'Entity name', 533 | }, 534 | start: { 535 | type: 'boolean', 536 | description: 'Boolean flag description', 537 | example: true, 538 | default: true, 539 | }, 540 | config: { 541 | $ref: '#/components/schemas/EntityConfig', 542 | }, 543 | }, 544 | required: ['name'], 545 | }, 546 | EntityConfig: { 547 | type: 'object', 548 | properties: { 549 | foo: { 550 | type: 'string', 551 | example: 'bar', 552 | }, 553 | }, 554 | }, 555 | }, 556 | }; 557 | 558 | const parser = new N8NPropertiesBuilder({paths, components}, { 559 | OperationsCollector: BaseOperationsCollector, 560 | operation: new CustomOperationParser(), 561 | resource: new CustomResourceParser(), 562 | }); 563 | const result = parser.build() 564 | 565 | expect(result).toEqual([ 566 | { 567 | "default": "", 568 | "displayName": "Resource", 569 | "name": "resource", 570 | "noDataExpression": true, 571 | "options": [ 572 | { 573 | "description": "", 574 | "name": "🖥️ Entity", 575 | "value": "Entity" 576 | } 577 | ], 578 | "type": "options" 579 | }, 580 | { 581 | displayName: 'Operation', 582 | name: 'operation', 583 | type: 'options', 584 | noDataExpression: true, 585 | displayOptions: { 586 | show: { 587 | resource: ['Entity'], 588 | }, 589 | }, 590 | options: [ 591 | { 592 | name: 'Create', 593 | value: 'Create', 594 | action: 'Create entity', 595 | description: 'Create entity', 596 | routing: { 597 | request: { 598 | method: 'POST', 599 | url: '=/api/entities', 600 | }, 601 | }, 602 | }, 603 | ], 604 | // eslint-disable-next-line 605 | default: '', 606 | }, 607 | { 608 | displayName: 'Name', 609 | name: 'name', 610 | type: 'string', 611 | default: 'default', 612 | description: 'Entity name', 613 | required: true, 614 | displayOptions: { 615 | show: { 616 | resource: ['Entity'], 617 | operation: ['Create'], 618 | }, 619 | }, 620 | routing: { 621 | "send": { 622 | "property": "name", 623 | "propertyInDotNotation": false, 624 | "type": "body", 625 | "value": "={{ $value }}" 626 | }, 627 | }, 628 | }, 629 | { 630 | displayName: 'Start', 631 | name: 'start', 632 | type: 'boolean', 633 | default: true, 634 | required: undefined, 635 | // eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether 636 | description: 'Boolean flag description', 637 | displayOptions: { 638 | show: { 639 | resource: ['Entity'], 640 | operation: ['Create'], 641 | }, 642 | }, 643 | routing: { 644 | "send": { 645 | "property": "start", 646 | "propertyInDotNotation": false, 647 | "type": "body", 648 | "value": "={{ $value }}" 649 | }, 650 | }, 651 | }, 652 | { 653 | displayName: 'Config', 654 | name: 'config', 655 | type: 'json', 656 | displayOptions: { 657 | show: { 658 | resource: ['Entity'], 659 | operation: ['Create'], 660 | }, 661 | }, 662 | default: JSON.stringify({foo: 'bar'}, null, 2), 663 | required: undefined, 664 | routing: { 665 | "send": { 666 | "property": "config", 667 | "propertyInDotNotation": false, 668 | "type": "body", 669 | "value": "={{ JSON.parse($value) }}" 670 | }, 671 | }, 672 | }, 673 | ]); 674 | }); 675 | 676 | test('enum schema', () => { 677 | const paths = { 678 | '/api/entities': { 679 | post: { 680 | operationId: 'EntityController_create', 681 | summary: 'Create entity', 682 | requestBody: { 683 | content: { 684 | 'application/json': { 685 | schema: { 686 | type: 'object', 687 | properties: { 688 | type: { 689 | type: 'string', 690 | enum: ['type1', 'type2'], 691 | }, 692 | }, 693 | required: ['type'], 694 | }, 695 | }, 696 | }, 697 | }, 698 | tags: ['🖥️ Entity'], 699 | }, 700 | }, 701 | }; 702 | 703 | // @ts-ignore 704 | const parser = new N8NPropertiesBuilder({paths}, { 705 | OperationsCollector: BaseOperationsCollector, 706 | operation: new CustomOperationParser(), 707 | resource: new CustomResourceParser(), 708 | }); 709 | const result = parser.build() 710 | 711 | expect(result).toEqual([ 712 | { 713 | "default": "", 714 | "displayName": "Resource", 715 | "name": "resource", 716 | "noDataExpression": true, 717 | "options": [ 718 | { 719 | "description": "", 720 | "name": "🖥️ Entity", 721 | "value": "Entity" 722 | } 723 | ], 724 | "type": "options" 725 | }, 726 | 727 | { 728 | displayName: 'Operation', 729 | name: 'operation', 730 | type: 'options', 731 | noDataExpression: true, 732 | displayOptions: { 733 | show: { 734 | resource: ['Entity'], 735 | }, 736 | }, 737 | options: [ 738 | { 739 | name: 'Create', 740 | value: 'Create', 741 | action: 'Create entity', 742 | description: 'Create entity', 743 | routing: { 744 | request: { 745 | method: 'POST', 746 | url: '=/api/entities', 747 | }, 748 | }, 749 | }, 750 | ], 751 | // eslint-disable-next-line 752 | default: '', 753 | }, 754 | { 755 | displayName: 'Type', 756 | name: 'type', 757 | type: 'options', 758 | default: 'type1', 759 | required: true, 760 | options: [ 761 | { 762 | name: 'Type 1', 763 | value: 'type1', 764 | }, 765 | { 766 | name: 'Type 2', 767 | value: 'type2', 768 | }, 769 | ], 770 | displayOptions: { 771 | show: { 772 | resource: ['Entity'], 773 | operation: ['Create'], 774 | }, 775 | }, 776 | routing: { 777 | "send": { 778 | "property": "type", 779 | "propertyInDotNotation": false, 780 | "type": "body", 781 | "value": "={{ $value }}" 782 | }, 783 | }, 784 | }, 785 | ]); 786 | }); 787 | 788 | test('body "array" param', () => { 789 | const paths = { 790 | '/api/entities': { 791 | post: { 792 | operationId: 'EntityController_create', 793 | summary: 'Create entity', 794 | requestBody: { 795 | content: { 796 | 'application/json': { 797 | schema: { 798 | type: 'array', 799 | items: { 800 | type: 'string', 801 | }, 802 | }, 803 | }, 804 | }, 805 | }, 806 | tags: ['🖥️ Entity'], 807 | }, 808 | }, 809 | }; 810 | 811 | const parser = new N8NPropertiesBuilder({paths}, { 812 | OperationsCollector: BaseOperationsCollector, 813 | operation: new CustomOperationParser(), 814 | resource: new CustomResourceParser(), 815 | }); 816 | const result = parser.build() 817 | 818 | const expected: any[] = [ 819 | { 820 | "default": "", 821 | "displayName": "Resource", 822 | "name": "resource", 823 | "noDataExpression": true, 824 | "options": [ 825 | { 826 | "description": "", 827 | "name": "🖥️ Entity", 828 | "value": "Entity" 829 | } 830 | ], 831 | "type": "options" 832 | }, 833 | { 834 | "default": "", 835 | "displayName": "Operation", 836 | "displayOptions": { 837 | "show": { 838 | "resource": [ 839 | "Entity" 840 | ] 841 | } 842 | }, 843 | "name": "operation", 844 | "noDataExpression": true, 845 | "options": [ 846 | { 847 | "action": "Create entity", 848 | "description": "Create entity", 849 | "name": "Create", 850 | "routing": { 851 | "request": { 852 | "method": "POST", 853 | "url": "=/api/entities" 854 | } 855 | }, 856 | "value": "Create" 857 | } 858 | ], 859 | "type": "options" 860 | }, 861 | { 862 | "default": "", 863 | "displayName": "Body", 864 | "displayOptions": { 865 | "show": { 866 | "operation": [ 867 | "Create" 868 | ], 869 | "resource": [ 870 | "Entity" 871 | ] 872 | } 873 | }, 874 | "name": "body", 875 | "routing": { 876 | "request": { 877 | "body": "={{ JSON.parse($value) }}" 878 | } 879 | }, 880 | "type": "string" 881 | } 882 | ] 883 | expect(result).toEqual(expected) 884 | } 885 | ) 886 | 887 | test('test overrides', () => { 888 | const paths = { 889 | '/api/entities': { 890 | post: { 891 | operationId: 'EntityController_create', 892 | summary: 'Create entity', 893 | requestBody: { 894 | content: { 895 | 'application/json': { 896 | schema: { 897 | $ref: '#/components/schemas/Entity', 898 | }, 899 | }, 900 | }, 901 | }, 902 | tags: ['🖥️ Entity'], 903 | }, 904 | }, 905 | }; 906 | const components = { 907 | schemas: { 908 | Entity: { 909 | type: 'object', 910 | properties: { 911 | name: { 912 | type: 'string', 913 | maxLength: 54, 914 | example: 'default', 915 | description: 'Entity name', 916 | }, 917 | start: { 918 | type: 'boolean', 919 | description: 'Boolean flag description', 920 | example: true, 921 | default: true, 922 | }, 923 | config: { 924 | $ref: '#/components/schemas/EntityConfig', 925 | }, 926 | }, 927 | required: ['name'], 928 | }, 929 | EntityConfig: { 930 | type: 'object', 931 | properties: { 932 | foo: { 933 | type: 'string', 934 | example: 'bar', 935 | }, 936 | }, 937 | }, 938 | }, 939 | }; 940 | 941 | const customDefaults: Override[] = [ 942 | { 943 | find: { 944 | name: 'config', 945 | displayOptions: { 946 | show: { 947 | resource: ['Entity'], 948 | operation: ['Create'], 949 | }, 950 | }, 951 | }, 952 | replace: { 953 | default: '={{ $json.config }}', 954 | }, 955 | }, 956 | ]; 957 | 958 | const parser = new N8NPropertiesBuilder({paths, components}, { 959 | OperationsCollector: BaseOperationsCollector, 960 | operation: new CustomOperationParser(), 961 | resource: new CustomResourceParser(), 962 | }); 963 | const result = parser.build(customDefaults) 964 | 965 | expect(result).toEqual([ 966 | { 967 | "default": "", 968 | "displayName": "Resource", 969 | "name": "resource", 970 | "noDataExpression": true, 971 | "options": [ 972 | { 973 | "description": "", 974 | "name": "🖥️ Entity", 975 | "value": "Entity" 976 | } 977 | ], 978 | "type": "options" 979 | }, 980 | { 981 | displayName: 'Operation', 982 | name: 'operation', 983 | type: 'options', 984 | noDataExpression: true, 985 | displayOptions: { 986 | show: { 987 | resource: ['Entity'], 988 | }, 989 | }, 990 | options: [ 991 | { 992 | name: 'Create', 993 | value: 'Create', 994 | action: 'Create entity', 995 | description: 'Create entity', 996 | routing: { 997 | request: { 998 | method: 'POST', 999 | url: '=/api/entities', 1000 | }, 1001 | }, 1002 | }, 1003 | ], 1004 | // eslint-disable-next-line 1005 | default: '', 1006 | }, 1007 | { 1008 | displayName: 'Name', 1009 | name: 'name', 1010 | type: 'string', 1011 | default: 'default', 1012 | description: 'Entity name', 1013 | required: true, 1014 | displayOptions: { 1015 | show: { 1016 | resource: ['Entity'], 1017 | operation: ['Create'], 1018 | }, 1019 | }, 1020 | routing: { 1021 | "send": { 1022 | "property": "name", 1023 | "propertyInDotNotation": false, 1024 | "type": "body", 1025 | "value": "={{ $value }}" 1026 | }, 1027 | }, 1028 | }, 1029 | { 1030 | displayName: 'Start', 1031 | name: 'start', 1032 | type: 'boolean', 1033 | default: true, 1034 | required: undefined, 1035 | // eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether 1036 | description: 'Boolean flag description', 1037 | displayOptions: { 1038 | show: { 1039 | resource: ['Entity'], 1040 | operation: ['Create'], 1041 | }, 1042 | }, 1043 | routing: { 1044 | "send": { 1045 | "property": "start", 1046 | "propertyInDotNotation": false, 1047 | "type": "body", 1048 | "value": "={{ $value }}" 1049 | }, 1050 | }, 1051 | }, 1052 | { 1053 | displayName: 'Config', 1054 | name: 'config', 1055 | type: 'json', 1056 | displayOptions: { 1057 | show: { 1058 | resource: ['Entity'], 1059 | operation: ['Create'], 1060 | }, 1061 | }, 1062 | default: "={{ $json.config }}", 1063 | required: undefined, 1064 | routing: { 1065 | "send": { 1066 | "property": "config", 1067 | "propertyInDotNotation": false, 1068 | "type": "body", 1069 | "value": "={{ JSON.parse($value) }}" 1070 | }, 1071 | }, 1072 | }, 1073 | ]); 1074 | }); 1075 | 1076 | test('multiple tags', () => { 1077 | const paths = { 1078 | '/api/entities': { 1079 | get: { 1080 | operationId: 'EntityController_list', 1081 | summary: 'List all entities', 1082 | parameters: [ 1083 | { 1084 | name: 'all', 1085 | required: false, 1086 | in: 'query', 1087 | example: false, 1088 | description: 'Boolean flag description', 1089 | schema: { 1090 | type: 'boolean', 1091 | }, 1092 | }, 1093 | ], 1094 | tags: [ 1095 | '🖥️ Entity', 1096 | "Another Tag" 1097 | ], 1098 | }, 1099 | }, 1100 | }; 1101 | 1102 | const parser = new N8NPropertiesBuilder({paths}, { 1103 | operation: new CustomOperationParser(), 1104 | resource: new CustomResourceParser(), 1105 | }) 1106 | const result = parser.build() 1107 | 1108 | expect(result).toEqual( 1109 | [ 1110 | { 1111 | "default": "", 1112 | "displayName": "Resource", 1113 | "name": "resource", 1114 | "noDataExpression": true, 1115 | "options": [ 1116 | { 1117 | "description": "", 1118 | "name": "🖥️ Entity", 1119 | "value": "Entity" 1120 | }, 1121 | { 1122 | "description": "", 1123 | "name": "Another Tag", 1124 | "value": "Another Tag" 1125 | } 1126 | ], 1127 | "type": "options" 1128 | }, 1129 | { 1130 | "default": "", 1131 | "displayName": "Operation", 1132 | "displayOptions": { 1133 | "show": { 1134 | "resource": [ 1135 | "Entity" 1136 | ] 1137 | } 1138 | }, 1139 | "name": "operation", 1140 | "noDataExpression": true, 1141 | "options": [ 1142 | { 1143 | "action": "List all entities", 1144 | "description": "List all entities", 1145 | "name": "List", 1146 | "routing": { 1147 | "request": { 1148 | "method": "GET", 1149 | "url": "=/api/entities" 1150 | } 1151 | }, 1152 | "value": "List" 1153 | } 1154 | ], 1155 | "type": "options" 1156 | }, 1157 | { 1158 | "default": "", 1159 | "displayName": "Operation", 1160 | "displayOptions": { 1161 | "show": { 1162 | "resource": [ 1163 | "Another Tag" 1164 | ] 1165 | } 1166 | }, 1167 | "name": "operation", 1168 | "noDataExpression": true, 1169 | "options": [ 1170 | { 1171 | "action": "List all entities", 1172 | "description": "List all entities", 1173 | "name": "List", 1174 | "routing": { 1175 | "request": { 1176 | "method": "GET", 1177 | "url": "=/api/entities" 1178 | } 1179 | }, 1180 | "value": "List" 1181 | } 1182 | ], 1183 | "type": "options" 1184 | }, 1185 | { 1186 | "default": "", 1187 | "displayName": "GET /api/entities", 1188 | "displayOptions": { 1189 | "show": { 1190 | "operation": [ 1191 | "List" 1192 | ], 1193 | "resource": [ 1194 | "Entity" 1195 | ] 1196 | } 1197 | }, 1198 | "name": "operation", 1199 | "type": "notice", 1200 | "typeOptions": { 1201 | "theme": "info" 1202 | } 1203 | }, 1204 | { 1205 | "default": false, 1206 | "description": "Boolean flag description", 1207 | "displayName": "All", 1208 | "displayOptions": { 1209 | "show": { 1210 | "operation": [ 1211 | "List" 1212 | ], 1213 | "resource": [ 1214 | "Entity" 1215 | ] 1216 | } 1217 | }, 1218 | "name": "all", 1219 | "routing": { 1220 | "send": { 1221 | "property": "all", 1222 | "propertyInDotNotation": false, 1223 | "type": "query", 1224 | "value": "={{ $value }}" 1225 | }, 1226 | }, 1227 | "type": "boolean" 1228 | }, 1229 | { 1230 | "default": "", 1231 | "displayName": "GET /api/entities", 1232 | "displayOptions": { 1233 | "show": { 1234 | "operation": [ 1235 | "List" 1236 | ], 1237 | "resource": [ 1238 | "Another Tag" 1239 | ] 1240 | } 1241 | }, 1242 | "name": "operation", 1243 | "type": "notice", 1244 | "typeOptions": { 1245 | "theme": "info" 1246 | } 1247 | }, 1248 | { 1249 | "default": false, 1250 | "description": "Boolean flag description", 1251 | "displayName": "All", 1252 | "displayOptions": { 1253 | "show": { 1254 | "operation": [ 1255 | "List" 1256 | ], 1257 | "resource": [ 1258 | "Another Tag" 1259 | ] 1260 | } 1261 | }, 1262 | "name": "all", 1263 | "routing": { 1264 | "send": { 1265 | "property": "all", 1266 | "propertyInDotNotation": false, 1267 | "type": "query", 1268 | "value": "={{ $value }}" 1269 | }, 1270 | }, 1271 | "type": "boolean" 1272 | } 1273 | ] 1274 | ); 1275 | }); 1276 | 1277 | test('no tags - default tag', () => { 1278 | const paths = { 1279 | '/api/entities': { 1280 | get: { 1281 | operationId: 'EntityController_list', 1282 | summary: 'List all entities', 1283 | parameters: [ 1284 | { 1285 | name: 'all', 1286 | required: false, 1287 | in: 'query', 1288 | example: false, 1289 | description: 'Boolean flag description', 1290 | schema: { 1291 | type: 'boolean', 1292 | }, 1293 | }, 1294 | ], 1295 | tags: [], 1296 | }, 1297 | }, 1298 | }; 1299 | 1300 | const parser = new N8NPropertiesBuilder({paths}, { 1301 | operation: new CustomOperationParser(), 1302 | resource: new CustomResourceParser(), 1303 | }); 1304 | const result = parser.build() 1305 | 1306 | expect(result).toEqual( 1307 | [ 1308 | { 1309 | "default": "", 1310 | "displayName": "Resource", 1311 | "name": "resource", 1312 | "noDataExpression": true, 1313 | "options": [ 1314 | { 1315 | "description": "", 1316 | "name": "Default", 1317 | "value": "Default" 1318 | } 1319 | ], 1320 | "type": "options" 1321 | }, 1322 | { 1323 | "default": "", 1324 | "displayName": "Operation", 1325 | "displayOptions": { 1326 | "show": { 1327 | "resource": [ 1328 | "Default" 1329 | ] 1330 | } 1331 | }, 1332 | "name": "operation", 1333 | "noDataExpression": true, 1334 | "options": [ 1335 | { 1336 | "action": "List all entities", 1337 | "description": "List all entities", 1338 | "name": "List", 1339 | "routing": { 1340 | "request": { 1341 | "method": "GET", 1342 | "url": "=/api/entities" 1343 | } 1344 | }, 1345 | "value": "List" 1346 | } 1347 | ], 1348 | "type": "options" 1349 | }, 1350 | { 1351 | "default": "", 1352 | "displayName": "GET /api/entities", 1353 | "displayOptions": { 1354 | "show": { 1355 | "operation": [ 1356 | "List" 1357 | ], 1358 | "resource": [ 1359 | "Default" 1360 | ] 1361 | } 1362 | }, 1363 | "name": "operation", 1364 | "type": "notice", 1365 | "typeOptions": { 1366 | "theme": "info" 1367 | } 1368 | }, 1369 | { 1370 | "default": false, 1371 | "description": "Boolean flag description", 1372 | "displayName": "All", 1373 | "displayOptions": { 1374 | "show": { 1375 | "operation": [ 1376 | "List" 1377 | ], 1378 | "resource": [ 1379 | "Default" 1380 | ] 1381 | } 1382 | }, 1383 | "name": "all", 1384 | "routing": { 1385 | "send": { 1386 | "property": "all", 1387 | "propertyInDotNotation": false, 1388 | "type": "query", 1389 | "value": "={{ $value }}" 1390 | }, 1391 | }, 1392 | "type": "boolean" 1393 | } 1394 | ] 1395 | ); 1396 | }); 1397 | -------------------------------------------------------------------------------- /src/N8NPropertiesBuilder.ts: -------------------------------------------------------------------------------- 1 | import {INodeProperties} from 'n8n-workflow'; 2 | import {OpenAPIV3} from 'openapi-types'; 3 | import pino from 'pino'; 4 | import {OpenAPIWalker} from "./openapi/OpenAPIWalker"; 5 | import {ResourceCollector as ResourcePropertiesCollector} from "./ResourceCollector"; 6 | import {BaseOperationsCollector, OperationsCollector as OperationsCollectorImpl} from "./OperationsCollector"; 7 | import * as lodash from "lodash"; 8 | import {DefaultOperationParser, IOperationParser} from "./OperationParser"; 9 | import {DefaultResourceParser, IResourceParser} from "./ResourceParser"; 10 | 11 | export interface Override { 12 | find: any; 13 | replace: any; 14 | } 15 | 16 | export interface N8NPropertiesBuilderConfig { 17 | logger?: pino.Logger; 18 | OperationsCollector?: typeof BaseOperationsCollector, 19 | ResourcePropertiesCollector?: typeof ResourcePropertiesCollector 20 | operation?: IOperationParser, 21 | resource?: IResourceParser, 22 | } 23 | 24 | /** 25 | * 26 | * Builds n8n node "properties" from an OpenAPI document. 27 | * It uses a walker to traverse the OpenAPI document and collect the necessary information. 28 | * The collected information is then used to build the n8n node properties. 29 | * The class uses a set of parsers to parse the OpenAPI document and build the n8n node properties. 30 | * 31 | */ 32 | export class N8NPropertiesBuilder { 33 | private readonly doc: OpenAPIV3.Document; 34 | private readonly logger: pino.Logger 35 | private readonly walker: OpenAPIWalker; 36 | 37 | // DI 38 | private readonly operationParser: IOperationParser; 39 | private readonly resourceParser: IResourceParser; 40 | private readonly OperationsCollector: typeof BaseOperationsCollector; 41 | private readonly ResourcePropertiesCollector: typeof ResourcePropertiesCollector; 42 | 43 | constructor(doc: any, config?: N8NPropertiesBuilderConfig) { 44 | this.doc = doc 45 | this.logger = config?.logger || pino({transport: {target: 'pino-pretty'}}) 46 | this.walker = new OpenAPIWalker(this.doc) 47 | 48 | // DI 49 | this.operationParser = config?.operation || new DefaultOperationParser() 50 | this.resourceParser = config?.resource || new DefaultResourceParser() 51 | this.OperationsCollector = config?.OperationsCollector ? config.OperationsCollector : OperationsCollectorImpl 52 | this.ResourcePropertiesCollector = config?.ResourcePropertiesCollector ? config.ResourcePropertiesCollector : ResourcePropertiesCollector 53 | } 54 | 55 | build(overrides: Override[] = []): INodeProperties[] { 56 | const resourcePropertiesCollector = new this.ResourcePropertiesCollector(this.resourceParser) 57 | this.walker.walk(resourcePropertiesCollector) 58 | const resourceNode = resourcePropertiesCollector.resources 59 | 60 | const operationsCollector = new this.OperationsCollector( 61 | this.doc, 62 | this.operationParser, 63 | this.resourceParser, 64 | this.logger, 65 | ) 66 | this.walker.walk(operationsCollector) 67 | const operations = operationsCollector.operations 68 | const fields = operationsCollector.fields 69 | 70 | const properties = [resourceNode, ...operations, ...fields] 71 | return this.update(properties, overrides) 72 | } 73 | 74 | private update(fields: any[], patterns: Override[]) { 75 | for (const pattern of patterns) { 76 | for (const element of lodash.filter(fields, pattern.find)) { 77 | Object.assign(element, pattern.replace); 78 | } 79 | } 80 | return fields 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/OperationParser.ts: -------------------------------------------------------------------------------- 1 | import {OpenAPIV3} from "openapi-types"; 2 | import * as lodash from "lodash"; 3 | import {OperationContext} from "./openapi/OpenAPIVisitor"; 4 | 5 | /** 6 | * Extract information for n8n node from OpenAPI operation 7 | */ 8 | export interface IOperationParser { 9 | /** 10 | * Name of the operation (e.g. "Create User") 11 | */ 12 | name(operation: OpenAPIV3.OperationObject, context: OperationContext): string 13 | 14 | /** 15 | * Value of the operation (e.g. "create-user") 16 | */ 17 | value(operation: OpenAPIV3.OperationObject, context: OperationContext): string 18 | 19 | /** 20 | * Action of the operation (e.g. "Create User") - will be visible in list of actions 21 | */ 22 | action(operation: OpenAPIV3.OperationObject, context: OperationContext): string 23 | 24 | /** 25 | * Description of the operation 26 | */ 27 | description(operation: OpenAPIV3.OperationObject, context: OperationContext): string 28 | 29 | /** 30 | * Should skip this operation or not 31 | */ 32 | shouldSkip(operation: OpenAPIV3.OperationObject, context: OperationContext): boolean; 33 | } 34 | 35 | /** 36 | * Default behaviour for OpenAPI to n8n operation parser 37 | * It will use operationId as name, value and action and summary as description 38 | * Skip deprecated operations 39 | */ 40 | export class DefaultOperationParser implements IOperationParser { 41 | shouldSkip(operation: OpenAPIV3.OperationObject, context: OperationContext): boolean { 42 | return !!operation.deprecated 43 | } 44 | 45 | name(operation: OpenAPIV3.OperationObject, context: OperationContext): string { 46 | if (operation.operationId) { 47 | return lodash.startCase(operation.operationId) 48 | } 49 | return context.method.toUpperCase() + " " + context.pattern 50 | } 51 | 52 | value(operation: OpenAPIV3.OperationObject, context: OperationContext): string { 53 | let name = this.name(operation, context) 54 | // replace all non-alphanumeric characters with '-' 55 | return name.replace(/[^a-zA-Z0-9 ]/g, '-'); 56 | } 57 | 58 | action(operation: OpenAPIV3.OperationObject, context: OperationContext): string { 59 | return operation.summary || this.name(operation, context) 60 | } 61 | 62 | description(operation: OpenAPIV3.OperationObject, context: OperationContext): string { 63 | return operation.description || operation.summary || ''; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/OperationsCollector.ts: -------------------------------------------------------------------------------- 1 | import {OpenAPIVisitor, OperationContext} from "./openapi/OpenAPIVisitor"; 2 | import * as lodash from "lodash"; 3 | import pino from "pino"; 4 | import {OpenAPIV3} from "openapi-types"; 5 | import {N8NINodeProperties} from "./n8n/SchemaToINodeProperties"; 6 | import {IOperationParser} from "./OperationParser"; 7 | import {OptionsByResourceMap} from "./n8n/OptionsByResourceMap"; 8 | import {INodeProperties} from "n8n-workflow"; 9 | import {replacePathVarsToParameter} from "./n8n/utils"; 10 | import {IResourceParser} from "./ResourceParser"; 11 | 12 | export class BaseOperationsCollector implements OpenAPIVisitor { 13 | public readonly _fields: INodeProperties[] 14 | private optionsByResource: OptionsByResourceMap = new OptionsByResourceMap() 15 | private n8nNodeProperties: N8NINodeProperties; 16 | 17 | // Log context 18 | private bindings: any 19 | 20 | constructor( 21 | doc: any, 22 | protected operationParser: IOperationParser, 23 | protected resourceParser: IResourceParser, 24 | protected logger: pino.Logger 25 | ) { 26 | this._fields = [] 27 | this.n8nNodeProperties = new N8NINodeProperties(doc) 28 | } 29 | 30 | get operations(): INodeProperties[] { 31 | if (this.optionsByResource.size === 0) { 32 | throw new Error('No operations found in OpenAPI document') 33 | } 34 | 35 | const operations = [] 36 | for (const [resource, options] of this.optionsByResource) { 37 | const operation: INodeProperties = { 38 | displayName: 'Operation', 39 | name: 'operation', 40 | type: 'options', 41 | noDataExpression: true, 42 | displayOptions: { 43 | show: { 44 | resource: [resource], 45 | }, 46 | }, 47 | options: options, 48 | default: '', 49 | }; 50 | operations.push(operation); 51 | } 52 | return operations 53 | } 54 | 55 | get fields() { 56 | return [...this._fields] 57 | } 58 | 59 | visitOperation(operation: OpenAPIV3.OperationObject, context: OperationContext) { 60 | const bindings = { 61 | operation: { 62 | pattern: context.pattern, 63 | method: context.method, 64 | operationId: operation.operationId 65 | } 66 | } 67 | this.bindings = bindings 68 | try { 69 | this._visitOperation(operation, context) 70 | } catch (error) { 71 | // @ts-ignore 72 | const data = {...this.bindings, error: `${error}`} 73 | // @ts-ignore 74 | this.logger.warn(data, 'Failed to parse operation') 75 | } 76 | } 77 | 78 | _visitOperation(operation: OpenAPIV3.OperationObject, context: OperationContext) { 79 | if (this.operationParser.shouldSkip(operation, context)) { 80 | this.logger.info(this.bindings, 'Skipping operation') 81 | return 82 | } 83 | const {option, fields: operationFields} = this.parseOperation(operation, context); 84 | const resources = operation.tags!!.map((tag: string) => this.resourceParser.value({name: tag})) 85 | for (const resourceName of resources) { 86 | const fields = lodash.cloneDeep(operationFields) 87 | const operationName = option.name; 88 | this.addDisplayOption(fields, resourceName, operationName) 89 | this.optionsByResource.add(resourceName, option); 90 | this._fields.push(...fields) 91 | } 92 | } 93 | 94 | /** 95 | * Parse fields from operation, both parameters and request body 96 | */ 97 | parseFields(operation: OpenAPIV3.OperationObject, context: OperationContext) { 98 | const fields = []; 99 | const parameterFields = this.n8nNodeProperties.fromParameters(operation.parameters) 100 | fields.push(...parameterFields); 101 | 102 | try { 103 | const bodyFields = this.n8nNodeProperties.fromRequestBody(operation.requestBody) 104 | fields.push(...bodyFields); 105 | } catch (error) { 106 | const data = {...this.bindings, error: `${error}`} 107 | // @ts-ignore 108 | this.logger.warn(data, 'Failed to parse request body') 109 | const msg = "There's no body available for request, kindly use HTTP Request node to send body" 110 | const notice: INodeProperties = { 111 | displayName: `${context.method.toUpperCase()} ${context.pattern}

${msg}`, 112 | name: 'operation', 113 | type: 'notice', 114 | default: '', 115 | } 116 | fields.push(notice) 117 | } 118 | return fields; 119 | } 120 | 121 | private addDisplayOption(fields: INodeProperties[], resource: string, operation: string) { 122 | const displayOptions = { 123 | show: { 124 | resource: [resource], 125 | operation: [operation], 126 | }, 127 | } 128 | fields.forEach((field) => { 129 | field.displayOptions = displayOptions 130 | }) 131 | } 132 | 133 | protected parseOperation(operation: OpenAPIV3.OperationObject, context: OperationContext) { 134 | const method = context.method 135 | const uri = context.pattern; 136 | const parser = this.operationParser 137 | const option = { 138 | name: parser.name(operation, context), 139 | value: parser.value(operation, context), 140 | action: parser.action(operation, context), 141 | description: parser.description(operation, context), 142 | routing: { 143 | request: { 144 | method: method.toUpperCase(), 145 | url: `=${replacePathVarsToParameter(uri)}`, 146 | }, 147 | }, 148 | }; 149 | const fields = this.parseFields(operation, context); 150 | 151 | 152 | return { 153 | option: option, 154 | fields: fields, 155 | }; 156 | } 157 | } 158 | 159 | export class OperationsCollector extends BaseOperationsCollector { 160 | protected parseOperation(operation: OpenAPIV3.OperationObject, context: OperationContext) { 161 | const result = super.parseOperation(operation, context) 162 | const notice: INodeProperties = { 163 | displayName: `${context.method.toUpperCase()} ${context.pattern}`, 164 | name: 'operation', 165 | type: 'notice', 166 | typeOptions: { 167 | theme: 'info', 168 | }, 169 | default: '', 170 | }; 171 | result.fields.unshift(notice); 172 | return result 173 | } 174 | } 175 | 176 | -------------------------------------------------------------------------------- /src/ResourceCollector.ts: -------------------------------------------------------------------------------- 1 | import {OpenAPIVisitor, OperationContext} from "./openapi/OpenAPIVisitor"; 2 | import {OpenAPIV3} from "openapi-types"; 3 | import {INodeProperties} from "n8n-workflow"; 4 | import {IResourceParser} from "./ResourceParser"; 5 | 6 | interface TagObject { 7 | name: string; 8 | description: string; 9 | } 10 | 11 | /** 12 | * Collects resource properties from OpenAPI document 13 | * Resource is basically tags from OpenAPI spec 14 | */ 15 | export class ResourceCollector implements OpenAPIVisitor { 16 | private tags: Map; 17 | private tagsOrder = new Map(); 18 | 19 | constructor(protected resourceParser: IResourceParser) { 20 | this.tags = new Map() 21 | } 22 | 23 | get resources(): INodeProperties { 24 | const tags = this.sortedTags 25 | const parser = this.resourceParser 26 | const options = tags.map((tag) => { 27 | return { 28 | name: parser.name(tag), 29 | value: parser.value(tag), 30 | description: parser.description(tag), 31 | }; 32 | }); 33 | return { 34 | displayName: 'Resource', 35 | name: 'resource', 36 | type: 'options', 37 | noDataExpression: true, 38 | options: options, 39 | default: '', 40 | }; 41 | } 42 | 43 | private get sortedTags() { 44 | const tags = Array.from(this.tags.values()) 45 | tags.sort((a, b) => { 46 | return this.tagsOrder.get(a.name,)! - this.tagsOrder.get(b.name)!; 47 | }) 48 | // put "default" at the end if not present explicitly in 'tags" 49 | if (!this.tagsOrder.has('default')) { 50 | const defaultTag = tags.find((tag) => tag.name === 'default') 51 | if (defaultTag) { 52 | tags.splice(tags.indexOf(defaultTag), 1) 53 | tags.push(defaultTag) 54 | } 55 | } 56 | return tags; 57 | } 58 | 59 | visitOperation(operation: OpenAPIV3.OperationObject, context: OperationContext) { 60 | let tags = operation.tags as string[] 61 | tags.forEach((tag) => this.addTagByName(tag)) 62 | } 63 | 64 | private addTagByName(tag: string) { 65 | // insert if not found 66 | if (!this.tags.has(tag)) { 67 | this.tags.set(tag, { 68 | name: tag, 69 | description: '', 70 | }); 71 | } 72 | } 73 | 74 | visitTag(tag: OpenAPIV3.TagObject): void { 75 | const name = tag.name; 76 | this.tags.set(name, { 77 | name: name, 78 | description: tag.description || '', 79 | }); 80 | this.tagsOrder.set(name, this.tagsOrder.size); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/ResourceParser.ts: -------------------------------------------------------------------------------- 1 | import {OpenAPIV3} from "openapi-types"; 2 | import * as lodash from "lodash"; 3 | 4 | /** 5 | * Extract information for n8n node from OpenAPI resource 6 | */ 7 | export interface IResourceParser { 8 | /** 9 | * Name of the resource (e.g. "User") 10 | * @param tag 11 | */ 12 | name(tag: OpenAPIV3.TagObject): string 13 | 14 | /** 15 | * Value of the resource (e.g. "user") 16 | * @note - will be used on operations as well, only "name" will be available for that 17 | */ 18 | value(tag: Pick): string 19 | 20 | /** 21 | * Description of the resource 22 | * @param tag 23 | */ 24 | description(tag: OpenAPIV3.TagObject): string 25 | } 26 | 27 | /** 28 | * Default behaviour for OpenAPI to n8n resource parser 29 | * It will use tag name as name and value and description as description 30 | */ 31 | export class DefaultResourceParser { 32 | name(tag: OpenAPIV3.TagObject): string { 33 | return lodash.startCase(tag.name); 34 | } 35 | 36 | value(tag: Pick): string { 37 | return lodash.startCase(tag.name) 38 | } 39 | 40 | description(tag: OpenAPIV3.TagObject): string { 41 | return tag.description || ''; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // export 2 | import {OpenAPIVisitor, OperationContext} from "./openapi/OpenAPIVisitor"; 3 | import {OpenAPIWalker} from "./openapi/OpenAPIWalker"; 4 | import {N8NPropertiesBuilder, N8NPropertiesBuilderConfig, Override} from "./N8NPropertiesBuilder"; 5 | import {OperationsCollector} from "./OperationsCollector"; 6 | import {DefaultOperationParser, IOperationParser} from "./OperationParser"; 7 | import {DefaultResourceParser, IResourceParser} from "./ResourceParser"; 8 | import {ResourceCollector} from "./ResourceCollector"; 9 | 10 | 11 | export { 12 | N8NPropertiesBuilderConfig, 13 | N8NPropertiesBuilder, 14 | OpenAPIVisitor, 15 | OpenAPIWalker, 16 | OperationContext, 17 | IOperationParser, 18 | DefaultOperationParser, 19 | OperationsCollector, 20 | IResourceParser, 21 | DefaultResourceParser, 22 | Override, 23 | ResourceCollector, 24 | } 25 | -------------------------------------------------------------------------------- /src/n8n/OptionsByResourceMap.ts: -------------------------------------------------------------------------------- 1 | import * as lodash from "lodash"; 2 | 3 | export class OptionsByResourceMap extends Map { 4 | add(resource: string, option: any) { 5 | if (!this.has(resource)) { 6 | this.set(resource, []); 7 | } 8 | const options = this.get(resource)!!; 9 | if (lodash.find(options, {value: option.value})) { 10 | throw new Error(`Duplicate operation '${option.value}' for resource '${resource}'`); 11 | } 12 | options.push(option); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/n8n/SchemaToINodeProperties.ts: -------------------------------------------------------------------------------- 1 | import {OpenAPIV3} from "openapi-types"; 2 | import {INodeProperties, NodePropertyTypes} from "n8n-workflow"; 3 | import {RefResolver} from "../openapi/RefResolver"; 4 | import * as lodash from "lodash"; 5 | import {SchemaExample} from "../openapi/SchemaExample"; 6 | 7 | type Schema = OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject; 8 | type FromSchemaNodeProperty = Pick; 9 | 10 | function combine(...sources: Partial[]): INodeProperties { 11 | const obj = lodash.defaults({}, ...sources) 12 | if (!obj.required) { 13 | // n8n does want to have required: false|null|undefined 14 | delete obj.required 15 | } 16 | return obj 17 | } 18 | 19 | /** 20 | * in obj find key starts with regexp 21 | * Return first match VALUE of the key 22 | */ 23 | function findKey(obj: any, regexp: RegExp): any | undefined { 24 | const key = Object.keys(obj).find((key) => regexp.test(key)) 25 | return key ? obj[key] : undefined 26 | } 27 | 28 | /** 29 | * One level deep - meaning only top fields of the schema 30 | * The rest represent as JSON string 31 | */ 32 | export class N8NINodeProperties { 33 | private refResolver: RefResolver; 34 | private schemaExample: SchemaExample; 35 | 36 | constructor(doc: any) { 37 | this.refResolver = new RefResolver(doc) 38 | this.schemaExample = new SchemaExample(doc) 39 | } 40 | 41 | fromSchema(schema: Schema): FromSchemaNodeProperty { 42 | schema = this.refResolver.resolve(schema) 43 | let type: NodePropertyTypes; 44 | let defaultValue = this.schemaExample.extractExample(schema) 45 | 46 | switch (schema.type) { 47 | case 'boolean': 48 | type = 'boolean'; 49 | defaultValue = defaultValue !== undefined ? defaultValue : true; 50 | break; 51 | case 'string': 52 | case undefined: 53 | type = 'string'; 54 | defaultValue = defaultValue !== undefined ? defaultValue : ''; 55 | break; 56 | case 'object': 57 | type = 'json'; 58 | defaultValue = defaultValue !== undefined ? JSON.stringify(defaultValue, null, 2) : '{}'; 59 | break; 60 | case 'array': 61 | type = 'json'; 62 | defaultValue = defaultValue !== undefined ? JSON.stringify(defaultValue, null, 2) : '[]'; 63 | break; 64 | case 'number': 65 | case 'integer': 66 | type = 'number'; 67 | defaultValue = defaultValue !== undefined ? defaultValue : 0; 68 | break; 69 | } 70 | 71 | const field: FromSchemaNodeProperty = { 72 | type: type, 73 | default: defaultValue, 74 | description: schema.description, 75 | }; 76 | if (schema.enum && schema.enum.length > 0) { 77 | field.type = 'options'; 78 | field.options = schema.enum.map((value: string) => { 79 | return { 80 | name: lodash.startCase(value), 81 | value: value, 82 | }; 83 | }); 84 | field.default = field.default ? field.default : schema.enum[0]; 85 | } 86 | return field; 87 | } 88 | 89 | fromParameter(parameter: OpenAPIV3.ReferenceObject | OpenAPIV3.ParameterObject): INodeProperties { 90 | parameter = this.refResolver.resolve(parameter) 91 | let fieldSchemaKeys 92 | if (parameter.schema) { 93 | fieldSchemaKeys = this.fromSchema(parameter.schema!!); 94 | } 95 | if (!fieldSchemaKeys) { 96 | const regexp = /application\/json.*/ 97 | const content = findKey(parameter.content, regexp) 98 | fieldSchemaKeys = this.fromSchema(content.schema); 99 | } 100 | if (!fieldSchemaKeys) { 101 | throw new Error(`Parameter schema nor content not found`) 102 | } 103 | const fieldParameterKeys: Partial = { 104 | displayName: lodash.startCase(parameter.name), 105 | name: encodeURIComponent(parameter.name.replace(/\./g, "-")), 106 | required: parameter.required, 107 | description: parameter.description, 108 | default: parameter.example, 109 | }; 110 | const field = combine(fieldParameterKeys, fieldSchemaKeys) 111 | 112 | switch (parameter.in) { 113 | case "query": 114 | field.routing = { 115 | send: { 116 | type: 'query', 117 | property: parameter.name, 118 | value: '={{ $value }}', 119 | propertyInDotNotation: false, 120 | }, 121 | }; 122 | break; 123 | case "path" : 124 | field.required = true 125 | break 126 | case "header": 127 | field.routing = { 128 | request: { 129 | headers: { 130 | [parameter.name]: '={{ $value }}', 131 | }, 132 | }, 133 | }; 134 | break 135 | default: 136 | throw new Error(`Unknown parameter location '${parameter.in}'`); 137 | } 138 | if (!field.required) { 139 | delete field.required 140 | } 141 | return field 142 | } 143 | 144 | fromParameters(parameters: (OpenAPIV3.ReferenceObject | OpenAPIV3.ParameterObject)[] | undefined): INodeProperties[] { 145 | if (!parameters) { 146 | return []; 147 | } 148 | const fields = []; 149 | for (const parameter of parameters) { 150 | const field = this.fromParameter(parameter) 151 | fields.push(field); 152 | } 153 | return fields; 154 | } 155 | 156 | fromSchemaProperty(name: string, property: OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject): INodeProperties { 157 | const fieldSchemaKeys: FromSchemaNodeProperty = this.fromSchema(property) 158 | const fieldParameterKeys: Partial = { 159 | displayName: lodash.startCase(name), 160 | name: name.replace(/\./g, "-"), 161 | } 162 | const field = combine(fieldParameterKeys, fieldSchemaKeys) 163 | return field 164 | } 165 | 166 | fromRequestBody(body: OpenAPIV3.ReferenceObject | OpenAPIV3.RequestBodyObject | undefined): INodeProperties[] { 167 | if (!body) { 168 | return []; 169 | } 170 | body = this.refResolver.resolve(body) 171 | const regexp = /application\/json.*/ 172 | const content = findKey(body.content, regexp) 173 | if (!content) { 174 | throw new Error(`No '${regexp}' content found`); 175 | } 176 | const requestBodySchema = content.schema!!; 177 | const schema = this.refResolver.resolve(requestBodySchema) 178 | if (!schema.properties && schema.type != 'object' && schema.type != 'array') { 179 | throw new Error(`Request body schema type '${schema.type}' not supported`); 180 | } 181 | 182 | const fields = []; 183 | if (schema.type === "array" && schema.items) { 184 | const innerSchema = this.refResolver.resolve(schema.items) 185 | const fieldPropertyKeys: FromSchemaNodeProperty = this.fromSchemaProperty("body", innerSchema) 186 | const fieldDefaults: Partial = { 187 | required: !!schema.required 188 | } 189 | const field = combine(fieldDefaults, fieldPropertyKeys) 190 | field.routing = { 191 | request: { 192 | body: '={{ JSON.parse($value) }}' 193 | }, 194 | }; 195 | fields.push(field); 196 | } 197 | 198 | 199 | const properties = schema.properties; 200 | for (const key in properties) { 201 | const property = properties[key]; 202 | const fieldPropertyKeys: FromSchemaNodeProperty = this.fromSchemaProperty(key, property) 203 | const fieldDefaults: Partial = { 204 | required: schema.required && schema.required?.includes(key), 205 | } 206 | const field = combine(fieldDefaults, fieldPropertyKeys) 207 | if (field.type === 'json') { 208 | field.routing = { 209 | send: { 210 | "property": key, 211 | "propertyInDotNotation": false, 212 | "type": "body", 213 | "value": '={{ JSON.parse($value) }}' 214 | }, 215 | }; 216 | } else { 217 | field.routing = { 218 | send: { 219 | "property": key, 220 | "propertyInDotNotation": false, 221 | "type": "body", 222 | "value": '={{ $value }}' 223 | }, 224 | }; 225 | } 226 | fields.push(field); 227 | } 228 | return fields; 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/n8n/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * /api/entities/{entity} => /api/entities/{{$parameter["entity"]}} 3 | */ 4 | export function replacePathVarsToParameter(uri: string): string { 5 | return uri.replace(/{([^}]*)}/g, '{{$parameter["$1"]}}'); 6 | } 7 | -------------------------------------------------------------------------------- /src/openapi/OpenAPIVisitor.ts: -------------------------------------------------------------------------------- 1 | import {OpenAPIV3} from "openapi-types"; 2 | 3 | export type OperationContext = { 4 | pattern: string, 5 | path: OpenAPIV3.PathItemObject, 6 | method: OpenAPIV3.HttpMethods, 7 | } 8 | 9 | export interface OpenAPIVisitor { 10 | visitDocument?(doc: OpenAPIV3.Document): void; 11 | 12 | visitOperation?(operation: OpenAPIV3.OperationObject, context: OperationContext): void; 13 | 14 | visitTag?(tag: OpenAPIV3.TagObject): void; 15 | 16 | finish?(): void; 17 | } 18 | -------------------------------------------------------------------------------- /src/openapi/OpenAPIWalker.ts: -------------------------------------------------------------------------------- 1 | import {OpenAPIV3} from "openapi-types"; 2 | import {OpenAPIVisitor} from "./OpenAPIVisitor"; 3 | 4 | const HttpMethods: string[] = Object.values(OpenAPIV3.HttpMethods); 5 | 6 | export class OpenAPIWalker { 7 | private readonly doc: OpenAPIV3.Document 8 | 9 | constructor(doc: any) { 10 | this.doc = doc; 11 | 12 | } 13 | 14 | walk(visitor: OpenAPIVisitor) { 15 | this.walkDocument(visitor); 16 | this.walkPaths(visitor); 17 | this.walkTags(visitor); 18 | if (visitor.finish) { 19 | visitor.finish(); 20 | } 21 | } 22 | 23 | private walkDocument(visitor: OpenAPIVisitor, doc?: OpenAPIV3.Document) { 24 | if (!doc) { 25 | doc = this.doc; 26 | } 27 | if (visitor.visitDocument) { 28 | visitor.visitDocument(doc) 29 | } 30 | } 31 | 32 | private walkPaths(visitor: OpenAPIVisitor, paths?: OpenAPIV3.PathsObject) { 33 | if (!paths) { 34 | paths = this.doc.paths; 35 | } 36 | if (!paths) { 37 | return; 38 | } 39 | for (const path in paths) { 40 | const pathItem: OpenAPIV3.PathItemObject = paths[path] as OpenAPIV3.PathItemObject; 41 | let method: string; 42 | let operation: any; 43 | for ([method, operation] of Object.entries(pathItem)) { 44 | if (!HttpMethods.includes(method)) { 45 | continue; 46 | } 47 | if (!operation.tags || operation.tags.length === 0) { 48 | operation.tags = ['default'] 49 | } 50 | if (operation && visitor.visitOperation) { 51 | const context = {pattern: path, path: pathItem, method: method as OpenAPIV3.HttpMethods}; 52 | visitor.visitOperation(operation, context); 53 | } 54 | } 55 | } 56 | } 57 | 58 | private walkTags(visitor: OpenAPIVisitor, tags?: OpenAPIV3.TagObject[]) { 59 | if (!tags) { 60 | tags = this.doc.tags; 61 | } 62 | if (!tags) { 63 | return; 64 | } 65 | if (!visitor.visitTag) { 66 | return; 67 | } 68 | for (const tag of tags) { 69 | visitor.visitTag(tag); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/openapi/RefResolver.ts: -------------------------------------------------------------------------------- 1 | import {OpenAPIV3} from "openapi-types"; 2 | import * as lodash from "lodash"; 3 | 4 | export class RefResolver { 5 | constructor(private doc: any) { 6 | 7 | } 8 | 9 | /** 10 | * Resolve ref and return it if found 11 | * @param schema 12 | */ 13 | resolveRef(schema: OpenAPIV3.ReferenceObject | T): [T, string[]?] { 14 | // @ts-ignore 15 | if ("properties" in schema) { 16 | return [schema as T, undefined] 17 | } 18 | // @ts-ignore 19 | if ("oneOf" in schema) { 20 | // @ts-ignore 21 | schema = schema.oneOf[0] 22 | } 23 | // @ts-ignore 24 | if ("anyOf" in schema) { 25 | // @ts-ignore 26 | schema = schema.anyOf[0] 27 | } 28 | // @ts-ignore 29 | if ("allOf" in schema) { 30 | // @ts-ignore 31 | const results = schema.allOf.map((s) => this.resolveRef(s)); 32 | const schemas = results.map((r: any) => r[0]); 33 | const refs = results.map((r: any) => r[1]); 34 | const refsFlat = lodash.flatten(refs); 35 | const object = Object.assign({}, ...schemas); 36 | return [object as T, refsFlat] 37 | } 38 | // @ts-ignore 39 | if ('$ref' in schema) { 40 | const schemaResolved = this.findRef(schema['$ref']); 41 | // Remove $ref from schema, add all other properties 42 | const {$ref, ...rest} = schema; 43 | Object.assign(rest, schemaResolved); 44 | return [rest as T, [$ref]] 45 | } 46 | return [schema as T, undefined] 47 | } 48 | 49 | resolve(schema: OpenAPIV3.ReferenceObject | T): T { 50 | return this.resolveRef(schema)[0] 51 | } 52 | 53 | private findRef(ref: string): OpenAPIV3.SchemaObject { 54 | const refPath = ref.split('/').slice(1); 55 | let schema: any = this.doc; 56 | for (const path of refPath) { 57 | // @ts-ignore 58 | schema = schema[path]; 59 | if (!schema) { 60 | throw new Error(`Schema not found for ref '${ref}'`); 61 | } 62 | } 63 | if ('$ref' in schema) { 64 | return this.findRef(schema['$ref']); 65 | } 66 | return schema; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/openapi/SchemaExample.ts: -------------------------------------------------------------------------------- 1 | import {RefResolver} from "./RefResolver"; 2 | import {OpenAPIV3} from "openapi-types"; 3 | 4 | class SchemaExampleBuilder { 5 | private visitedRefs: Set = new Set(); 6 | 7 | constructor(private resolver: RefResolver) { 8 | } 9 | 10 | build(schema: OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject): any { 11 | let refs: string[] | undefined 12 | [schema, refs] = this.resolver.resolveRef(schema) 13 | 14 | if (refs) { 15 | // Prevent infinite recursion 16 | for (const ref of refs) { 17 | if (this.visitedRefs.has(ref)) { 18 | return {} 19 | } 20 | this.visitedRefs.add(ref); 21 | } 22 | } 23 | if ('oneOf' in schema) { 24 | return this.build(schema.oneOf!![0]); 25 | } 26 | if ('allOf' in schema) { 27 | const examples = schema.allOf!!.map((s) => this.build(s)); 28 | return Object.assign({}, ...examples); 29 | } 30 | if (schema.example !== undefined) { 31 | return schema.example; 32 | } 33 | if (schema.default !== undefined) { 34 | return schema.default; 35 | } 36 | if (schema.properties) { 37 | const obj: any = {}; 38 | for (const key in schema.properties) { 39 | obj[key] = this.build(schema.properties[key]); 40 | } 41 | return obj; 42 | } 43 | if ('items' in schema && schema.items) { 44 | return [this.build(schema.items)]; 45 | } 46 | return undefined; 47 | } 48 | } 49 | 50 | export class SchemaExample { 51 | private resolver: RefResolver; 52 | 53 | constructor(doc: any) { 54 | this.resolver = new RefResolver(doc); 55 | } 56 | 57 | extractExample(schema: OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject): any { 58 | return new SchemaExampleBuilder(this.resolver).build(schema); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/petstore.spec.ts: -------------------------------------------------------------------------------- 1 | import {N8NPropertiesBuilder} from "../src/N8NPropertiesBuilder"; 2 | import {INodeProperties} from "n8n-workflow"; 3 | 4 | test('petstore.json', () => { 5 | const doc = require('./samples/petstore.json'); 6 | const config = {} 7 | const parser = new N8NPropertiesBuilder(doc, config); 8 | const result = parser.build() 9 | 10 | const expected: INodeProperties[] = [ 11 | { 12 | "default": "", 13 | "displayName": "Resource", 14 | "name": "resource", 15 | "noDataExpression": true, 16 | "options": [ 17 | { 18 | "description": "Everything about your Pets", 19 | "name": "Pet", 20 | "value": "Pet" 21 | }, 22 | { 23 | "description": "Access to Petstore orders", 24 | "name": "Store", 25 | "value": "Store" 26 | }, 27 | { 28 | "description": "Operations about user", 29 | "name": "User", 30 | "value": "User" 31 | } 32 | ], 33 | "type": "options" 34 | }, 35 | { 36 | "default": "", 37 | "displayName": "Operation", 38 | "displayOptions": { 39 | "show": { 40 | "resource": [ 41 | "Pet" 42 | ] 43 | } 44 | }, 45 | "name": "operation", 46 | "noDataExpression": true, 47 | "options": [ 48 | { 49 | "action": "Update an existing pet", 50 | "description": "Update an existing pet by Id", 51 | "name": "Update Pet", 52 | "routing": { 53 | "request": { 54 | "method": "PUT", 55 | "url": "=/pet" 56 | } 57 | }, 58 | "value": "Update Pet" 59 | }, 60 | { 61 | "action": "Add a new pet to the store", 62 | "description": "Add a new pet to the store", 63 | "name": "Add Pet", 64 | "routing": { 65 | "request": { 66 | "method": "POST", 67 | "url": "=/pet" 68 | } 69 | }, 70 | "value": "Add Pet" 71 | }, 72 | { 73 | "action": "Finds Pets by status", 74 | "description": "Multiple status values can be provided with comma separated strings", 75 | "name": "Find Pets By Status", 76 | "routing": { 77 | "request": { 78 | "method": "GET", 79 | "url": "=/pet/findByStatus" 80 | } 81 | }, 82 | "value": "Find Pets By Status" 83 | }, 84 | { 85 | "action": "Finds Pets by tags", 86 | "description": "Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.", 87 | "name": "Find Pets By Tags", 88 | "routing": { 89 | "request": { 90 | "method": "GET", 91 | "url": "=/pet/findByTags" 92 | } 93 | }, 94 | "value": "Find Pets By Tags" 95 | }, 96 | { 97 | "action": "Find pet by ID", 98 | "description": "Returns a single pet", 99 | "name": "Get Pet By Id", 100 | "routing": { 101 | "request": { 102 | "method": "GET", 103 | "url": "=/pet/{{$parameter[\"petId\"]}}" 104 | } 105 | }, 106 | "value": "Get Pet By Id" 107 | }, 108 | { 109 | "action": "Updates a pet in the store with form data", 110 | "description": "Updates a pet in the store with form data", 111 | "name": "Update Pet With Form", 112 | "routing": { 113 | "request": { 114 | "method": "POST", 115 | "url": "=/pet/{{$parameter[\"petId\"]}}" 116 | } 117 | }, 118 | "value": "Update Pet With Form" 119 | }, 120 | { 121 | "action": "Deletes a pet", 122 | "description": "Deletes a pet", 123 | "name": "Delete Pet", 124 | "routing": { 125 | "request": { 126 | "method": "DELETE", 127 | "url": "=/pet/{{$parameter[\"petId\"]}}" 128 | } 129 | }, 130 | "value": "Delete Pet" 131 | }, 132 | { 133 | "action": "uploads an image", 134 | "description": "uploads an image", 135 | "name": "Upload File", 136 | "routing": { 137 | "request": { 138 | "method": "POST", 139 | "url": "=/pet/{{$parameter[\"petId\"]}}/uploadImage" 140 | } 141 | }, 142 | "value": "Upload File" 143 | } 144 | ], 145 | "type": "options" 146 | }, 147 | { 148 | "default": "", 149 | "displayName": "Operation", 150 | "displayOptions": { 151 | "show": { 152 | "resource": [ 153 | "Store" 154 | ] 155 | } 156 | }, 157 | "name": "operation", 158 | "noDataExpression": true, 159 | "options": [ 160 | { 161 | "action": "Returns pet inventories by status", 162 | "description": "Returns a map of status codes to quantities", 163 | "name": "Get Inventory", 164 | "routing": { 165 | "request": { 166 | "method": "GET", 167 | "url": "=/store/inventory" 168 | } 169 | }, 170 | "value": "Get Inventory" 171 | }, 172 | { 173 | "action": "Place an order for a pet", 174 | "description": "Place a new order in the store", 175 | "name": "Place Order", 176 | "routing": { 177 | "request": { 178 | "method": "POST", 179 | "url": "=/store/order" 180 | } 181 | }, 182 | "value": "Place Order" 183 | }, 184 | { 185 | "action": "Find purchase order by ID", 186 | "description": "For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions.", 187 | "name": "Get Order By Id", 188 | "routing": { 189 | "request": { 190 | "method": "GET", 191 | "url": "=/store/order/{{$parameter[\"orderId\"]}}" 192 | } 193 | }, 194 | "value": "Get Order By Id" 195 | }, 196 | { 197 | "action": "Delete purchase order by ID", 198 | "description": "For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors", 199 | "name": "Delete Order", 200 | "routing": { 201 | "request": { 202 | "method": "DELETE", 203 | "url": "=/store/order/{{$parameter[\"orderId\"]}}" 204 | } 205 | }, 206 | "value": "Delete Order" 207 | } 208 | ], 209 | "type": "options" 210 | }, 211 | { 212 | "default": "", 213 | "displayName": "Operation", 214 | "displayOptions": { 215 | "show": { 216 | "resource": [ 217 | "User" 218 | ] 219 | } 220 | }, 221 | "name": "operation", 222 | "noDataExpression": true, 223 | "options": [ 224 | { 225 | "action": "Create user", 226 | "description": "This can only be done by the logged in user.", 227 | "name": "Create User", 228 | "routing": { 229 | "request": { 230 | "method": "POST", 231 | "url": "=/user" 232 | } 233 | }, 234 | "value": "Create User" 235 | }, 236 | { 237 | "action": "Creates list of users with given input array", 238 | "description": "Creates list of users with given input array", 239 | "name": "Create Users With List Input", 240 | "routing": { 241 | "request": { 242 | "method": "POST", 243 | "url": "=/user/createWithList" 244 | } 245 | }, 246 | "value": "Create Users With List Input" 247 | }, 248 | { 249 | "action": "Logs user into the system", 250 | "description": "Logs user into the system", 251 | "name": "Login User", 252 | "routing": { 253 | "request": { 254 | "method": "GET", 255 | "url": "=/user/login" 256 | } 257 | }, 258 | "value": "Login User" 259 | }, 260 | { 261 | "action": "Logs out current logged in user session", 262 | "description": "Logs out current logged in user session", 263 | "name": "Logout User", 264 | "routing": { 265 | "request": { 266 | "method": "GET", 267 | "url": "=/user/logout" 268 | } 269 | }, 270 | "value": "Logout User" 271 | }, 272 | { 273 | "action": "Get user by user name", 274 | "description": "Get user by user name", 275 | "name": "Get User By Name", 276 | "routing": { 277 | "request": { 278 | "method": "GET", 279 | "url": "=/user/{{$parameter[\"username\"]}}" 280 | } 281 | }, 282 | "value": "Get User By Name" 283 | }, 284 | { 285 | "action": "Update user", 286 | "description": "This can only be done by the logged in user.", 287 | "name": "Update User", 288 | "routing": { 289 | "request": { 290 | "method": "PUT", 291 | "url": "=/user/{{$parameter[\"username\"]}}" 292 | } 293 | }, 294 | "value": "Update User" 295 | }, 296 | { 297 | "action": "Delete user", 298 | "description": "This can only be done by the logged in user.", 299 | "name": "Delete User", 300 | "routing": { 301 | "request": { 302 | "method": "DELETE", 303 | "url": "=/user/{{$parameter[\"username\"]}}" 304 | } 305 | }, 306 | "value": "Delete User" 307 | } 308 | ], 309 | "type": "options" 310 | }, 311 | { 312 | "default": "", 313 | "displayName": "PUT /pet", 314 | "displayOptions": { 315 | "show": { 316 | "operation": [ 317 | "Update Pet" 318 | ], 319 | "resource": [ 320 | "Pet" 321 | ] 322 | } 323 | }, 324 | "name": "operation", 325 | "type": "notice", 326 | "typeOptions": { 327 | "theme": "info" 328 | } 329 | }, 330 | { 331 | "default": 10, 332 | "displayName": "Id", 333 | "displayOptions": { 334 | "show": { 335 | "operation": [ 336 | "Update Pet" 337 | ], 338 | "resource": [ 339 | "Pet" 340 | ] 341 | } 342 | }, 343 | "name": "id", 344 | "routing": { 345 | "send": { 346 | "property": "id", 347 | "propertyInDotNotation": false, 348 | "type": "body", 349 | "value": "={{ $value }}" 350 | } 351 | }, 352 | "type": "number" 353 | }, 354 | { 355 | "default": "doggie", 356 | "displayName": "Name", 357 | "displayOptions": { 358 | "show": { 359 | "operation": [ 360 | "Update Pet" 361 | ], 362 | "resource": [ 363 | "Pet" 364 | ] 365 | } 366 | }, 367 | "name": "name", 368 | "required": true, 369 | "routing": { 370 | "send": { 371 | "property": "name", 372 | "propertyInDotNotation": false, 373 | "type": "body", 374 | "value": "={{ $value }}" 375 | } 376 | }, 377 | "type": "string" 378 | }, 379 | { 380 | "default": "{\n \"id\": 1,\n \"name\": \"Dogs\"\n}", 381 | "displayName": "Category", 382 | "displayOptions": { 383 | "show": { 384 | "operation": [ 385 | "Update Pet" 386 | ], 387 | "resource": [ 388 | "Pet" 389 | ] 390 | } 391 | }, 392 | "name": "category", 393 | "routing": { 394 | "send": { 395 | "property": "category", 396 | "propertyInDotNotation": false, 397 | "type": "body", 398 | "value": "={{ JSON.parse($value) }}" 399 | } 400 | }, 401 | "type": "json" 402 | }, 403 | { 404 | "default": "[\n null\n]", 405 | "displayName": "Photo Urls", 406 | "displayOptions": { 407 | "show": { 408 | "operation": [ 409 | "Update Pet" 410 | ], 411 | "resource": [ 412 | "Pet" 413 | ] 414 | } 415 | }, 416 | "name": "photoUrls", 417 | "required": true, 418 | "routing": { 419 | "send": { 420 | "property": "photoUrls", 421 | "propertyInDotNotation": false, 422 | "type": "body", 423 | "value": "={{ JSON.parse($value) }}" 424 | } 425 | }, 426 | "type": "json" 427 | }, 428 | { 429 | "default": "[\n {}\n]", 430 | "displayName": "Tags", 431 | "displayOptions": { 432 | "show": { 433 | "operation": [ 434 | "Update Pet" 435 | ], 436 | "resource": [ 437 | "Pet" 438 | ] 439 | } 440 | }, 441 | "name": "tags", 442 | "routing": { 443 | "send": { 444 | "property": "tags", 445 | "propertyInDotNotation": false, 446 | "type": "body", 447 | "value": "={{ JSON.parse($value) }}" 448 | } 449 | }, 450 | "type": "json" 451 | }, 452 | { 453 | "default": "available", 454 | "description": "pet status in the store", 455 | "displayName": "Status", 456 | "displayOptions": { 457 | "show": { 458 | "operation": [ 459 | "Update Pet" 460 | ], 461 | "resource": [ 462 | "Pet" 463 | ] 464 | } 465 | }, 466 | "name": "status", 467 | "options": [ 468 | { 469 | "name": "Available", 470 | "value": "available" 471 | }, 472 | { 473 | "name": "Pending", 474 | "value": "pending" 475 | }, 476 | { 477 | "name": "Sold", 478 | "value": "sold" 479 | } 480 | ], 481 | "routing": { 482 | "send": { 483 | "property": "status", 484 | "propertyInDotNotation": false, 485 | "type": "body", 486 | "value": "={{ $value }}" 487 | } 488 | }, 489 | "type": "options" 490 | }, 491 | { 492 | "default": "", 493 | "displayName": "POST /pet", 494 | "displayOptions": { 495 | "show": { 496 | "operation": [ 497 | "Add Pet" 498 | ], 499 | "resource": [ 500 | "Pet" 501 | ] 502 | } 503 | }, 504 | "name": "operation", 505 | "type": "notice", 506 | "typeOptions": { 507 | "theme": "info" 508 | } 509 | }, 510 | { 511 | "default": 10, 512 | "displayName": "Id", 513 | "displayOptions": { 514 | "show": { 515 | "operation": [ 516 | "Add Pet" 517 | ], 518 | "resource": [ 519 | "Pet" 520 | ] 521 | } 522 | }, 523 | "name": "id", 524 | "routing": { 525 | "send": { 526 | "property": "id", 527 | "propertyInDotNotation": false, 528 | "type": "body", 529 | "value": "={{ $value }}" 530 | } 531 | }, 532 | "type": "number" 533 | }, 534 | { 535 | "default": "doggie", 536 | "displayName": "Name", 537 | "displayOptions": { 538 | "show": { 539 | "operation": [ 540 | "Add Pet" 541 | ], 542 | "resource": [ 543 | "Pet" 544 | ] 545 | } 546 | }, 547 | "name": "name", 548 | "required": true, 549 | "routing": { 550 | "send": { 551 | "property": "name", 552 | "propertyInDotNotation": false, 553 | "type": "body", 554 | "value": "={{ $value }}" 555 | } 556 | }, 557 | "type": "string" 558 | }, 559 | { 560 | "default": "{\n \"id\": 1,\n \"name\": \"Dogs\"\n}", 561 | "displayName": "Category", 562 | "displayOptions": { 563 | "show": { 564 | "operation": [ 565 | "Add Pet" 566 | ], 567 | "resource": [ 568 | "Pet" 569 | ] 570 | } 571 | }, 572 | "name": "category", 573 | "routing": { 574 | "send": { 575 | "property": "category", 576 | "propertyInDotNotation": false, 577 | "type": "body", 578 | "value": "={{ JSON.parse($value) }}" 579 | } 580 | }, 581 | "type": "json" 582 | }, 583 | { 584 | "default": "[\n null\n]", 585 | "displayName": "Photo Urls", 586 | "displayOptions": { 587 | "show": { 588 | "operation": [ 589 | "Add Pet" 590 | ], 591 | "resource": [ 592 | "Pet" 593 | ] 594 | } 595 | }, 596 | "name": "photoUrls", 597 | "required": true, 598 | "routing": { 599 | "send": { 600 | "property": "photoUrls", 601 | "propertyInDotNotation": false, 602 | "type": "body", 603 | "value": "={{ JSON.parse($value) }}" 604 | } 605 | }, 606 | "type": "json" 607 | }, 608 | { 609 | "default": "[\n {}\n]", 610 | "displayName": "Tags", 611 | "displayOptions": { 612 | "show": { 613 | "operation": [ 614 | "Add Pet" 615 | ], 616 | "resource": [ 617 | "Pet" 618 | ] 619 | } 620 | }, 621 | "name": "tags", 622 | "routing": { 623 | "send": { 624 | "property": "tags", 625 | "propertyInDotNotation": false, 626 | "type": "body", 627 | "value": "={{ JSON.parse($value) }}" 628 | } 629 | }, 630 | "type": "json" 631 | }, 632 | { 633 | "default": "available", 634 | "description": "pet status in the store", 635 | "displayName": "Status", 636 | "displayOptions": { 637 | "show": { 638 | "operation": [ 639 | "Add Pet" 640 | ], 641 | "resource": [ 642 | "Pet" 643 | ] 644 | } 645 | }, 646 | "name": "status", 647 | "options": [ 648 | { 649 | "name": "Available", 650 | "value": "available" 651 | }, 652 | { 653 | "name": "Pending", 654 | "value": "pending" 655 | }, 656 | { 657 | "name": "Sold", 658 | "value": "sold" 659 | } 660 | ], 661 | "routing": { 662 | "send": { 663 | "property": "status", 664 | "propertyInDotNotation": false, 665 | "type": "body", 666 | "value": "={{ $value }}" 667 | } 668 | }, 669 | "type": "options" 670 | }, 671 | { 672 | "default": "", 673 | "displayName": "GET /pet/findByStatus", 674 | "displayOptions": { 675 | "show": { 676 | "operation": [ 677 | "Find Pets By Status" 678 | ], 679 | "resource": [ 680 | "Pet" 681 | ] 682 | } 683 | }, 684 | "name": "operation", 685 | "type": "notice", 686 | "typeOptions": { 687 | "theme": "info" 688 | } 689 | }, 690 | { 691 | "default": "available", 692 | "description": "Status values that need to be considered for filter", 693 | "displayName": "Status", 694 | "displayOptions": { 695 | "show": { 696 | "operation": [ 697 | "Find Pets By Status" 698 | ], 699 | "resource": [ 700 | "Pet" 701 | ] 702 | } 703 | }, 704 | "name": "status", 705 | "options": [ 706 | { 707 | "name": "Available", 708 | "value": "available" 709 | }, 710 | { 711 | "name": "Pending", 712 | "value": "pending" 713 | }, 714 | { 715 | "name": "Sold", 716 | "value": "sold" 717 | } 718 | ], 719 | "routing": { 720 | "send": { 721 | "property": "status", 722 | "propertyInDotNotation": false, 723 | "type": "query", 724 | "value": "={{ $value }}" 725 | } 726 | }, 727 | "type": "options" 728 | }, 729 | { 730 | "default": "", 731 | "displayName": "GET /pet/findByTags", 732 | "displayOptions": { 733 | "show": { 734 | "operation": [ 735 | "Find Pets By Tags" 736 | ], 737 | "resource": [ 738 | "Pet" 739 | ] 740 | } 741 | }, 742 | "name": "operation", 743 | "type": "notice", 744 | "typeOptions": { 745 | "theme": "info" 746 | } 747 | }, 748 | { 749 | "default": "[\n null\n]", 750 | "description": "Tags to filter by", 751 | "displayName": "Tags", 752 | "displayOptions": { 753 | "show": { 754 | "operation": [ 755 | "Find Pets By Tags" 756 | ], 757 | "resource": [ 758 | "Pet" 759 | ] 760 | } 761 | }, 762 | "name": "tags", 763 | "routing": { 764 | "send": { 765 | "property": "tags", 766 | "propertyInDotNotation": false, 767 | "type": "query", 768 | "value": "={{ $value }}" 769 | } 770 | }, 771 | "type": "json" 772 | }, 773 | { 774 | "default": "", 775 | "displayName": "GET /pet/{petId}", 776 | "displayOptions": { 777 | "show": { 778 | "operation": [ 779 | "Get Pet By Id" 780 | ], 781 | "resource": [ 782 | "Pet" 783 | ] 784 | } 785 | }, 786 | "name": "operation", 787 | "type": "notice", 788 | "typeOptions": { 789 | "theme": "info" 790 | } 791 | }, 792 | { 793 | "default": 0, 794 | "description": "ID of pet to return", 795 | "displayName": "Pet Id", 796 | "displayOptions": { 797 | "show": { 798 | "operation": [ 799 | "Get Pet By Id" 800 | ], 801 | "resource": [ 802 | "Pet" 803 | ] 804 | } 805 | }, 806 | "name": "petId", 807 | "required": true, 808 | "type": "number" 809 | }, 810 | { 811 | "default": "", 812 | "displayName": "POST /pet/{petId}", 813 | "displayOptions": { 814 | "show": { 815 | "operation": [ 816 | "Update Pet With Form" 817 | ], 818 | "resource": [ 819 | "Pet" 820 | ] 821 | } 822 | }, 823 | "name": "operation", 824 | "type": "notice", 825 | "typeOptions": { 826 | "theme": "info" 827 | } 828 | }, 829 | { 830 | "default": 0, 831 | "description": "ID of pet that needs to be updated", 832 | "displayName": "Pet Id", 833 | "displayOptions": { 834 | "show": { 835 | "operation": [ 836 | "Update Pet With Form" 837 | ], 838 | "resource": [ 839 | "Pet" 840 | ] 841 | } 842 | }, 843 | "name": "petId", 844 | "required": true, 845 | "type": "number" 846 | }, 847 | { 848 | "default": "", 849 | "description": "Name of pet that needs to be updated", 850 | "displayName": "Name", 851 | "displayOptions": { 852 | "show": { 853 | "operation": [ 854 | "Update Pet With Form" 855 | ], 856 | "resource": [ 857 | "Pet" 858 | ] 859 | } 860 | }, 861 | "name": "name", 862 | "routing": { 863 | "send": { 864 | "property": "name", 865 | "propertyInDotNotation": false, 866 | "type": "query", 867 | "value": "={{ $value }}" 868 | } 869 | }, 870 | "type": "string" 871 | }, 872 | { 873 | "default": "", 874 | "description": "Status of pet that needs to be updated", 875 | "displayName": "Status", 876 | "displayOptions": { 877 | "show": { 878 | "operation": [ 879 | "Update Pet With Form" 880 | ], 881 | "resource": [ 882 | "Pet" 883 | ] 884 | } 885 | }, 886 | "name": "status", 887 | "routing": { 888 | "send": { 889 | "property": "status", 890 | "propertyInDotNotation": false, 891 | "type": "query", 892 | "value": "={{ $value }}" 893 | } 894 | }, 895 | "type": "string" 896 | }, 897 | { 898 | "default": "", 899 | "displayName": "DELETE /pet/{petId}", 900 | "displayOptions": { 901 | "show": { 902 | "operation": [ 903 | "Delete Pet" 904 | ], 905 | "resource": [ 906 | "Pet" 907 | ] 908 | } 909 | }, 910 | "name": "operation", 911 | "type": "notice", 912 | "typeOptions": { 913 | "theme": "info" 914 | } 915 | }, 916 | { 917 | "default": "", 918 | "description": "", 919 | "displayName": "Api Key", 920 | "displayOptions": { 921 | "show": { 922 | "operation": [ 923 | "Delete Pet" 924 | ], 925 | "resource": [ 926 | "Pet" 927 | ] 928 | } 929 | }, 930 | "name": "api_key", 931 | "routing": { 932 | "request": { 933 | "headers": { 934 | "api_key": "={{ $value }}" 935 | } 936 | } 937 | }, 938 | "type": "string" 939 | }, 940 | { 941 | "default": 0, 942 | "description": "Pet id to delete", 943 | "displayName": "Pet Id", 944 | "displayOptions": { 945 | "show": { 946 | "operation": [ 947 | "Delete Pet" 948 | ], 949 | "resource": [ 950 | "Pet" 951 | ] 952 | } 953 | }, 954 | "name": "petId", 955 | "required": true, 956 | "type": "number" 957 | }, 958 | { 959 | "default": "", 960 | "displayName": "POST /pet/{petId}/uploadImage", 961 | "displayOptions": { 962 | "show": { 963 | "operation": [ 964 | "Upload File" 965 | ], 966 | "resource": [ 967 | "Pet" 968 | ] 969 | } 970 | }, 971 | "name": "operation", 972 | "type": "notice", 973 | "typeOptions": { 974 | "theme": "info" 975 | } 976 | }, 977 | { 978 | "default": 0, 979 | "description": "ID of pet to update", 980 | "displayName": "Pet Id", 981 | "displayOptions": { 982 | "show": { 983 | "operation": [ 984 | "Upload File" 985 | ], 986 | "resource": [ 987 | "Pet" 988 | ] 989 | } 990 | }, 991 | "name": "petId", 992 | "required": true, 993 | "type": "number" 994 | }, 995 | { 996 | "default": "", 997 | "description": "Additional Metadata", 998 | "displayName": "Additional Metadata", 999 | "displayOptions": { 1000 | "show": { 1001 | "operation": [ 1002 | "Upload File" 1003 | ], 1004 | "resource": [ 1005 | "Pet" 1006 | ] 1007 | } 1008 | }, 1009 | "name": "additionalMetadata", 1010 | "routing": { 1011 | "send": { 1012 | "property": "additionalMetadata", 1013 | "propertyInDotNotation": false, 1014 | "type": "query", 1015 | "value": "={{ $value }}" 1016 | } 1017 | }, 1018 | "type": "string" 1019 | }, 1020 | { 1021 | "default": "", 1022 | "displayName": "POST /pet/{petId}/uploadImage

There's no body available for request, kindly use HTTP Request node to send body", 1023 | "displayOptions": { 1024 | "show": { 1025 | "operation": [ 1026 | "Upload File" 1027 | ], 1028 | "resource": [ 1029 | "Pet" 1030 | ] 1031 | } 1032 | }, 1033 | "name": "operation", 1034 | "type": "notice" 1035 | }, 1036 | { 1037 | "default": "", 1038 | "displayName": "GET /store/inventory", 1039 | "displayOptions": { 1040 | "show": { 1041 | "operation": [ 1042 | "Get Inventory" 1043 | ], 1044 | "resource": [ 1045 | "Store" 1046 | ] 1047 | } 1048 | }, 1049 | "name": "operation", 1050 | "type": "notice", 1051 | "typeOptions": { 1052 | "theme": "info" 1053 | } 1054 | }, 1055 | { 1056 | "default": "", 1057 | "displayName": "POST /store/order", 1058 | "displayOptions": { 1059 | "show": { 1060 | "operation": [ 1061 | "Place Order" 1062 | ], 1063 | "resource": [ 1064 | "Store" 1065 | ] 1066 | } 1067 | }, 1068 | "name": "operation", 1069 | "type": "notice", 1070 | "typeOptions": { 1071 | "theme": "info" 1072 | } 1073 | }, 1074 | { 1075 | "default": 10, 1076 | "displayName": "Id", 1077 | "displayOptions": { 1078 | "show": { 1079 | "operation": [ 1080 | "Place Order" 1081 | ], 1082 | "resource": [ 1083 | "Store" 1084 | ] 1085 | } 1086 | }, 1087 | "name": "id", 1088 | "routing": { 1089 | "send": { 1090 | "property": "id", 1091 | "propertyInDotNotation": false, 1092 | "type": "body", 1093 | "value": "={{ $value }}" 1094 | } 1095 | }, 1096 | "type": "number" 1097 | }, 1098 | { 1099 | "default": 198772, 1100 | "displayName": "Pet Id", 1101 | "displayOptions": { 1102 | "show": { 1103 | "operation": [ 1104 | "Place Order" 1105 | ], 1106 | "resource": [ 1107 | "Store" 1108 | ] 1109 | } 1110 | }, 1111 | "name": "petId", 1112 | "routing": { 1113 | "send": { 1114 | "property": "petId", 1115 | "propertyInDotNotation": false, 1116 | "type": "body", 1117 | "value": "={{ $value }}" 1118 | } 1119 | }, 1120 | "type": "number" 1121 | }, 1122 | { 1123 | "default": 7, 1124 | "displayName": "Quantity", 1125 | "displayOptions": { 1126 | "show": { 1127 | "operation": [ 1128 | "Place Order" 1129 | ], 1130 | "resource": [ 1131 | "Store" 1132 | ] 1133 | } 1134 | }, 1135 | "name": "quantity", 1136 | "routing": { 1137 | "send": { 1138 | "property": "quantity", 1139 | "propertyInDotNotation": false, 1140 | "type": "body", 1141 | "value": "={{ $value }}" 1142 | } 1143 | }, 1144 | "type": "number" 1145 | }, 1146 | { 1147 | "default": "", 1148 | "displayName": "Ship Date", 1149 | "displayOptions": { 1150 | "show": { 1151 | "operation": [ 1152 | "Place Order" 1153 | ], 1154 | "resource": [ 1155 | "Store" 1156 | ] 1157 | } 1158 | }, 1159 | "name": "shipDate", 1160 | "routing": { 1161 | "send": { 1162 | "property": "shipDate", 1163 | "propertyInDotNotation": false, 1164 | "type": "body", 1165 | "value": "={{ $value }}" 1166 | } 1167 | }, 1168 | "type": "string" 1169 | }, 1170 | { 1171 | "default": "approved", 1172 | "description": "Order Status", 1173 | "displayName": "Status", 1174 | "displayOptions": { 1175 | "show": { 1176 | "operation": [ 1177 | "Place Order" 1178 | ], 1179 | "resource": [ 1180 | "Store" 1181 | ] 1182 | } 1183 | }, 1184 | "name": "status", 1185 | "options": [ 1186 | { 1187 | "name": "Placed", 1188 | "value": "placed" 1189 | }, 1190 | { 1191 | "name": "Approved", 1192 | "value": "approved" 1193 | }, 1194 | { 1195 | "name": "Delivered", 1196 | "value": "delivered" 1197 | } 1198 | ], 1199 | "routing": { 1200 | "send": { 1201 | "property": "status", 1202 | "propertyInDotNotation": false, 1203 | "type": "body", 1204 | "value": "={{ $value }}" 1205 | } 1206 | }, 1207 | "type": "options" 1208 | }, 1209 | { 1210 | "default": true, 1211 | "displayName": "Complete", 1212 | "displayOptions": { 1213 | "show": { 1214 | "operation": [ 1215 | "Place Order" 1216 | ], 1217 | "resource": [ 1218 | "Store" 1219 | ] 1220 | } 1221 | }, 1222 | "name": "complete", 1223 | "routing": { 1224 | "send": { 1225 | "property": "complete", 1226 | "propertyInDotNotation": false, 1227 | "type": "body", 1228 | "value": "={{ $value }}" 1229 | } 1230 | }, 1231 | "type": "boolean" 1232 | }, 1233 | { 1234 | "default": "", 1235 | "displayName": "GET /store/order/{orderId}", 1236 | "displayOptions": { 1237 | "show": { 1238 | "operation": [ 1239 | "Get Order By Id" 1240 | ], 1241 | "resource": [ 1242 | "Store" 1243 | ] 1244 | } 1245 | }, 1246 | "name": "operation", 1247 | "type": "notice", 1248 | "typeOptions": { 1249 | "theme": "info" 1250 | } 1251 | }, 1252 | { 1253 | "default": 0, 1254 | "description": "ID of order that needs to be fetched", 1255 | "displayName": "Order Id", 1256 | "displayOptions": { 1257 | "show": { 1258 | "operation": [ 1259 | "Get Order By Id" 1260 | ], 1261 | "resource": [ 1262 | "Store" 1263 | ] 1264 | } 1265 | }, 1266 | "name": "orderId", 1267 | "required": true, 1268 | "type": "number" 1269 | }, 1270 | { 1271 | "default": "", 1272 | "displayName": "DELETE /store/order/{orderId}", 1273 | "displayOptions": { 1274 | "show": { 1275 | "operation": [ 1276 | "Delete Order" 1277 | ], 1278 | "resource": [ 1279 | "Store" 1280 | ] 1281 | } 1282 | }, 1283 | "name": "operation", 1284 | "type": "notice", 1285 | "typeOptions": { 1286 | "theme": "info" 1287 | } 1288 | }, 1289 | { 1290 | "default": 0, 1291 | "description": "ID of the order that needs to be deleted", 1292 | "displayName": "Order Id", 1293 | "displayOptions": { 1294 | "show": { 1295 | "operation": [ 1296 | "Delete Order" 1297 | ], 1298 | "resource": [ 1299 | "Store" 1300 | ] 1301 | } 1302 | }, 1303 | "name": "orderId", 1304 | "required": true, 1305 | "type": "number" 1306 | }, 1307 | { 1308 | "default": "", 1309 | "displayName": "POST /user", 1310 | "displayOptions": { 1311 | "show": { 1312 | "operation": [ 1313 | "Create User" 1314 | ], 1315 | "resource": [ 1316 | "User" 1317 | ] 1318 | } 1319 | }, 1320 | "name": "operation", 1321 | "type": "notice", 1322 | "typeOptions": { 1323 | "theme": "info" 1324 | } 1325 | }, 1326 | { 1327 | "default": 10, 1328 | "displayName": "Id", 1329 | "displayOptions": { 1330 | "show": { 1331 | "operation": [ 1332 | "Create User" 1333 | ], 1334 | "resource": [ 1335 | "User" 1336 | ] 1337 | } 1338 | }, 1339 | "name": "id", 1340 | "routing": { 1341 | "send": { 1342 | "property": "id", 1343 | "propertyInDotNotation": false, 1344 | "type": "body", 1345 | "value": "={{ $value }}" 1346 | } 1347 | }, 1348 | "type": "number" 1349 | }, 1350 | { 1351 | "default": "theUser", 1352 | "displayName": "Username", 1353 | "displayOptions": { 1354 | "show": { 1355 | "operation": [ 1356 | "Create User" 1357 | ], 1358 | "resource": [ 1359 | "User" 1360 | ] 1361 | } 1362 | }, 1363 | "name": "username", 1364 | "routing": { 1365 | "send": { 1366 | "property": "username", 1367 | "propertyInDotNotation": false, 1368 | "type": "body", 1369 | "value": "={{ $value }}" 1370 | } 1371 | }, 1372 | "type": "string" 1373 | }, 1374 | { 1375 | "default": "John", 1376 | "displayName": "First Name", 1377 | "displayOptions": { 1378 | "show": { 1379 | "operation": [ 1380 | "Create User" 1381 | ], 1382 | "resource": [ 1383 | "User" 1384 | ] 1385 | } 1386 | }, 1387 | "name": "firstName", 1388 | "routing": { 1389 | "send": { 1390 | "property": "firstName", 1391 | "propertyInDotNotation": false, 1392 | "type": "body", 1393 | "value": "={{ $value }}" 1394 | } 1395 | }, 1396 | "type": "string" 1397 | }, 1398 | { 1399 | "default": "James", 1400 | "displayName": "Last Name", 1401 | "displayOptions": { 1402 | "show": { 1403 | "operation": [ 1404 | "Create User" 1405 | ], 1406 | "resource": [ 1407 | "User" 1408 | ] 1409 | } 1410 | }, 1411 | "name": "lastName", 1412 | "routing": { 1413 | "send": { 1414 | "property": "lastName", 1415 | "propertyInDotNotation": false, 1416 | "type": "body", 1417 | "value": "={{ $value }}" 1418 | } 1419 | }, 1420 | "type": "string" 1421 | }, 1422 | { 1423 | "default": "john@email.com", 1424 | "displayName": "Email", 1425 | "displayOptions": { 1426 | "show": { 1427 | "operation": [ 1428 | "Create User" 1429 | ], 1430 | "resource": [ 1431 | "User" 1432 | ] 1433 | } 1434 | }, 1435 | "name": "email", 1436 | "routing": { 1437 | "send": { 1438 | "property": "email", 1439 | "propertyInDotNotation": false, 1440 | "type": "body", 1441 | "value": "={{ $value }}" 1442 | } 1443 | }, 1444 | "type": "string" 1445 | }, 1446 | { 1447 | "default": "12345", 1448 | "displayName": "Password", 1449 | "displayOptions": { 1450 | "show": { 1451 | "operation": [ 1452 | "Create User" 1453 | ], 1454 | "resource": [ 1455 | "User" 1456 | ] 1457 | } 1458 | }, 1459 | "name": "password", 1460 | "routing": { 1461 | "send": { 1462 | "property": "password", 1463 | "propertyInDotNotation": false, 1464 | "type": "body", 1465 | "value": "={{ $value }}" 1466 | } 1467 | }, 1468 | "type": "string" 1469 | }, 1470 | { 1471 | "default": "12345", 1472 | "displayName": "Phone", 1473 | "displayOptions": { 1474 | "show": { 1475 | "operation": [ 1476 | "Create User" 1477 | ], 1478 | "resource": [ 1479 | "User" 1480 | ] 1481 | } 1482 | }, 1483 | "name": "phone", 1484 | "routing": { 1485 | "send": { 1486 | "property": "phone", 1487 | "propertyInDotNotation": false, 1488 | "type": "body", 1489 | "value": "={{ $value }}" 1490 | } 1491 | }, 1492 | "type": "string" 1493 | }, 1494 | { 1495 | "default": 1, 1496 | "description": "User Status", 1497 | "displayName": "User Status", 1498 | "displayOptions": { 1499 | "show": { 1500 | "operation": [ 1501 | "Create User" 1502 | ], 1503 | "resource": [ 1504 | "User" 1505 | ] 1506 | } 1507 | }, 1508 | "name": "userStatus", 1509 | "routing": { 1510 | "send": { 1511 | "property": "userStatus", 1512 | "propertyInDotNotation": false, 1513 | "type": "body", 1514 | "value": "={{ $value }}" 1515 | } 1516 | }, 1517 | "type": "number" 1518 | }, 1519 | { 1520 | "default": "", 1521 | "displayName": "POST /user/createWithList", 1522 | "displayOptions": { 1523 | "show": { 1524 | "operation": [ 1525 | "Create Users With List Input" 1526 | ], 1527 | "resource": [ 1528 | "User" 1529 | ] 1530 | } 1531 | }, 1532 | "name": "operation", 1533 | "type": "notice", 1534 | "typeOptions": { 1535 | "theme": "info" 1536 | } 1537 | }, 1538 | { 1539 | "default": "{\n \"id\": 10,\n \"username\": \"theUser\",\n \"firstName\": \"John\",\n \"lastName\": \"James\",\n \"email\": \"john@email.com\",\n \"password\": \"12345\",\n \"phone\": \"12345\",\n \"userStatus\": 1\n}", 1540 | "displayName": "Body", 1541 | "displayOptions": { 1542 | "show": { 1543 | "operation": [ 1544 | "Create Users With List Input" 1545 | ], 1546 | "resource": [ 1547 | "User" 1548 | ] 1549 | } 1550 | }, 1551 | "name": "body", 1552 | "routing": { 1553 | "request": { 1554 | "body": "={{ JSON.parse($value) }}" 1555 | } 1556 | }, 1557 | "type": "json" 1558 | }, 1559 | { 1560 | "default": "", 1561 | "displayName": "GET /user/login", 1562 | "displayOptions": { 1563 | "show": { 1564 | "operation": [ 1565 | "Login User" 1566 | ], 1567 | "resource": [ 1568 | "User" 1569 | ] 1570 | } 1571 | }, 1572 | "name": "operation", 1573 | "type": "notice", 1574 | "typeOptions": { 1575 | "theme": "info" 1576 | } 1577 | }, 1578 | { 1579 | "default": "", 1580 | "description": "The user name for login", 1581 | "displayName": "Username", 1582 | "displayOptions": { 1583 | "show": { 1584 | "operation": [ 1585 | "Login User" 1586 | ], 1587 | "resource": [ 1588 | "User" 1589 | ] 1590 | } 1591 | }, 1592 | "name": "username", 1593 | "routing": { 1594 | "send": { 1595 | "property": "username", 1596 | "propertyInDotNotation": false, 1597 | "type": "query", 1598 | "value": "={{ $value }}" 1599 | } 1600 | }, 1601 | "type": "string" 1602 | }, 1603 | { 1604 | "default": "", 1605 | "description": "The password for login in clear text", 1606 | "displayName": "Password", 1607 | "displayOptions": { 1608 | "show": { 1609 | "operation": [ 1610 | "Login User" 1611 | ], 1612 | "resource": [ 1613 | "User" 1614 | ] 1615 | } 1616 | }, 1617 | "name": "password", 1618 | "routing": { 1619 | "send": { 1620 | "property": "password", 1621 | "propertyInDotNotation": false, 1622 | "type": "query", 1623 | "value": "={{ $value }}" 1624 | } 1625 | }, 1626 | "type": "string" 1627 | }, 1628 | { 1629 | "default": "", 1630 | "displayName": "GET /user/logout", 1631 | "displayOptions": { 1632 | "show": { 1633 | "operation": [ 1634 | "Logout User" 1635 | ], 1636 | "resource": [ 1637 | "User" 1638 | ] 1639 | } 1640 | }, 1641 | "name": "operation", 1642 | "type": "notice", 1643 | "typeOptions": { 1644 | "theme": "info" 1645 | } 1646 | }, 1647 | { 1648 | "default": "", 1649 | "displayName": "GET /user/{username}", 1650 | "displayOptions": { 1651 | "show": { 1652 | "operation": [ 1653 | "Get User By Name" 1654 | ], 1655 | "resource": [ 1656 | "User" 1657 | ] 1658 | } 1659 | }, 1660 | "name": "operation", 1661 | "type": "notice", 1662 | "typeOptions": { 1663 | "theme": "info" 1664 | } 1665 | }, 1666 | { 1667 | "default": "", 1668 | "description": "The name that needs to be fetched. Use user1 for testing. ", 1669 | "displayName": "Username", 1670 | "displayOptions": { 1671 | "show": { 1672 | "operation": [ 1673 | "Get User By Name" 1674 | ], 1675 | "resource": [ 1676 | "User" 1677 | ] 1678 | } 1679 | }, 1680 | "name": "username", 1681 | "required": true, 1682 | "type": "string" 1683 | }, 1684 | { 1685 | "default": "", 1686 | "displayName": "PUT /user/{username}", 1687 | "displayOptions": { 1688 | "show": { 1689 | "operation": [ 1690 | "Update User" 1691 | ], 1692 | "resource": [ 1693 | "User" 1694 | ] 1695 | } 1696 | }, 1697 | "name": "operation", 1698 | "type": "notice", 1699 | "typeOptions": { 1700 | "theme": "info" 1701 | } 1702 | }, 1703 | { 1704 | "default": "", 1705 | "description": "name that needs to be updated", 1706 | "displayName": "Username", 1707 | "displayOptions": { 1708 | "show": { 1709 | "operation": [ 1710 | "Update User" 1711 | ], 1712 | "resource": [ 1713 | "User" 1714 | ] 1715 | } 1716 | }, 1717 | "name": "username", 1718 | "required": true, 1719 | "type": "string" 1720 | }, 1721 | { 1722 | "default": 10, 1723 | "displayName": "Id", 1724 | "displayOptions": { 1725 | "show": { 1726 | "operation": [ 1727 | "Update User" 1728 | ], 1729 | "resource": [ 1730 | "User" 1731 | ] 1732 | } 1733 | }, 1734 | "name": "id", 1735 | "routing": { 1736 | "send": { 1737 | "property": "id", 1738 | "propertyInDotNotation": false, 1739 | "type": "body", 1740 | "value": "={{ $value }}" 1741 | } 1742 | }, 1743 | "type": "number" 1744 | }, 1745 | { 1746 | "default": "theUser", 1747 | "displayName": "Username", 1748 | "displayOptions": { 1749 | "show": { 1750 | "operation": [ 1751 | "Update User" 1752 | ], 1753 | "resource": [ 1754 | "User" 1755 | ] 1756 | } 1757 | }, 1758 | "name": "username", 1759 | "routing": { 1760 | "send": { 1761 | "property": "username", 1762 | "propertyInDotNotation": false, 1763 | "type": "body", 1764 | "value": "={{ $value }}" 1765 | } 1766 | }, 1767 | "type": "string" 1768 | }, 1769 | { 1770 | "default": "John", 1771 | "displayName": "First Name", 1772 | "displayOptions": { 1773 | "show": { 1774 | "operation": [ 1775 | "Update User" 1776 | ], 1777 | "resource": [ 1778 | "User" 1779 | ] 1780 | } 1781 | }, 1782 | "name": "firstName", 1783 | "routing": { 1784 | "send": { 1785 | "property": "firstName", 1786 | "propertyInDotNotation": false, 1787 | "type": "body", 1788 | "value": "={{ $value }}" 1789 | } 1790 | }, 1791 | "type": "string" 1792 | }, 1793 | { 1794 | "default": "James", 1795 | "displayName": "Last Name", 1796 | "displayOptions": { 1797 | "show": { 1798 | "operation": [ 1799 | "Update User" 1800 | ], 1801 | "resource": [ 1802 | "User" 1803 | ] 1804 | } 1805 | }, 1806 | "name": "lastName", 1807 | "routing": { 1808 | "send": { 1809 | "property": "lastName", 1810 | "propertyInDotNotation": false, 1811 | "type": "body", 1812 | "value": "={{ $value }}" 1813 | } 1814 | }, 1815 | "type": "string" 1816 | }, 1817 | { 1818 | "default": "john@email.com", 1819 | "displayName": "Email", 1820 | "displayOptions": { 1821 | "show": { 1822 | "operation": [ 1823 | "Update User" 1824 | ], 1825 | "resource": [ 1826 | "User" 1827 | ] 1828 | } 1829 | }, 1830 | "name": "email", 1831 | "routing": { 1832 | "send": { 1833 | "property": "email", 1834 | "propertyInDotNotation": false, 1835 | "type": "body", 1836 | "value": "={{ $value }}" 1837 | } 1838 | }, 1839 | "type": "string" 1840 | }, 1841 | { 1842 | "default": "12345", 1843 | "displayName": "Password", 1844 | "displayOptions": { 1845 | "show": { 1846 | "operation": [ 1847 | "Update User" 1848 | ], 1849 | "resource": [ 1850 | "User" 1851 | ] 1852 | } 1853 | }, 1854 | "name": "password", 1855 | "routing": { 1856 | "send": { 1857 | "property": "password", 1858 | "propertyInDotNotation": false, 1859 | "type": "body", 1860 | "value": "={{ $value }}" 1861 | } 1862 | }, 1863 | "type": "string" 1864 | }, 1865 | { 1866 | "default": "12345", 1867 | "displayName": "Phone", 1868 | "displayOptions": { 1869 | "show": { 1870 | "operation": [ 1871 | "Update User" 1872 | ], 1873 | "resource": [ 1874 | "User" 1875 | ] 1876 | } 1877 | }, 1878 | "name": "phone", 1879 | "routing": { 1880 | "send": { 1881 | "property": "phone", 1882 | "propertyInDotNotation": false, 1883 | "type": "body", 1884 | "value": "={{ $value }}" 1885 | } 1886 | }, 1887 | "type": "string" 1888 | }, 1889 | { 1890 | "default": 1, 1891 | "description": "User Status", 1892 | "displayName": "User Status", 1893 | "displayOptions": { 1894 | "show": { 1895 | "operation": [ 1896 | "Update User" 1897 | ], 1898 | "resource": [ 1899 | "User" 1900 | ] 1901 | } 1902 | }, 1903 | "name": "userStatus", 1904 | "routing": { 1905 | "send": { 1906 | "property": "userStatus", 1907 | "propertyInDotNotation": false, 1908 | "type": "body", 1909 | "value": "={{ $value }}" 1910 | } 1911 | }, 1912 | "type": "number" 1913 | }, 1914 | { 1915 | "default": "", 1916 | "displayName": "DELETE /user/{username}", 1917 | "displayOptions": { 1918 | "show": { 1919 | "operation": [ 1920 | "Delete User" 1921 | ], 1922 | "resource": [ 1923 | "User" 1924 | ] 1925 | } 1926 | }, 1927 | "name": "operation", 1928 | "type": "notice", 1929 | "typeOptions": { 1930 | "theme": "info" 1931 | } 1932 | }, 1933 | { 1934 | "default": "", 1935 | "description": "The name that needs to be deleted", 1936 | "displayName": "Username", 1937 | "displayOptions": { 1938 | "show": { 1939 | "operation": [ 1940 | "Delete User" 1941 | ], 1942 | "resource": [ 1943 | "User" 1944 | ] 1945 | } 1946 | }, 1947 | "name": "username", 1948 | "required": true, 1949 | "type": "string" 1950 | } 1951 | ] 1952 | expect(result).toEqual(expected); 1953 | }) 1954 | -------------------------------------------------------------------------------- /tests/samples/petstore.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.2", 3 | "info": { 4 | "title": "Swagger Petstore - OpenAPI 3.0", 5 | "description": "This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about\nSwagger at [http://swagger.io](http://swagger.io). In the third iteration of the pet store, we've switched to the design first approach!\nYou can now help us improve the API whether it's by making changes to the definition itself or to the code.\nThat way, with time, we can improve the API in general, and expose some of the new features in OAS3.\n\nSome useful links:\n- [The Pet Store repository](https://github.com/swagger-api/swagger-petstore)\n- [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml)", 6 | "termsOfService": "http://swagger.io/terms/", 7 | "contact": { 8 | "email": "apiteam@swagger.io" 9 | }, 10 | "license": { 11 | "name": "Apache 2.0", 12 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 13 | }, 14 | "version": "1.0.19" 15 | }, 16 | "externalDocs": { 17 | "description": "Find out more about Swagger", 18 | "url": "http://swagger.io" 19 | }, 20 | "servers": [ 21 | { 22 | "url": "/api/v3" 23 | } 24 | ], 25 | "tags": [ 26 | { 27 | "name": "pet", 28 | "description": "Everything about your Pets", 29 | "externalDocs": { 30 | "description": "Find out more", 31 | "url": "http://swagger.io" 32 | } 33 | }, 34 | { 35 | "name": "store", 36 | "description": "Access to Petstore orders", 37 | "externalDocs": { 38 | "description": "Find out more about our store", 39 | "url": "http://swagger.io" 40 | } 41 | }, 42 | { 43 | "name": "user", 44 | "description": "Operations about user" 45 | } 46 | ], 47 | "paths": { 48 | "/pet": { 49 | "put": { 50 | "tags": [ 51 | "pet" 52 | ], 53 | "summary": "Update an existing pet", 54 | "description": "Update an existing pet by Id", 55 | "operationId": "updatePet", 56 | "requestBody": { 57 | "description": "Update an existent pet in the store", 58 | "content": { 59 | "application/json": { 60 | "schema": { 61 | "$ref": "#/components/schemas/Pet" 62 | } 63 | }, 64 | "application/xml": { 65 | "schema": { 66 | "$ref": "#/components/schemas/Pet" 67 | } 68 | }, 69 | "application/x-www-form-urlencoded": { 70 | "schema": { 71 | "$ref": "#/components/schemas/Pet" 72 | } 73 | } 74 | }, 75 | "required": true 76 | }, 77 | "responses": { 78 | "200": { 79 | "description": "Successful operation", 80 | "content": { 81 | "application/xml": { 82 | "schema": { 83 | "$ref": "#/components/schemas/Pet" 84 | } 85 | }, 86 | "application/json": { 87 | "schema": { 88 | "$ref": "#/components/schemas/Pet" 89 | } 90 | } 91 | } 92 | }, 93 | "400": { 94 | "description": "Invalid ID supplied" 95 | }, 96 | "404": { 97 | "description": "Pet not found" 98 | }, 99 | "405": { 100 | "description": "Validation exception" 101 | } 102 | }, 103 | "security": [ 104 | { 105 | "petstore_auth": [ 106 | "write:pets", 107 | "read:pets" 108 | ] 109 | } 110 | ] 111 | }, 112 | "post": { 113 | "tags": [ 114 | "pet" 115 | ], 116 | "summary": "Add a new pet to the store", 117 | "description": "Add a new pet to the store", 118 | "operationId": "addPet", 119 | "requestBody": { 120 | "description": "Create a new pet in the store", 121 | "content": { 122 | "application/json": { 123 | "schema": { 124 | "$ref": "#/components/schemas/Pet" 125 | } 126 | }, 127 | "application/xml": { 128 | "schema": { 129 | "$ref": "#/components/schemas/Pet" 130 | } 131 | }, 132 | "application/x-www-form-urlencoded": { 133 | "schema": { 134 | "$ref": "#/components/schemas/Pet" 135 | } 136 | } 137 | }, 138 | "required": true 139 | }, 140 | "responses": { 141 | "200": { 142 | "description": "Successful operation", 143 | "content": { 144 | "application/xml": { 145 | "schema": { 146 | "$ref": "#/components/schemas/Pet" 147 | } 148 | }, 149 | "application/json": { 150 | "schema": { 151 | "$ref": "#/components/schemas/Pet" 152 | } 153 | } 154 | } 155 | }, 156 | "405": { 157 | "description": "Invalid input" 158 | } 159 | }, 160 | "security": [ 161 | { 162 | "petstore_auth": [ 163 | "write:pets", 164 | "read:pets" 165 | ] 166 | } 167 | ] 168 | } 169 | }, 170 | "/pet/findByStatus": { 171 | "get": { 172 | "tags": [ 173 | "pet" 174 | ], 175 | "summary": "Finds Pets by status", 176 | "description": "Multiple status values can be provided with comma separated strings", 177 | "operationId": "findPetsByStatus", 178 | "parameters": [ 179 | { 180 | "name": "status", 181 | "in": "query", 182 | "description": "Status values that need to be considered for filter", 183 | "required": false, 184 | "explode": true, 185 | "schema": { 186 | "type": "string", 187 | "default": "available", 188 | "enum": [ 189 | "available", 190 | "pending", 191 | "sold" 192 | ] 193 | } 194 | } 195 | ], 196 | "responses": { 197 | "200": { 198 | "description": "successful operation", 199 | "content": { 200 | "application/xml": { 201 | "schema": { 202 | "type": "array", 203 | "items": { 204 | "$ref": "#/components/schemas/Pet" 205 | } 206 | } 207 | }, 208 | "application/json": { 209 | "schema": { 210 | "type": "array", 211 | "items": { 212 | "$ref": "#/components/schemas/Pet" 213 | } 214 | } 215 | } 216 | } 217 | }, 218 | "400": { 219 | "description": "Invalid status value" 220 | } 221 | }, 222 | "security": [ 223 | { 224 | "petstore_auth": [ 225 | "write:pets", 226 | "read:pets" 227 | ] 228 | } 229 | ] 230 | } 231 | }, 232 | "/pet/findByTags": { 233 | "get": { 234 | "tags": [ 235 | "pet" 236 | ], 237 | "summary": "Finds Pets by tags", 238 | "description": "Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.", 239 | "operationId": "findPetsByTags", 240 | "parameters": [ 241 | { 242 | "name": "tags", 243 | "in": "query", 244 | "description": "Tags to filter by", 245 | "required": false, 246 | "explode": true, 247 | "schema": { 248 | "type": "array", 249 | "items": { 250 | "type": "string" 251 | } 252 | } 253 | } 254 | ], 255 | "responses": { 256 | "200": { 257 | "description": "successful operation", 258 | "content": { 259 | "application/xml": { 260 | "schema": { 261 | "type": "array", 262 | "items": { 263 | "$ref": "#/components/schemas/Pet" 264 | } 265 | } 266 | }, 267 | "application/json": { 268 | "schema": { 269 | "type": "array", 270 | "items": { 271 | "$ref": "#/components/schemas/Pet" 272 | } 273 | } 274 | } 275 | } 276 | }, 277 | "400": { 278 | "description": "Invalid tag value" 279 | } 280 | }, 281 | "security": [ 282 | { 283 | "petstore_auth": [ 284 | "write:pets", 285 | "read:pets" 286 | ] 287 | } 288 | ] 289 | } 290 | }, 291 | "/pet/{petId}": { 292 | "get": { 293 | "tags": [ 294 | "pet" 295 | ], 296 | "summary": "Find pet by ID", 297 | "description": "Returns a single pet", 298 | "operationId": "getPetById", 299 | "parameters": [ 300 | { 301 | "name": "petId", 302 | "in": "path", 303 | "description": "ID of pet to return", 304 | "required": true, 305 | "schema": { 306 | "type": "integer", 307 | "format": "int64" 308 | } 309 | } 310 | ], 311 | "responses": { 312 | "200": { 313 | "description": "successful operation", 314 | "content": { 315 | "application/xml": { 316 | "schema": { 317 | "$ref": "#/components/schemas/Pet" 318 | } 319 | }, 320 | "application/json": { 321 | "schema": { 322 | "$ref": "#/components/schemas/Pet" 323 | } 324 | } 325 | } 326 | }, 327 | "400": { 328 | "description": "Invalid ID supplied" 329 | }, 330 | "404": { 331 | "description": "Pet not found" 332 | } 333 | }, 334 | "security": [ 335 | { 336 | "api_key": [] 337 | }, 338 | { 339 | "petstore_auth": [ 340 | "write:pets", 341 | "read:pets" 342 | ] 343 | } 344 | ] 345 | }, 346 | "post": { 347 | "tags": [ 348 | "pet" 349 | ], 350 | "summary": "Updates a pet in the store with form data", 351 | "description": "", 352 | "operationId": "updatePetWithForm", 353 | "parameters": [ 354 | { 355 | "name": "petId", 356 | "in": "path", 357 | "description": "ID of pet that needs to be updated", 358 | "required": true, 359 | "schema": { 360 | "type": "integer", 361 | "format": "int64" 362 | } 363 | }, 364 | { 365 | "name": "name", 366 | "in": "query", 367 | "description": "Name of pet that needs to be updated", 368 | "schema": { 369 | "type": "string" 370 | } 371 | }, 372 | { 373 | "name": "status", 374 | "in": "query", 375 | "description": "Status of pet that needs to be updated", 376 | "schema": { 377 | "type": "string" 378 | } 379 | } 380 | ], 381 | "responses": { 382 | "405": { 383 | "description": "Invalid input" 384 | } 385 | }, 386 | "security": [ 387 | { 388 | "petstore_auth": [ 389 | "write:pets", 390 | "read:pets" 391 | ] 392 | } 393 | ] 394 | }, 395 | "delete": { 396 | "tags": [ 397 | "pet" 398 | ], 399 | "summary": "Deletes a pet", 400 | "description": "", 401 | "operationId": "deletePet", 402 | "parameters": [ 403 | { 404 | "name": "api_key", 405 | "in": "header", 406 | "description": "", 407 | "required": false, 408 | "schema": { 409 | "type": "string" 410 | } 411 | }, 412 | { 413 | "name": "petId", 414 | "in": "path", 415 | "description": "Pet id to delete", 416 | "required": true, 417 | "schema": { 418 | "type": "integer", 419 | "format": "int64" 420 | } 421 | } 422 | ], 423 | "responses": { 424 | "400": { 425 | "description": "Invalid pet value" 426 | } 427 | }, 428 | "security": [ 429 | { 430 | "petstore_auth": [ 431 | "write:pets", 432 | "read:pets" 433 | ] 434 | } 435 | ] 436 | } 437 | }, 438 | "/pet/{petId}/uploadImage": { 439 | "post": { 440 | "tags": [ 441 | "pet" 442 | ], 443 | "summary": "uploads an image", 444 | "description": "", 445 | "operationId": "uploadFile", 446 | "parameters": [ 447 | { 448 | "name": "petId", 449 | "in": "path", 450 | "description": "ID of pet to update", 451 | "required": true, 452 | "schema": { 453 | "type": "integer", 454 | "format": "int64" 455 | } 456 | }, 457 | { 458 | "name": "additionalMetadata", 459 | "in": "query", 460 | "description": "Additional Metadata", 461 | "required": false, 462 | "schema": { 463 | "type": "string" 464 | } 465 | } 466 | ], 467 | "requestBody": { 468 | "content": { 469 | "application/octet-stream": { 470 | "schema": { 471 | "type": "string", 472 | "format": "binary" 473 | } 474 | } 475 | } 476 | }, 477 | "responses": { 478 | "200": { 479 | "description": "successful operation", 480 | "content": { 481 | "application/json": { 482 | "schema": { 483 | "$ref": "#/components/schemas/ApiResponse" 484 | } 485 | } 486 | } 487 | } 488 | }, 489 | "security": [ 490 | { 491 | "petstore_auth": [ 492 | "write:pets", 493 | "read:pets" 494 | ] 495 | } 496 | ] 497 | } 498 | }, 499 | "/store/inventory": { 500 | "get": { 501 | "tags": [ 502 | "store" 503 | ], 504 | "summary": "Returns pet inventories by status", 505 | "description": "Returns a map of status codes to quantities", 506 | "operationId": "getInventory", 507 | "responses": { 508 | "200": { 509 | "description": "successful operation", 510 | "content": { 511 | "application/json": { 512 | "schema": { 513 | "type": "object", 514 | "additionalProperties": { 515 | "type": "integer", 516 | "format": "int32" 517 | } 518 | } 519 | } 520 | } 521 | } 522 | }, 523 | "security": [ 524 | { 525 | "api_key": [] 526 | } 527 | ] 528 | } 529 | }, 530 | "/store/order": { 531 | "post": { 532 | "tags": [ 533 | "store" 534 | ], 535 | "summary": "Place an order for a pet", 536 | "description": "Place a new order in the store", 537 | "operationId": "placeOrder", 538 | "requestBody": { 539 | "content": { 540 | "application/json": { 541 | "schema": { 542 | "$ref": "#/components/schemas/Order" 543 | } 544 | }, 545 | "application/xml": { 546 | "schema": { 547 | "$ref": "#/components/schemas/Order" 548 | } 549 | }, 550 | "application/x-www-form-urlencoded": { 551 | "schema": { 552 | "$ref": "#/components/schemas/Order" 553 | } 554 | } 555 | } 556 | }, 557 | "responses": { 558 | "200": { 559 | "description": "successful operation", 560 | "content": { 561 | "application/json": { 562 | "schema": { 563 | "$ref": "#/components/schemas/Order" 564 | } 565 | } 566 | } 567 | }, 568 | "405": { 569 | "description": "Invalid input" 570 | } 571 | } 572 | } 573 | }, 574 | "/store/order/{orderId}": { 575 | "get": { 576 | "tags": [ 577 | "store" 578 | ], 579 | "summary": "Find purchase order by ID", 580 | "description": "For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions.", 581 | "operationId": "getOrderById", 582 | "parameters": [ 583 | { 584 | "name": "orderId", 585 | "in": "path", 586 | "description": "ID of order that needs to be fetched", 587 | "required": true, 588 | "schema": { 589 | "type": "integer", 590 | "format": "int64" 591 | } 592 | } 593 | ], 594 | "responses": { 595 | "200": { 596 | "description": "successful operation", 597 | "content": { 598 | "application/xml": { 599 | "schema": { 600 | "$ref": "#/components/schemas/Order" 601 | } 602 | }, 603 | "application/json": { 604 | "schema": { 605 | "$ref": "#/components/schemas/Order" 606 | } 607 | } 608 | } 609 | }, 610 | "400": { 611 | "description": "Invalid ID supplied" 612 | }, 613 | "404": { 614 | "description": "Order not found" 615 | } 616 | } 617 | }, 618 | "delete": { 619 | "tags": [ 620 | "store" 621 | ], 622 | "summary": "Delete purchase order by ID", 623 | "description": "For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors", 624 | "operationId": "deleteOrder", 625 | "parameters": [ 626 | { 627 | "name": "orderId", 628 | "in": "path", 629 | "description": "ID of the order that needs to be deleted", 630 | "required": true, 631 | "schema": { 632 | "type": "integer", 633 | "format": "int64" 634 | } 635 | } 636 | ], 637 | "responses": { 638 | "400": { 639 | "description": "Invalid ID supplied" 640 | }, 641 | "404": { 642 | "description": "Order not found" 643 | } 644 | } 645 | } 646 | }, 647 | "/user": { 648 | "post": { 649 | "tags": [ 650 | "user" 651 | ], 652 | "summary": "Create user", 653 | "description": "This can only be done by the logged in user.", 654 | "operationId": "createUser", 655 | "requestBody": { 656 | "description": "Created user object", 657 | "content": { 658 | "application/json": { 659 | "schema": { 660 | "$ref": "#/components/schemas/User" 661 | } 662 | }, 663 | "application/xml": { 664 | "schema": { 665 | "$ref": "#/components/schemas/User" 666 | } 667 | }, 668 | "application/x-www-form-urlencoded": { 669 | "schema": { 670 | "$ref": "#/components/schemas/User" 671 | } 672 | } 673 | } 674 | }, 675 | "responses": { 676 | "default": { 677 | "description": "successful operation", 678 | "content": { 679 | "application/json": { 680 | "schema": { 681 | "$ref": "#/components/schemas/User" 682 | } 683 | }, 684 | "application/xml": { 685 | "schema": { 686 | "$ref": "#/components/schemas/User" 687 | } 688 | } 689 | } 690 | } 691 | } 692 | } 693 | }, 694 | "/user/createWithList": { 695 | "post": { 696 | "tags": [ 697 | "user" 698 | ], 699 | "summary": "Creates list of users with given input array", 700 | "description": "Creates list of users with given input array", 701 | "operationId": "createUsersWithListInput", 702 | "requestBody": { 703 | "content": { 704 | "application/json": { 705 | "schema": { 706 | "type": "array", 707 | "items": { 708 | "$ref": "#/components/schemas/User" 709 | } 710 | } 711 | } 712 | } 713 | }, 714 | "responses": { 715 | "200": { 716 | "description": "Successful operation", 717 | "content": { 718 | "application/xml": { 719 | "schema": { 720 | "$ref": "#/components/schemas/User" 721 | } 722 | }, 723 | "application/json": { 724 | "schema": { 725 | "$ref": "#/components/schemas/User" 726 | } 727 | } 728 | } 729 | }, 730 | "default": { 731 | "description": "successful operation" 732 | } 733 | } 734 | } 735 | }, 736 | "/user/login": { 737 | "get": { 738 | "tags": [ 739 | "user" 740 | ], 741 | "summary": "Logs user into the system", 742 | "description": "", 743 | "operationId": "loginUser", 744 | "parameters": [ 745 | { 746 | "name": "username", 747 | "in": "query", 748 | "description": "The user name for login", 749 | "required": false, 750 | "schema": { 751 | "type": "string" 752 | } 753 | }, 754 | { 755 | "name": "password", 756 | "in": "query", 757 | "description": "The password for login in clear text", 758 | "required": false, 759 | "schema": { 760 | "type": "string" 761 | } 762 | } 763 | ], 764 | "responses": { 765 | "200": { 766 | "description": "successful operation", 767 | "headers": { 768 | "X-Rate-Limit": { 769 | "description": "calls per hour allowed by the user", 770 | "schema": { 771 | "type": "integer", 772 | "format": "int32" 773 | } 774 | }, 775 | "X-Expires-After": { 776 | "description": "date in UTC when token expires", 777 | "schema": { 778 | "type": "string", 779 | "format": "date-time" 780 | } 781 | } 782 | }, 783 | "content": { 784 | "application/xml": { 785 | "schema": { 786 | "type": "string" 787 | } 788 | }, 789 | "application/json": { 790 | "schema": { 791 | "type": "string" 792 | } 793 | } 794 | } 795 | }, 796 | "400": { 797 | "description": "Invalid username/password supplied" 798 | } 799 | } 800 | } 801 | }, 802 | "/user/logout": { 803 | "get": { 804 | "tags": [ 805 | "user" 806 | ], 807 | "summary": "Logs out current logged in user session", 808 | "description": "", 809 | "operationId": "logoutUser", 810 | "parameters": [], 811 | "responses": { 812 | "default": { 813 | "description": "successful operation" 814 | } 815 | } 816 | } 817 | }, 818 | "/user/{username}": { 819 | "get": { 820 | "tags": [ 821 | "user" 822 | ], 823 | "summary": "Get user by user name", 824 | "description": "", 825 | "operationId": "getUserByName", 826 | "parameters": [ 827 | { 828 | "name": "username", 829 | "in": "path", 830 | "description": "The name that needs to be fetched. Use user1 for testing. ", 831 | "required": true, 832 | "schema": { 833 | "type": "string" 834 | } 835 | } 836 | ], 837 | "responses": { 838 | "200": { 839 | "description": "successful operation", 840 | "content": { 841 | "application/xml": { 842 | "schema": { 843 | "$ref": "#/components/schemas/User" 844 | } 845 | }, 846 | "application/json": { 847 | "schema": { 848 | "$ref": "#/components/schemas/User" 849 | } 850 | } 851 | } 852 | }, 853 | "400": { 854 | "description": "Invalid username supplied" 855 | }, 856 | "404": { 857 | "description": "User not found" 858 | } 859 | } 860 | }, 861 | "put": { 862 | "tags": [ 863 | "user" 864 | ], 865 | "summary": "Update user", 866 | "description": "This can only be done by the logged in user.", 867 | "operationId": "updateUser", 868 | "parameters": [ 869 | { 870 | "name": "username", 871 | "in": "path", 872 | "description": "name that needs to be updated", 873 | "required": true, 874 | "schema": { 875 | "type": "string" 876 | } 877 | } 878 | ], 879 | "requestBody": { 880 | "description": "Update an existent user in the store", 881 | "content": { 882 | "application/json": { 883 | "schema": { 884 | "$ref": "#/components/schemas/User" 885 | } 886 | }, 887 | "application/xml": { 888 | "schema": { 889 | "$ref": "#/components/schemas/User" 890 | } 891 | }, 892 | "application/x-www-form-urlencoded": { 893 | "schema": { 894 | "$ref": "#/components/schemas/User" 895 | } 896 | } 897 | } 898 | }, 899 | "responses": { 900 | "default": { 901 | "description": "successful operation" 902 | } 903 | } 904 | }, 905 | "delete": { 906 | "tags": [ 907 | "user" 908 | ], 909 | "summary": "Delete user", 910 | "description": "This can only be done by the logged in user.", 911 | "operationId": "deleteUser", 912 | "parameters": [ 913 | { 914 | "name": "username", 915 | "in": "path", 916 | "description": "The name that needs to be deleted", 917 | "required": true, 918 | "schema": { 919 | "type": "string" 920 | } 921 | } 922 | ], 923 | "responses": { 924 | "400": { 925 | "description": "Invalid username supplied" 926 | }, 927 | "404": { 928 | "description": "User not found" 929 | } 930 | } 931 | } 932 | } 933 | }, 934 | "components": { 935 | "schemas": { 936 | "Order": { 937 | "type": "object", 938 | "properties": { 939 | "id": { 940 | "type": "integer", 941 | "format": "int64", 942 | "example": 10 943 | }, 944 | "petId": { 945 | "type": "integer", 946 | "format": "int64", 947 | "example": 198772 948 | }, 949 | "quantity": { 950 | "type": "integer", 951 | "format": "int32", 952 | "example": 7 953 | }, 954 | "shipDate": { 955 | "type": "string", 956 | "format": "date-time" 957 | }, 958 | "status": { 959 | "type": "string", 960 | "description": "Order Status", 961 | "example": "approved", 962 | "enum": [ 963 | "placed", 964 | "approved", 965 | "delivered" 966 | ] 967 | }, 968 | "complete": { 969 | "type": "boolean" 970 | } 971 | }, 972 | "xml": { 973 | "name": "order" 974 | } 975 | }, 976 | "Customer": { 977 | "type": "object", 978 | "properties": { 979 | "id": { 980 | "type": "integer", 981 | "format": "int64", 982 | "example": 100000 983 | }, 984 | "username": { 985 | "type": "string", 986 | "example": "fehguy" 987 | }, 988 | "address": { 989 | "type": "array", 990 | "xml": { 991 | "name": "addresses", 992 | "wrapped": true 993 | }, 994 | "items": { 995 | "$ref": "#/components/schemas/Address" 996 | } 997 | } 998 | }, 999 | "xml": { 1000 | "name": "customer" 1001 | } 1002 | }, 1003 | "Address": { 1004 | "type": "object", 1005 | "properties": { 1006 | "street": { 1007 | "type": "string", 1008 | "example": "437 Lytton" 1009 | }, 1010 | "city": { 1011 | "type": "string", 1012 | "example": "Palo Alto" 1013 | }, 1014 | "state": { 1015 | "type": "string", 1016 | "example": "CA" 1017 | }, 1018 | "zip": { 1019 | "type": "string", 1020 | "example": "94301" 1021 | } 1022 | }, 1023 | "xml": { 1024 | "name": "address" 1025 | } 1026 | }, 1027 | "Category": { 1028 | "type": "object", 1029 | "properties": { 1030 | "id": { 1031 | "type": "integer", 1032 | "format": "int64", 1033 | "example": 1 1034 | }, 1035 | "name": { 1036 | "type": "string", 1037 | "example": "Dogs" 1038 | } 1039 | }, 1040 | "xml": { 1041 | "name": "category" 1042 | } 1043 | }, 1044 | "User": { 1045 | "type": "object", 1046 | "properties": { 1047 | "id": { 1048 | "type": "integer", 1049 | "format": "int64", 1050 | "example": 10 1051 | }, 1052 | "username": { 1053 | "type": "string", 1054 | "example": "theUser" 1055 | }, 1056 | "firstName": { 1057 | "type": "string", 1058 | "example": "John" 1059 | }, 1060 | "lastName": { 1061 | "type": "string", 1062 | "example": "James" 1063 | }, 1064 | "email": { 1065 | "type": "string", 1066 | "example": "john@email.com" 1067 | }, 1068 | "password": { 1069 | "type": "string", 1070 | "example": "12345" 1071 | }, 1072 | "phone": { 1073 | "type": "string", 1074 | "example": "12345" 1075 | }, 1076 | "userStatus": { 1077 | "type": "integer", 1078 | "description": "User Status", 1079 | "format": "int32", 1080 | "example": 1 1081 | } 1082 | }, 1083 | "xml": { 1084 | "name": "user" 1085 | } 1086 | }, 1087 | "Tag": { 1088 | "type": "object", 1089 | "properties": { 1090 | "id": { 1091 | "type": "integer", 1092 | "format": "int64" 1093 | }, 1094 | "name": { 1095 | "type": "string" 1096 | } 1097 | }, 1098 | "xml": { 1099 | "name": "tag" 1100 | } 1101 | }, 1102 | "Pet": { 1103 | "required": [ 1104 | "name", 1105 | "photoUrls" 1106 | ], 1107 | "type": "object", 1108 | "properties": { 1109 | "id": { 1110 | "type": "integer", 1111 | "format": "int64", 1112 | "example": 10 1113 | }, 1114 | "name": { 1115 | "type": "string", 1116 | "example": "doggie" 1117 | }, 1118 | "category": { 1119 | "$ref": "#/components/schemas/Category" 1120 | }, 1121 | "photoUrls": { 1122 | "type": "array", 1123 | "xml": { 1124 | "wrapped": true 1125 | }, 1126 | "items": { 1127 | "type": "string", 1128 | "xml": { 1129 | "name": "photoUrl" 1130 | } 1131 | } 1132 | }, 1133 | "tags": { 1134 | "type": "array", 1135 | "xml": { 1136 | "wrapped": true 1137 | }, 1138 | "items": { 1139 | "$ref": "#/components/schemas/Tag" 1140 | } 1141 | }, 1142 | "status": { 1143 | "type": "string", 1144 | "description": "pet status in the store", 1145 | "enum": [ 1146 | "available", 1147 | "pending", 1148 | "sold" 1149 | ] 1150 | } 1151 | }, 1152 | "xml": { 1153 | "name": "pet" 1154 | } 1155 | }, 1156 | "ApiResponse": { 1157 | "type": "object", 1158 | "properties": { 1159 | "code": { 1160 | "type": "integer", 1161 | "format": "int32" 1162 | }, 1163 | "type": { 1164 | "type": "string" 1165 | }, 1166 | "message": { 1167 | "type": "string" 1168 | } 1169 | }, 1170 | "xml": { 1171 | "name": "##default" 1172 | } 1173 | } 1174 | }, 1175 | "requestBodies": { 1176 | "Pet": { 1177 | "description": "Pet object that needs to be added to the store", 1178 | "content": { 1179 | "application/json": { 1180 | "schema": { 1181 | "$ref": "#/components/schemas/Pet" 1182 | } 1183 | }, 1184 | "application/xml": { 1185 | "schema": { 1186 | "$ref": "#/components/schemas/Pet" 1187 | } 1188 | } 1189 | } 1190 | }, 1191 | "UserArray": { 1192 | "description": "List of user object", 1193 | "content": { 1194 | "application/json": { 1195 | "schema": { 1196 | "type": "array", 1197 | "items": { 1198 | "$ref": "#/components/schemas/User" 1199 | } 1200 | } 1201 | } 1202 | } 1203 | } 1204 | }, 1205 | "securitySchemes": { 1206 | "petstore_auth": { 1207 | "type": "oauth2", 1208 | "flows": { 1209 | "implicit": { 1210 | "authorizationUrl": "https://petstore3.swagger.io/oauth/authorize", 1211 | "scopes": { 1212 | "write:pets": "modify pets in your account", 1213 | "read:pets": "read your pets" 1214 | } 1215 | } 1216 | } 1217 | }, 1218 | "api_key": { 1219 | "type": "apiKey", 1220 | "name": "api_key", 1221 | "in": "header" 1222 | } 1223 | } 1224 | } 1225 | } 1226 | -------------------------------------------------------------------------------- /tests/smoke.spec.ts: -------------------------------------------------------------------------------- 1 | import {N8NPropertiesBuilder} from "../src/N8NPropertiesBuilder"; 2 | import {readdirSync} from 'fs'; 3 | import {join} from 'path'; 4 | import {sleep} from "n8n-workflow"; 5 | 6 | // Define the path to your 'samples' directory 7 | const samplesDir = join(__dirname, 'samples'); 8 | 9 | // Read all JSON files from the samples directory 10 | const jsonFiles = readdirSync(samplesDir).filter(file => file.endsWith('.json')); 11 | 12 | describe('smoke', () => { 13 | test.each(jsonFiles)('%s', async (fileName) => { 14 | const filePath = join(samplesDir, fileName); 15 | const doc = require(filePath); 16 | const parser = new N8NPropertiesBuilder(doc); 17 | parser.build(); 18 | await sleep(50) // wait for logs a bit 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "skipLibCheck": true, 10 | "resolveJsonModule": true, 11 | "sourceMap": true, 12 | "declaration": true, 13 | "outDir": "./dist/" 14 | }, 15 | "include": [ 16 | "src/**/*", 17 | "package.json" 18 | ], 19 | "$schema": "https://json.schemastore.org/tsconfig" 20 | } 21 | --------------------------------------------------------------------------------