From 06c407def88e5872b2478ac240430006055a2a22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milorad=20FIlipovi=C4=87?= Date: Fri, 27 May 2022 18:04:56 +0200 Subject: [PATCH] feat(PostBin Node): Add PostBin node (#3236) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🚧 Initial progress on PostBin node. * ✨ Implemented Bin and Request operations for PostBin node. * 🚧 Reworked the node in the declarative way. * 🚧 PosBin node refactoring after reworking it. * ✨ Implemented Bin id parsing in PostBin node. Done some final refactoring and documentation. * :zap: Improvements * :zap: Add comments * 👌Updating the PostBin node based on the product review * 💄Updating PostBin node Bin ID validation logic * :zap: Small improvements * :zap: Transform the bin requests and add additional properties Co-authored-by: ricardo Co-authored-by: Jan Oberhauser --- packages/core/src/NodeExecuteFunctions.ts | 5 +- .../nodes/PostBin/BinDescription.ts | 104 ++++++++++++ .../nodes/PostBin/GenericFunctions.ts | 113 +++++++++++++ .../nodes/PostBin/PostBin.node.json | 16 ++ .../nodes-base/nodes/PostBin/PostBin.node.ts | 59 +++++++ .../nodes/PostBin/RequestDescription.ts | 155 ++++++++++++++++++ packages/nodes-base/nodes/PostBin/postbin.svg | 3 + packages/nodes-base/package.json | 1 + 8 files changed, 455 insertions(+), 1 deletion(-) create mode 100644 packages/nodes-base/nodes/PostBin/BinDescription.ts create mode 100644 packages/nodes-base/nodes/PostBin/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/PostBin/PostBin.node.json create mode 100644 packages/nodes-base/nodes/PostBin/PostBin.node.ts create mode 100644 packages/nodes-base/nodes/PostBin/RequestDescription.ts create mode 100644 packages/nodes-base/nodes/PostBin/postbin.svg diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index fc33761a96..1da062693e 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -708,7 +708,10 @@ function convertN8nRequestToAxios(n8nRequest: IHttpRequestOptions): AxiosRequest }; } - if (n8nRequest.body) { + // if there is a body and it's empty (does not have properties), + // make sure not to send anything in it as some services fail when + // sending GET request with empty body. + if (n8nRequest.body && Object.keys(n8nRequest.body).length) { axiosRequest.data = n8nRequest.body; // Let's add some useful header standards here. const existingContentTypeHeaderKey = searchForHeader(axiosRequest.headers, 'content-type'); diff --git a/packages/nodes-base/nodes/PostBin/BinDescription.ts b/packages/nodes-base/nodes/PostBin/BinDescription.ts new file mode 100644 index 0000000000..91dd007f6c --- /dev/null +++ b/packages/nodes-base/nodes/PostBin/BinDescription.ts @@ -0,0 +1,104 @@ +import { + INodeProperties +} from 'n8n-workflow'; + +import { + buildBinAPIURL, + transformBinReponse, +} from './GenericFunctions'; + + +// Operations for the `Bin` resource: +export const binOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'bin', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create bin', + routing: { + request: { + method: 'POST', + url: '/developers/postbin/api/bin', + }, + output: { + postReceive: [ + transformBinReponse, + ], + }, + }, + }, + { + name: 'Get', + value: 'get', + description: 'Get a bin', + routing: { + request: { + method: 'GET', + }, + output: { + postReceive: [ + transformBinReponse, + ], + }, + send: { + preSend: [ + // Parse binId before sending to make sure it's in the right format + buildBinAPIURL, + ], + }, + }, + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a bin', + routing: { + request: { + method: 'DELETE', + }, + send: { + preSend: [ + // Parse binId before sending to make sure it's in the right format + buildBinAPIURL, + ], + }, + }, + }, + ], + default: 'create', + }, +]; + +// Properties of the `Bin` resource +export const binFields: INodeProperties[] = [ + { + name: 'binId', + displayName: 'Bin ID', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'bin', + ], + operation: [ + 'get', + 'delete', + ], + }, + }, + description: 'Unique identifier for each bin', + }, +]; diff --git a/packages/nodes-base/nodes/PostBin/GenericFunctions.ts b/packages/nodes-base/nodes/PostBin/GenericFunctions.ts new file mode 100644 index 0000000000..3a07ae7768 --- /dev/null +++ b/packages/nodes-base/nodes/PostBin/GenericFunctions.ts @@ -0,0 +1,113 @@ +import { + IExecuteSingleFunctions, + IHttpRequestOptions, + IN8nHttpFullResponse, + INodeExecutionData, + NodeApiError, +} from 'n8n-workflow'; + +// Regular expressions used to extract binId from parameter value +const BIN_ID_REGEX = /\b\d{13}-\d{13}\b/g; + +/** + * Creates correctly-formatted PostBin API URL based on the entered binId. + * This function makes sure binId is in the expected format by parsing it + * from current node parameter value. + * + * @export + * @param {IExecuteSingleFunctions} this + * @param {IHttpRequestOptions} requestOptions + * @returns {Promise} requestOptions + */ +export async function buildBinAPIURL(this: IExecuteSingleFunctions, requestOptions: IHttpRequestOptions): Promise { + const binId = parseBinId(this); + // Assemble the PostBin API URL and put it back to requestOptions + requestOptions.url = `/developers/postbin/api/bin/${binId}`; + + return requestOptions; +} + +/** + * Creates correctly-formatted PostBin Bin test URL based on the entered binId. + * This function makes sure binId is in the expected format by parsing it + * from current node parameter value. + * + * @export + * @param {IExecuteSingleFunctions} this + * @param {IHttpRequestOptions} requestOptions + * @returns {Promise} requestOptions + */ +export async function buildBinTestURL(this: IExecuteSingleFunctions, requestOptions: IHttpRequestOptions): Promise { + const binId = parseBinId(this); + + // Assemble the PostBin API URL and put it back to requestOptions + requestOptions.url = `/developers/postbin/${binId}`; + return requestOptions; +} + +/** + * Creates correctly-formatted PostBin API URL based on the entered binId and reqId. + * This function makes sure binId is in the expected format by parsing it + * from current node parameter value. + * + * @export + * @param {IExecuteSingleFunctions} this + * @param {IHttpRequestOptions} requestOptions + * @returns {Promise} requestOptions + */ +export async function buildRequestURL(this: IExecuteSingleFunctions, requestOptions: IHttpRequestOptions): Promise { + const reqId = this.getNodeParameter('requestId', 'shift') as string; + const binId = parseBinId(this); + + requestOptions.url = `/developers/postbin/api/bin/${binId}/req/${reqId}`; + return requestOptions; +} + +/** + * Extracts the PostBin Bin Id from the specified string. + * This method should be able to extract bin Id from the + * PostBin URL or from the string in the following format: + * `Bin ''.` + * + * @param {IExecuteSingleFunctions} this + * @param {IHttpRequestOptions} requestOptions + * @returns {Promise} requestOptions + */ +function parseBinId(context: IExecuteSingleFunctions) { + const binId = context.getNodeParameter('binId') as string; + // Test if the Bin id is in the expected format + const idMatch = BIN_ID_REGEX.exec(binId); + + // Return what is matched + if(idMatch) { + return idMatch[0]; + } + + // If it's not recognized, error out + throw new NodeApiError(context.getNode(), {}, { + message: 'Bin ID format is not valid', + description: 'Please check the provided Bin ID and try again.', + parseXml: false, + }); +} + +/** + * Converts the bin response data and adds additional properties + * + * @param {IExecuteSingleFunctions} this + * @param {INodeExecutionData} items[] + * @param {IN8nHttpFullResponse} response + * @returns {Promise} + */ + export async function transformBinReponse(this: IExecuteSingleFunctions, items: INodeExecutionData[], response: IN8nHttpFullResponse): Promise { + items.forEach(item => item.json = { + 'binId': item.json.binId, + 'nowTimestamp': item.json.now, + 'nowIso': new Date(item.json.now as string).toISOString(), + 'expiresTimestamp': item.json.expires, + 'expiresIso': new Date(item.json.expires as string).toISOString(), + 'requestUrl': 'https://www.toptal.com/developers/postbin/' + item.json.binId, + 'viewUrl': 'https://www.toptal.com/developers/postbin/b/' + item.json.binId, + }); + return items; +} diff --git a/packages/nodes-base/nodes/PostBin/PostBin.node.json b/packages/nodes-base/nodes/PostBin/PostBin.node.json new file mode 100644 index 0000000000..1e46a63e04 --- /dev/null +++ b/packages/nodes-base/nodes/PostBin/PostBin.node.json @@ -0,0 +1,16 @@ +{ + "node": "n8n-nodes-base.postbin", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": [ + "Development", + "Data & Storage" + ], + "resources": { + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/nodes/n8n-nodes-base.postbin/" + } + ] + } +} diff --git a/packages/nodes-base/nodes/PostBin/PostBin.node.ts b/packages/nodes-base/nodes/PostBin/PostBin.node.ts new file mode 100644 index 0000000000..f6da2476ed --- /dev/null +++ b/packages/nodes-base/nodes/PostBin/PostBin.node.ts @@ -0,0 +1,59 @@ +import { + INodeType, + INodeTypeDescription +} from 'n8n-workflow'; + +import { + binFields, + binOperations, +} from './BinDescription'; + +import { + requestFields, + requestOperations, +} from './RequestDescription'; + +export class PostBin implements INodeType { + description: INodeTypeDescription = { + displayName: 'PostBin', + name: 'postBin', + icon: 'file:postbin.svg', + group: ['transform'], + version: 1, + subtitle: '={{ $parameter["operation"] + ": " + $parameter["resource"] }}', + description: 'Consume PostBin API', + defaults: { + name: 'PostBin', + color: '#4dc0b5', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [], + requestDefaults: { + baseURL: 'https://www.toptal.com', + }, + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Bin', + value: 'bin', + }, + { + name: 'Request', + value: 'request', + }, + ], + default: 'bin', + required: true, + }, + ...binOperations, + ...binFields, + ...requestOperations, + ...requestFields, + ], + }; +} diff --git a/packages/nodes-base/nodes/PostBin/RequestDescription.ts b/packages/nodes-base/nodes/PostBin/RequestDescription.ts new file mode 100644 index 0000000000..6ed545263d --- /dev/null +++ b/packages/nodes-base/nodes/PostBin/RequestDescription.ts @@ -0,0 +1,155 @@ +import { + INodeProperties +} from 'n8n-workflow'; + +import { + buildBinTestURL, + buildRequestURL +} from './GenericFunctions'; + +// Operations for the `Request` resource +export const requestOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'request', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get a request', + routing: { + request: { + method: 'GET', + url: '=/developers/postbin/api/bin/{{$parameter["binId"]}}/req/{{$parameter["requestId"]}}', + }, + send: { + preSend: [ + // Parse binId before sending to make sure it's in the right format + buildRequestURL, + ], + }, + }, + }, + { + name: 'Remove First', + value: 'removeFirst', + description: 'Remove the first request from bin', + routing: { + request: { + method: 'GET', + url: '=/developers/postbin/api/bin/{{$parameter["binId"]}}/req/shift', + }, + send: { + preSend: [ + // Parse binId before sending to make sure it's in the right format + buildRequestURL, + ], + }, + }, + }, + { + name: 'Send', + value: 'send', + description: 'Send a test request to the bin', + routing: { + request: { + method: 'POST', + }, + send: { + preSend: [ + // Parse binId before sending to make sure it's in the right format + buildBinTestURL, + ], + }, + output: { + postReceive: [ + { + type: 'set', + properties: { + value: '={{ { "requestId": $response.body } }}', + }, + }, + ], + }, + }, + }, + ], + default: 'get', + }, +]; + +// Properties of the `Request` resource +export const requestFields: INodeProperties[] = [ + { + name: 'binId', + displayName: 'Bin ID', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'request', + ], + operation: [ + 'get', + 'removeFirst', + 'send', + ], + }, + }, + description: 'Unique identifier for each bin', + }, + { + name: 'binContent', + displayName: 'Bin Content', + type: 'string', + default: '', + typeOptions: { + rows: 5, + }, + displayOptions: { + show: { + resource: [ + 'request', + ], + operation: [ + 'send', + ], + }, + }, + // Content is sent in the body of POST requests + routing: { + send: { + property: 'content', + type: 'body', + }, + }, + }, + { + name: 'requestId', + displayName: 'Request ID', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'request', + ], + operation: [ + 'get', + ], + }, + }, + description: 'Unique identifier for each request', + }, +]; diff --git a/packages/nodes-base/nodes/PostBin/postbin.svg b/packages/nodes-base/nodes/PostBin/postbin.svg new file mode 100644 index 0000000000..daea1576f6 --- /dev/null +++ b/packages/nodes-base/nodes/PostBin/postbin.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index e1a6f00494..cb1810385a 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -570,6 +570,7 @@ "dist/nodes/Pipedrive/Pipedrive.node.js", "dist/nodes/Pipedrive/PipedriveTrigger.node.js", "dist/nodes/Plivo/Plivo.node.js", + "dist/nodes/PostBin/PostBin.node.js", "dist/nodes/Postgres/Postgres.node.js", "dist/nodes/PostHog/PostHog.node.js", "dist/nodes/Postmark/PostmarkTrigger.node.js",