diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..2bbcd4d --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +packages/editor-ui +packages/design-system diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..a4fe925 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,106 @@ +module.exports = { + root: true, + + env: { + browser: true, + es6: true, + node: true, + }, + + parser: '@typescript-eslint/parser', + parserOptions: { + project: ['./tsconfig.json'], + sourceType: 'module', + }, + ignorePatterns: [ + '.eslintrc.js', + '**/*.js', + '**/node_modules/**', + '**/dist/**', + '**/test/**', + '**/templates/**', + '**/ormconfig.ts', + '**/migrations/**', + ], + + overrides: [ + { + files: [ './**/*.ts' ], + plugins: ['eslint-plugin-n8n-nodes-base'], + rules: { + 'n8n-nodes-base/filesystem-wrong-cred-filename': 'error', + 'n8n-nodes-base/filesystem-wrong-node-filename': 'error', + 'n8n-nodes-base/node-class-description-empty-string': 'error', + 'n8n-nodes-base/node-class-description-icon-not-svg': 'error', + 'n8n-nodes-base/node-class-description-inputs-wrong-trigger-node': 'error', + 'n8n-nodes-base/node-class-description-missing-subtitle': 'error', + 'n8n-nodes-base/node-class-description-outputs-wrong': 'error', + 'n8n-nodes-base/node-execute-block-double-assertion-for-items': 'error', + 'n8n-nodes-base/node-param-collection-type-unsorted-items': 'error', + 'n8n-nodes-base/node-param-default-missing': 'error', + 'n8n-nodes-base/node-param-default-wrong-for-boolean': 'error', + 'n8n-nodes-base/node-param-default-wrong-for-collection': 'error', + 'n8n-nodes-base/node-param-default-wrong-for-fixed-collection': 'error', + 'n8n-nodes-base/node-param-default-wrong-for-fixed-collection': 'error', + 'n8n-nodes-base/node-param-default-wrong-for-multi-options': 'error', + 'n8n-nodes-base/node-param-default-wrong-for-number': 'error', + 'n8n-nodes-base/node-param-default-wrong-for-simplify': 'error', + 'n8n-nodes-base/node-param-default-wrong-for-string': 'error', + 'n8n-nodes-base/node-param-description-boolean-without-whether': 'error', + 'n8n-nodes-base/node-param-description-comma-separated-hyphen': 'error', + 'n8n-nodes-base/node-param-description-empty-string': 'error', + 'n8n-nodes-base/node-param-description-excess-final-period': 'error', + 'n8n-nodes-base/node-param-description-excess-inner-whitespace': 'error', + 'n8n-nodes-base/node-param-description-identical-to-display-name': 'error', + 'n8n-nodes-base/node-param-description-line-break-html-tag': 'error', + 'n8n-nodes-base/node-param-description-lowercase-first-char': 'error', + 'n8n-nodes-base/node-param-description-miscased-id': 'error', + 'n8n-nodes-base/node-param-description-miscased-json': 'error', + 'n8n-nodes-base/node-param-description-miscased-url': 'error', + 'n8n-nodes-base/node-param-description-missing-final-period': 'error', + 'n8n-nodes-base/node-param-description-missing-for-ignore-ssl-issues': 'error', + 'n8n-nodes-base/node-param-description-missing-for-return-all': 'error', + 'n8n-nodes-base/node-param-description-missing-for-simplify': 'error', + 'n8n-nodes-base/node-param-description-missing-from-dynamic-options': 'error', + 'n8n-nodes-base/node-param-description-missing-from-limit': 'error', + 'n8n-nodes-base/node-param-description-unencoded-angle-brackets': 'error', + 'n8n-nodes-base/node-param-description-unneeded-backticks': 'error', + 'n8n-nodes-base/node-param-description-untrimmed': 'error', + 'n8n-nodes-base/node-param-description-url-missing-protocol': 'error', + 'n8n-nodes-base/node-param-description-weak': 'error', + 'n8n-nodes-base/node-param-description-wrong-for-dynamic-multi-options': 'error', + 'n8n-nodes-base/node-param-description-wrong-for-dynamic-options': 'error', + 'n8n-nodes-base/node-param-description-wrong-for-ignore-ssl-issues': 'error', + 'n8n-nodes-base/node-param-description-wrong-for-limit': 'error', + 'n8n-nodes-base/node-param-description-wrong-for-return-all': 'error', + 'n8n-nodes-base/node-param-description-wrong-for-simplify': 'error', + 'n8n-nodes-base/node-param-description-wrong-for-upsert': 'error', + 'n8n-nodes-base/node-param-display-name-excess-inner-whitespace': 'error', + 'n8n-nodes-base/node-param-display-name-miscased': 'error', + 'n8n-nodes-base/node-param-display-name-miscased-id': 'error', + 'n8n-nodes-base/node-param-display-name-untrimmed': 'error', + 'n8n-nodes-base/node-param-display-name-wrong-for-dynamic-multi-options': 'error', + 'n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options': 'error', + 'n8n-nodes-base/node-param-display-name-wrong-for-simplify': 'error', + 'n8n-nodes-base/node-param-display-name-wrong-for-update-fields': 'error', + 'n8n-nodes-base/node-param-min-value-wrong-for-limit': 'error', + 'n8n-nodes-base/node-param-multi-options-type-unsorted-items': 'error', + 'n8n-nodes-base/node-param-operation-without-no-data-expression': 'error', + 'n8n-nodes-base/node-param-option-description-identical-to-name': 'error', + 'n8n-nodes-base/node-param-option-name-containing-star': 'error', + 'n8n-nodes-base/node-param-option-name-duplicate': 'error', + 'n8n-nodes-base/node-param-option-name-wrong-for-get-all': 'error', + 'n8n-nodes-base/node-param-option-name-wrong-for-upsert': 'error', + 'n8n-nodes-base/node-param-option-value-duplicate': 'error', + 'n8n-nodes-base/node-param-options-type-unsorted-items': 'error', + 'n8n-nodes-base/node-param-placeholder-miscased-id': 'error', + 'n8n-nodes-base/node-param-placeholder-missing-email': 'error', + 'n8n-nodes-base/node-param-required-false': 'error', + 'n8n-nodes-base/node-param-resource-with-plural-option': 'error', + 'n8n-nodes-base/node-param-resource-without-no-data-expression': 'error', + 'n8n-nodes-base/node-param-type-options-missing-from-limit': 'error', + 'n8n-nodes-base/node-class-description-inputs-wrong-regular-node': 'error', + }, + }, + ], +}; diff --git a/.gitignore b/.gitignore index ffb419d..a80c4ec 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ tmp dist npm-debug.log* package-lock.json -yarn.lock \ No newline at end of file +yarn.lock +.vscode/launch.json diff --git a/README.md b/README.md index 52ca22c..2943e58 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,8 @@ All nodes are npm packages. To make your custom node available to the community, 2. Open the project in your editor. 3. Browse the examples in `/nodes`. Modify the examples, or replace them with your own nodes. 4. Update the `package.json` to match your details. +5. Run `npm run lint` to check for errors or `npm run lintfix` to automatically fix errors when possible. +6. Publish your package to npm. More information on the links below. ## More information diff --git a/credentials/HttpBinApi.credentials.ts b/credentials/HttpBinApi.credentials.ts new file mode 100644 index 0000000..8ccd44f --- /dev/null +++ b/credentials/HttpBinApi.credentials.ts @@ -0,0 +1,60 @@ +import { + ICredentialDataDecryptedObject, + ICredentialTestRequest, + ICredentialType, + IHttpRequestOptions, + INodeProperties, +} from 'n8n-workflow'; + +export class HttpBinApi implements ICredentialType { + name = 'httpbinApi'; + displayName = 'HttpBin API'; + documentationUrl = 'httpbin'; + properties: INodeProperties[] = [ + { + displayName: 'Token', + name: 'token', + type: 'string', + default: '', + }, + // { + // displayName: "API Key", + // name: "apiKey", + // type: "string", + // default: "", + // }, + { + displayName: 'Domain', + name: 'domain', + type: 'string', + default: 'https://httpbin.org', + }, + ]; + + // authenticate = { + // type: "headerAuth", + // properties: { + // name: "api-key", + // value: "={{$credentials.apiKey}}", + // }, + // } as IAuthenticateHeaderAuth; + + authenticate = async ( + credentials: ICredentialDataDecryptedObject, + requestOptions: IHttpRequestOptions, + ): Promise => { + const headers = requestOptions.headers || {}; + const authentication = { Authorization: `Bearer ${credentials.token}` }; + Object.assign(requestOptions, { + headers: { ...authentication, ...headers }, + }); + return requestOptions; + }; + + test: ICredentialTestRequest = { + request: { + baseURL: '={{$credentials?.domain}}', + url: '/bearer', + }, + }; +} diff --git a/nodes/HttpBin/HttpBin.node.json b/nodes/HttpBin/HttpBin.node.json new file mode 100644 index 0000000..cc9eb82 --- /dev/null +++ b/nodes/HttpBin/HttpBin.node.json @@ -0,0 +1,21 @@ +{ + "node": "n8n-nodes-base.httpbin", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": [ + "Development", + "Developer Tools" + ], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/credentials/httpbin" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/nodes/n8n-nodes-base.httpbin/" + } + ] + } +} diff --git a/nodes/HttpBin/HttpBin.node.ts b/nodes/HttpBin/HttpBin.node.ts new file mode 100644 index 0000000..00eaee7 --- /dev/null +++ b/nodes/HttpBin/HttpBin.node.ts @@ -0,0 +1,65 @@ +/* eslint-disable n8n-nodes-base/filesystem-wrong-node-filename */ +import { INodeType, INodeTypeDescription } from 'n8n-workflow'; +import { httpVerbFields, httpVerbOperations } from './HttpVerbDescriptions'; + +export class HttpBin implements INodeType { + description: INodeTypeDescription = { + displayName: 'HttpBin', + name: 'httpbin', + icon: 'file:httpbin.svg', + group: ['transform'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Interact with HttpBin API', + defaults: { + name: 'HttpBin', + color: '#3b4151', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'httpbinApi', + required: false, + }, + ], + requestDefaults: { + baseURL: 'https://httpbin.org', + url: '', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + }, + /** + * In the properties array we have two mandatory options objects required + * + * [Resource & Operation] + * + * + * https://docs.n8n.io/integrations/creating-nodes/code/create-first-node/#resources-and-operations + * + * In our example, the operations are separated into their own file (HTTPVerbDescription) + * to keep this class easy to read + * + */ + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'HTTP Verb', + value: 'httpverbs', + }, + ], + default: 'httpverbs', + }, + + ...httpVerbOperations, + ...httpVerbFields, + ], + }; +} diff --git a/nodes/HttpBin/HttpVerbDescriptions.ts b/nodes/HttpBin/HttpVerbDescriptions.ts new file mode 100644 index 0000000..265d131 --- /dev/null +++ b/nodes/HttpBin/HttpVerbDescriptions.ts @@ -0,0 +1,246 @@ +import { INodeProperties } from 'n8n-workflow'; + +// This maps the operations to when the Resource option HTTP Verbs is selected +export const httpVerbOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['httpverbs'], + }, + }, + options: [ + { + name: 'GET', + value: 'get', + routing: { + request: { + method: 'GET', + url: '/get', + }, + }, + }, + { + name: 'DELETE', + value: 'delete', + routing: { + request: { + method: 'DELETE', + url: '/delete', + }, + }, + }, + ], + default: 'get', + }, +]; + +// Here we define what to show when the GET Operation is selected +// We do that by adding operation: ["get"], to "displayOptions.show" +const getOperation: INodeProperties[] = [ + { + name: 'typeofData', + default: 'queryParameter', + description: 'Select type of data to send [Query Parameters]', + displayName: 'Type of Data', + displayOptions: { + show: { + resource: ['httpverbs'], + operation: ['get'], + }, + }, + type: 'options', + options: [ + { + name: 'Query', + value: 'queryParameter', + }, + ], + required: true, + }, + { + name: 'arguments', + default: {}, + description: 'The request\'s query parameters', + displayName: 'Query Parameters', + displayOptions: { + show: { + resource: ['httpverbs'], + operation: ['get'], + }, + }, + options: [ + { + name: 'keyvalue', + displayName: 'Key:Value', + values: [ + { + displayName: 'Key', + name: 'key', + type: 'string', + default: '', + required: true, + description: 'Key of query parameter', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + routing: { + send: { + property: '={{$parent.key}}', + type: 'query', + }, + }, + required: true, + description: 'Value of query parameter', + }, + ], + }, + ], + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + }, +]; + +// Here we define what to show when the DELETE Operation is selected +// We do that by adding operation: ["delete"], to "displayOptions.show" +const deleteOperation: INodeProperties[] = [ + { + name: 'typeofData', + default: 'queryParameter', + description: + 'Select type of data to send [Query Parameter Arguments, JSON-Body]', + displayName: 'Type of Data', + displayOptions: { + show: { + resource: ['httpverbs'], + operation: ['delete'], + }, + }, + options: [ + { + name: 'Query', + value: 'queryParameter', + }, + { + name: 'JSON', + value: 'jsonData', + }, + ], + required: true, + type: 'options', + }, + { + name: 'arguments', + default: {}, + description: 'The request\'s query parameters', + displayName: 'Query Parameters', + displayOptions: { + show: { + resource: ['httpverbs'], + operation: ['delete'], + typeofData: ['queryParameter'], + }, + }, + options: [ + { + name: 'keyvalue', + displayName: 'Key:Value', + values: [ + { + displayName: 'Key', + name: 'key', + type: 'string', + default: '', + required: true, + description: 'Key of query parameter', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + routing: { + send: { + property: '={{$parent.key}}', + type: 'query', + }, + }, + required: true, + description: 'Value of query parameter', + }, + ], + }, + ], + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + }, + { + name: 'arguments', + default: {}, + description: 'The request\'s JSON properties', + displayName: 'JSON Object', + displayOptions: { + show: { + resource: ['httpverbs'], + operation: ['delete'], + typeofData: ['jsonData'], + }, + }, + options: [ + { + name: 'keyvalue', + displayName: 'Key:Value', + values: [ + { + displayName: 'Key', + name: 'key', + type: 'string', + default: '', + required: true, + description: 'Key of JSON property', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + routing: { + send: { + property: '={{$parent.key}}', + type: 'body', + }, + }, + required: true, + description: 'Value of JSON property', + }, + ], + }, + ], + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + }, +]; + +export const httpVerbFields: INodeProperties[] = [ + /* -------------------------------------------------------------------------- */ + /* Http Verbs:Get */ + /* -------------------------------------------------------------------------- */ + ...getOperation, + + /* -------------------------------------------------------------------------- */ + /* Http Verbs:Delete */ + /* -------------------------------------------------------------------------- */ + ...deleteOperation, +]; diff --git a/nodes/HttpBin/httpbin.svg b/nodes/HttpBin/httpbin.svg new file mode 100644 index 0000000..aee1de1 --- /dev/null +++ b/nodes/HttpBin/httpbin.svg @@ -0,0 +1,18 @@ + + + + diff --git a/package.json b/package.json index 1fbbf31..f02b545 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,8 @@ "scripts": { "dev": "npm run watch", "build": "tsc && gulp", - "lint": "tslint -p tsconfig.json -c tslint.json", - "lintfix": "tslint --fix -p tsconfig.json -c tslint.json", - "nodelinter": "nodelinter", + "lint": "tslint -p tsconfig.json -c tslint.json && node_modules/eslint/bin/eslint.js ./nodes", + "lintfix": "tslint --fix -p tsconfig.json -c tslint.json && node_modules/eslint/bin/eslint.js --fix ./nodes", "watch": "tsc --watch", "test": "jest" }, @@ -30,10 +29,12 @@ ], "n8n": { "credentials": [ - "dist/credentials/ExampleCredentials.credentials.js" + "dist/credentials/ExampleCredentials.credentials.js", + "dist/credentials/HttpBinApi.credentials.js" ], "nodes": [ - "dist/nodes/ExampleNode/ExampleNode.node.js" + "dist/nodes/ExampleNode/ExampleNode.node.js", + "dist/nodes/HttpBin/HttpBin.node.js" ] }, "devDependencies": { @@ -41,10 +42,11 @@ "@types/jest": "^26.0.13", "@types/node": "^14.17.27", "@types/request-promise-native": "~1.0.15", + "@typescript-eslint/parser": "^5.29.0", + "eslint-plugin-n8n-nodes-base": "^1.0.43", "gulp": "^4.0.2", "jest": "^26.4.2", "n8n-workflow": "~0.104.0", - "nodelinter": "^0.1.9", "ts-jest": "^26.3.0", "tslint": "^6.1.2", "typescript": "~4.3.5" diff --git a/tsconfig.json b/tsconfig.json index 48efe0b..fc72671 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,7 +17,7 @@ "resolveJsonModule": true, "declaration": true, "outDir": "./dist/", - "target": "es2017", + "target": "es2019", "sourceMap": true, "esModuleInterop": true }, diff --git a/tslint.json b/tslint.json index 859893d..d8dc7ad 100644 --- a/tslint.json +++ b/tslint.json @@ -46,7 +46,11 @@ "forin": true, "jsdoc-format": true, "label-position": true, - "indent": [true, "tabs", 2], + "indent": [ + true, + "tabs", + 2 + ], "member-access": [ true, "no-public" @@ -61,10 +65,13 @@ "no-default-export": true, "no-duplicate-variable": true, "no-inferrable-types": true, - "ordered-imports": [true, { - "import-sources-order": "any", - "named-imports-order": "case-insensitive" - }], + "ordered-imports": [ + true, + { + "import-sources-order": "any", + "named-imports-order": "case-insensitive" + } + ], "no-namespace": [ true, "allow-declarations" @@ -90,13 +97,13 @@ "trailing-comma": [ true, { - "multiline": { - "objects": "always", - "arrays": "always", - "functions": "always", - "typeLiterals": "ignore" - }, - "esSpecCompliant": true + "multiline": { + "objects": "always", + "arrays": "always", + "functions": "always", + "typeLiterals": "ignore" + }, + "esSpecCompliant": true } ], "triple-equals": [ @@ -121,4 +128,4 @@ ] }, "rulesDirectory": [] -} \ No newline at end of file +}