From 638e688f2526d2e4aef48bb41c5fab064a020c63 Mon Sep 17 00:00:00 2001 From: Jan Date: Tue, 3 Nov 2020 23:33:10 +0100 Subject: [PATCH] :sparkles: Add Orbit Node (#1123) * :sparkles: Add Orbit node * :hammer: Refactor and add new functionality * :zap: Improvements to Orbit-Node * :zap: Improvements * :zap: Minor improvements to Orbit-Node Co-authored-by: Tanay Pant Co-authored-by: ricardo --- .../credentials/OrbitApi.credentials.ts | 18 + .../nodes/Orbit/ActivityDescription.ts | 248 +++++ .../nodes/Orbit/GenericFunctions.ts | 119 +++ packages/nodes-base/nodes/Orbit/Interfaces.ts | 23 + .../nodes/Orbit/MemberDescription.ts | 881 ++++++++++++++++++ .../nodes-base/nodes/Orbit/NoteDescription.ts | 272 ++++++ packages/nodes-base/nodes/Orbit/Orbit.node.ts | 458 +++++++++ .../nodes-base/nodes/Orbit/PostDescription.ts | 273 ++++++ packages/nodes-base/nodes/Orbit/orbit.svg | 9 + packages/nodes-base/package.json | 2 + 10 files changed, 2303 insertions(+) create mode 100644 packages/nodes-base/credentials/OrbitApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Orbit/ActivityDescription.ts create mode 100644 packages/nodes-base/nodes/Orbit/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Orbit/Interfaces.ts create mode 100644 packages/nodes-base/nodes/Orbit/MemberDescription.ts create mode 100644 packages/nodes-base/nodes/Orbit/NoteDescription.ts create mode 100644 packages/nodes-base/nodes/Orbit/Orbit.node.ts create mode 100644 packages/nodes-base/nodes/Orbit/PostDescription.ts create mode 100644 packages/nodes-base/nodes/Orbit/orbit.svg diff --git a/packages/nodes-base/credentials/OrbitApi.credentials.ts b/packages/nodes-base/credentials/OrbitApi.credentials.ts new file mode 100644 index 0000000000..c3d0e5650a --- /dev/null +++ b/packages/nodes-base/credentials/OrbitApi.credentials.ts @@ -0,0 +1,18 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class OrbitApi implements ICredentialType { + name = 'orbitApi'; + displayName = 'Orbit API'; + documentationUrl = 'orbit'; + properties = [ + { + displayName: 'API Token', + name: 'accessToken', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Orbit/ActivityDescription.ts b/packages/nodes-base/nodes/Orbit/ActivityDescription.ts new file mode 100644 index 0000000000..fad53ff169 --- /dev/null +++ b/packages/nodes-base/nodes/Orbit/ActivityDescription.ts @@ -0,0 +1,248 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const activityOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'activity', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create an activity for a member', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all activities', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const activityFields = [ + + /* -------------------------------------------------------------------------- */ + /* activity:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Workspace', + name: 'workspaceId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getWorkspaces', + }, + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'activity', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Member ID', + name: 'memberId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'activity', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'activity', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'activity', + ], + operation: [ + 'create', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Activity Type', + name: 'activityType', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getActivityTypes', + }, + default: '', + description: 'A user-defined way to group activities of the same nature', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: 'A description of the activity; displayed in the timeline', + }, + { + displayName: 'Key', + name: 'key', + type: 'string', + default: '', + description: 'Supply a key that must be unique or leave blank to have one generated', + }, + { + displayName: 'Link', + name: 'link', + type: 'string', + default: '', + description: 'A URL for the activity; displayed in the timeline', + }, + { + displayName: 'Link Text', + name: 'linkText', + type: 'string', + default: '', + description: 'The text for the timeline link', + }, + { + displayName: 'Occurred At', + name: 'occurredAt', + type: 'dateTime', + default: '', + description: 'The date and time the activity occurred; defaults to now', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* activity:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Workspace', + name: 'workspaceId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getWorkspaces', + }, + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'activity', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'activity', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'activity', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + resource: [ + 'activity', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Member ID', + name: 'memberId', + type: 'string', + default: '', + description: 'When set the post will be filtered by the member id.', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Orbit/GenericFunctions.ts b/packages/nodes-base/nodes/Orbit/GenericFunctions.ts new file mode 100644 index 0000000000..d1a4d48bb3 --- /dev/null +++ b/packages/nodes-base/nodes/Orbit/GenericFunctions.ts @@ -0,0 +1,119 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + IHookFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +import { + IRelation, +} from './Interfaces'; + +export async function orbitApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + try { + const credentials = this.getCredentials('orbitApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + let options: OptionsWithUri = { + headers: { + Authorization: `Bearer ${credentials.accessToken}`, + }, + method, + qs, + body, + uri: uri || `https://app.orbit.love/api/v1${resource}`, + json: true, + }; + + options = Object.assign({}, options, option); + + return await this.helpers.request!(options); + } catch (error) { + + if (error.response && error.response.body && error.response.body.message) { + // Try to return the error prettier + const errorBody = error.response.body; + throw new Error(`Orbit error response [${error.statusCode}]: ${errorBody.message}`); + } + + // Expected error data did not get returned so throw the actual error + throw error; + } +} + +/** + * Make an API request to paginated flow endpoint + * and return all results + */ +export async function orbitApiRequestAllItems(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, resource: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + query.page = 1; + + do { + responseData = await orbitApiRequest.call(this, method, resource, body, query); + returnData.push.apply(returnData, responseData[propertyName]); + + if (query.resolveIdentities === true) { + resolveIdentities(responseData); + } + + if (query.resolveMember === true) { + resolveMember(responseData); + } + + query.page++; + if (query.limit && (returnData.length >= query.limit)) { + return returnData; + } + + } while ( + responseData.data.length !== 0 + ); + return returnData; +} + +export function resolveIdentities(responseData: IRelation) { + const identities: IDataObject = {}; + for (const data of responseData.included) { + identities[data.id as string] = data; + } + + if (!Array.isArray(responseData.data)) { + responseData.data = [responseData.data]; + } + + for (let i = 0; i < responseData.data.length; i++) { + for (let y = 0; y < responseData.data[i].relationships.identities.data.length; y++) { + //@ts-ignore + responseData.data[i].relationships.identities.data[y] = identities[responseData.data[i].relationships.identities.data[y].id]; + } + } +} + +export function resolveMember(responseData: IRelation) { + const members: IDataObject = {}; + for (const data of responseData.included) { + members[data.id as string] = data; + } + + if (!Array.isArray(responseData.data)) { + responseData.data = [responseData.data]; + } + + for (let i = 0; i < responseData.data.length; i++) { + //@ts-ignore + responseData.data[i].relationships.member.data = members[responseData.data[i].relationships.member.data.id]; + } +} diff --git a/packages/nodes-base/nodes/Orbit/Interfaces.ts b/packages/nodes-base/nodes/Orbit/Interfaces.ts new file mode 100644 index 0000000000..2f49eb5701 --- /dev/null +++ b/packages/nodes-base/nodes/Orbit/Interfaces.ts @@ -0,0 +1,23 @@ +import { + IDataObject, +} from "n8n-workflow"; + +export interface IData { + data: [ + { + id: string, + }, + ]; +} + +export interface IRelation { + data: [ + { + relationships: { + identities: IData, + member: IData, + }, + }, + ]; + included: IDataObject[]; +} diff --git a/packages/nodes-base/nodes/Orbit/MemberDescription.ts b/packages/nodes-base/nodes/Orbit/MemberDescription.ts new file mode 100644 index 0000000000..60d4fa7600 --- /dev/null +++ b/packages/nodes-base/nodes/Orbit/MemberDescription.ts @@ -0,0 +1,881 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const memberOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'member', + ], + }, + }, + options: [ + { + name: 'Delete', + value: 'delete', + description: 'Delete a member', + }, + { + name: 'Get', + value: 'get', + description: 'Get a member', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all members in a workspace', + }, + { + name: 'Lookup', + value: 'lookup', + description: 'Lookup a member by identity', + }, + { + name: 'Update', + value: 'update', + description: 'Update a member', + }, + { + name: 'Upsert', + value: 'upsert', + description: 'Create/Update a member', + }, + ], + default: 'get', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const memberFields = [ + + /* -------------------------------------------------------------------------- */ + /* member:delete */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Workspace', + name: 'workspaceId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getWorkspaces', + }, + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'member', + ], + operation: [ + 'delete', + ], + }, + }, + description: 'The workspace', + }, + { + displayName: 'Member ID', + name: 'memberId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'member', + ], + operation: [ + 'delete', + ], + }, + }, + description: 'Member ID', + }, + + + /* -------------------------------------------------------------------------- */ + /* member:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Workspace', + name: 'workspaceId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getWorkspaces', + }, + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'member', + ], + operation: [ + 'get', + ], + }, + }, + description: 'The workspace', + }, + { + displayName: 'Member ID', + name: 'memberId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'member', + ], + operation: [ + 'get', + ], + }, + }, + description: 'Member ID', + }, + { + displayName: 'Resolve Identities', + name: 'resolveIdentities', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'member', + ], + }, + }, + default: false, + description: 'By default, the response just includes the reference of the identity. When set to true the identities will be resolved automatically.', + }, + + /* -------------------------------------------------------------------------- */ + /* member:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Workspace', + name: 'workspaceId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getWorkspaces', + }, + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'member', + ], + operation: [ + 'getAll', + ], + }, + }, + description: 'The workspace', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'member', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'member', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Resolve Identities', + name: 'resolveIdentities', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'member', + ], + }, + }, + default: false, + description: 'By default, the response just includes the reference of the identity. When set to true the identities will be resolved automatically.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource: [ + 'member', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Sort By', + name: 'sort', + type: 'string', + default: '', + description: 'Name of the field the response will be sorted by.', + }, + { + displayName: 'Sort Direction', + name: 'direction', + type: 'options', + options: [ + { + name: 'ASC', + value: 'ASC', + }, + { + name: 'DESC', + value: 'DESC', + }, + ], + default: '', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* member:lookup */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Workspace', + name: 'workspaceId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getWorkspaces', + }, + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'member', + ], + operation: [ + 'lookup', + ], + }, + }, + description: 'The workspace', + }, + { + displayName: 'Source', + name: 'source', + type: 'options', + options: [ + { + name: 'Discourse', + value: 'discourse', + }, + { + name: 'Email', + value: 'email', + }, + { + name: 'GitHub', + value: 'github', + }, + { + name: 'Twitter', + value: 'twitter', + }, + ], + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'member', + ], + operation: [ + 'lookup', + ], + }, + }, + description: `Set to github, twitter, email, discourse or the source of any identities you've manually created.`, + }, + { + displayName: 'Search By', + name: 'searchBy', + type: 'options', + options: [ + { + name: 'Username', + value: 'username', + }, + { + name: 'ID', + value: 'id', + }, + ], + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'member', + ], + operation: [ + 'lookup', + ], + source: [ + 'discourse', + 'github', + 'twitter', + ], + }, + }, + }, + { + displayName: 'ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'member', + ], + operation: [ + 'lookup', + ], + searchBy: [ + 'id', + ], + source: [ + 'discourse', + 'github', + 'twitter', + ], + }, + }, + description: `The username at the source.`, + }, + { + displayName: 'Username', + name: 'username', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'member', + ], + operation: [ + 'lookup', + ], + searchBy: [ + 'username', + ], + source: [ + 'discourse', + 'github', + 'twitter', + ], + }, + }, + description: `The username at the source.`, + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'member', + ], + operation: [ + 'lookup', + ], + source: [ + 'email', + ], + }, + }, + description: `The email address.`, + }, + { + displayName: 'Host', + name: 'host', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'member', + ], + operation: [ + 'lookup', + ], + source: [ + 'discourse', + ], + }, + }, + }, + + /* -------------------------------------------------------------------------- */ + /* member:update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Workspace', + name: 'workspaceId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getWorkspaces', + }, + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'member', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Member ID', + name: 'memberId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'member', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'member', + ], + operation: [ + 'update', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Bio', + name: 'bio', + type: 'string', + default: '', + }, + { + displayName: 'Birthday', + name: 'birthday', + type: 'dateTime', + default: '', + }, + { + displayName: 'Company', + name: 'company', + type: 'string', + default: '', + }, + { + displayName: 'Location', + name: 'location', + type: 'string', + default: '', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + }, + { + displayName: 'Pronouns', + name: 'pronouns', + type: 'string', + default: '', + }, + { + displayName: 'Shipping Address', + name: 'shippingAddress', + type: 'string', + default: '', + }, + { + displayName: 'Slug', + name: 'slug', + type: 'string', + default: '', + }, + { + displayName: 'Tags to Add', + name: 'tagsToAdd', + type: 'string', + default: '', + description: 'Adds tags to member; comma-separated string or array', + }, + { + displayName: 'Tag List', + name: 'tagList', + type: 'string', + default: '', + description: 'Replaces all tags for the member; comma-separated string or array', + }, + { + displayName: 'T-Shirt', + name: 'tShirt', + type: 'string', + default: '', + }, + { + displayName: 'Teammate', + name: 'teammate', + type: 'boolean', + default: false, + }, + { + displayName: 'URL', + name: 'url', + type: 'string', + default: '', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* member:upsert */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Workspace', + name: 'workspaceId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getWorkspaces', + }, + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'member', + ], + operation: [ + 'upsert', + ], + }, + }, + }, + { + displayName: 'Identity', + name: 'identityUi', + type: 'fixedCollection', + description: 'The identity is used to find the member. If no member exists, a new member will be created and linked to the provided identity.', + typeOptions: { + multipleValues: false, + }, + placeholder: 'Add Identity', + default: {}, + displayOptions: { + show: { + resource: [ + 'member', + ], + operation: [ + 'upsert', + ], + }, + }, + options: [ + { + displayName: 'Identity', + name: 'identityValue', + values: [ + { + displayName: 'Source', + name: 'source', + type: 'options', + options: [ + { + name: 'Discourse', + value: 'discourse', + }, + { + name: 'Email', + value: 'email', + }, + { + name: 'GitHub', + value: 'github', + }, + { + name: 'Twitter', + value: 'twitter', + }, + ], + default: '', + description: `Set to github, twitter, email, discourse or the source of any identities you've manually created.`, + }, + { + displayName: 'Search By', + name: 'searchBy', + type: 'options', + options: [ + { + name: 'Username', + value: 'username', + }, + { + name: 'ID', + value: 'id', + }, + ], + default: '', + required: true, + displayOptions: { + show: { + source: [ + 'discourse', + 'github', + 'twitter', + ], + }, + }, + }, + { + displayName: 'ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + searchBy: [ + 'id', + ], + source: [ + 'discourse', + 'github', + 'twitter', + ], + }, + }, + description: `The username at the source.`, + }, + { + displayName: 'Username', + name: 'username', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + searchBy: [ + 'username', + ], + source: [ + 'discourse', + 'github', + 'twitter', + ], + }, + }, + description: `The username at the source.`, + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + source: [ + 'email', + ], + }, + }, + }, + { + displayName: 'Host', + name: 'host', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + source: [ + 'discourse', + ], + }, + }, + }, + ], + }, + ], + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'member', + ], + operation: [ + 'upsert', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Bio', + name: 'bio', + type: 'string', + default: '', + }, + { + displayName: 'Birthday', + name: 'birthday', + type: 'dateTime', + default: '', + }, + { + displayName: 'Company', + name: 'company', + type: 'string', + default: '', + }, + { + displayName: 'Location', + name: 'location', + type: 'string', + default: '', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + }, + { + displayName: 'Pronouns', + name: 'pronouns', + type: 'string', + default: '', + }, + { + displayName: 'Shipping Address', + name: 'shippingAddress', + type: 'string', + default: '', + }, + { + displayName: 'Slug', + name: 'slug', + type: 'string', + default: '', + }, + { + displayName: 'Tags to Add', + name: 'tagsToAdd', + type: 'string', + default: '', + description: 'Adds tags to member; comma-separated string or array.', + }, + { + displayName: 'Tag List', + name: 'tagList', + type: 'string', + default: '', + description: 'Replaces all tags for the member; comma-separated string or array.', + }, + { + displayName: 'T-Shirt', + name: 'tShirt', + type: 'string', + default: '', + }, + { + displayName: 'Teammate', + name: 'teammate', + type: 'boolean', + default: false, + }, + { + displayName: 'URL', + name: 'url', + type: 'string', + default: '', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Orbit/NoteDescription.ts b/packages/nodes-base/nodes/Orbit/NoteDescription.ts new file mode 100644 index 0000000000..d643418177 --- /dev/null +++ b/packages/nodes-base/nodes/Orbit/NoteDescription.ts @@ -0,0 +1,272 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const noteOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'note', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a note', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all notes for a member', + }, + { + name: 'Update', + value: 'update', + description: 'Update a note', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const noteFields = [ + + /* -------------------------------------------------------------------------- */ + /* note:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Workspace', + name: 'workspaceId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getWorkspaces', + }, + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'note', + ], + operation: [ + 'create', + ], + }, + }, + description: 'The workspace', + }, + { + displayName: 'Member ID', + name: 'memberId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'note', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Note', + name: 'note', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'note', + ], + operation: [ + 'create', + ], + }, + }, + }, + + /* -------------------------------------------------------------------------- */ + /* note:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Workspace', + name: 'workspaceId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getWorkspaces', + }, + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'note', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Member ID', + name: 'memberId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'note', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'note', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'note', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Resolve Member', + name: 'resolveMember', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'note', + ], + }, + }, + default: false, + }, + + /* -------------------------------------------------------------------------- */ + /* note:update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Workspace', + name: 'workspaceId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getWorkspaces', + }, + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'note', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Member ID', + name: 'memberId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'note', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Note ID', + name: 'noteId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'note', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Note', + name: 'note', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'note', + ], + operation: [ + 'update', + ], + }, + }, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Orbit/Orbit.node.ts b/packages/nodes-base/nodes/Orbit/Orbit.node.ts new file mode 100644 index 0000000000..42ec460492 --- /dev/null +++ b/packages/nodes-base/nodes/Orbit/Orbit.node.ts @@ -0,0 +1,458 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + orbitApiRequest, + orbitApiRequestAllItems, + resolveIdentities, +} from './GenericFunctions'; + +import { + activityFields, + activityOperations, +} from './ActivityDescription'; + +import { + memberFields, + memberOperations, +} from './MemberDescription'; + +import { + noteFields, + noteOperations, +} from './NoteDescription'; + +import { + postFields, + postOperations, +} from './PostDescription'; + +import * as moment from 'moment'; + +export class Orbit implements INodeType { + description: INodeTypeDescription = { + displayName: 'Orbit', + name: 'orbit', + icon: 'file:orbit.svg', + group: ['output'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Orbit API', + defaults: { + name: 'Orbit', + color: '#00ade8', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'orbitApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Activity', + value: 'activity', + }, + { + name: 'Member', + value: 'member', + }, + { + name: 'Note', + value: 'note', + }, + { + name: 'Post', + value: 'post', + }, + ], + default: 'member', + description: 'Resource to consume.', + }, + // ACTIVITY + ...activityOperations, + ...activityFields, + // MEMBER + ...memberOperations, + ...memberFields, + // NOTE + ...noteOperations, + ...noteFields, + // POST + ...postOperations, + ...postFields, + ], + }; + + methods = { + loadOptions: { + async getWorkspaces( + this: ILoadOptionsFunctions, + ): Promise { + const returnData: INodePropertyOptions[] = []; + const workspaces = await orbitApiRequest.call( + this, + 'GET', + '/workspaces', + ); + for (const workspace of workspaces.data) { + returnData.push({ + name: workspace.attributes.name, + value: workspace.id, + }); + } + return returnData; + }, + async getActivityTypes( + this: ILoadOptionsFunctions, + ): Promise { + const returnData: INodePropertyOptions[] = []; + const { data } = await orbitApiRequest.call( + this, + 'GET', + '/activity_types', + ); + for (const activityType of data) { + returnData.push({ + name: activityType.attributes.short_name, + value: activityType.id, + }); + } + return returnData; + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = items.length as unknown as number; + const qs: IDataObject = {}; + let responseData; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + for (let i = 0; i < length; i++) { + if (resource === 'activity') { + if (operation === 'create') { + const workspaceId = this.getNodeParameter('workspaceId', i) as string; + const memberId = this.getNodeParameter('memberId', i) as string; + const title = this.getNodeParameter('title', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body: IDataObject = { + title, + }; + if (additionalFields.description) { + body.description = additionalFields.description as string; + } + if (additionalFields.link) { + body.link = additionalFields.link as string; + } + if (additionalFields.linkText) { + body.link_text = additionalFields.linkText as string; + } + if (additionalFields.activityType) { + body.activity_type = additionalFields.activityType as string; + } + if (additionalFields.key) { + body.key = additionalFields.key as string; + } + if (additionalFields.occurredAt) { + body.occurred_at = additionalFields.occurredAt as string; + } + + responseData = await orbitApiRequest.call(this, 'POST', `/${workspaceId}/members/${memberId}/activities`, body); + responseData = responseData.data; + } + if (operation === 'getAll') { + const workspaceId = this.getNodeParameter('workspaceId', i) as string; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const filters = this.getNodeParameter('filters', i) as IDataObject; + let endpoint = `/${workspaceId}/activities`; + if (filters.memberId) { + endpoint = `/${workspaceId}/members/${filters.memberId}/activities`; + } + if (returnAll === true) { + responseData = await orbitApiRequestAllItems.call(this, 'data', 'GET', endpoint, {}, qs); + } else { + qs.limit = this.getNodeParameter('limit', 0) as boolean; + responseData = await orbitApiRequestAllItems.call(this, 'data', 'GET', endpoint, {}, qs); + responseData = responseData.splice(0, qs.limit); + } + } + } + if (resource === 'member') { + if (operation === 'upsert') { + const workspaceId = this.getNodeParameter('workspaceId', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const member: IDataObject = {}; + const identity: IDataObject = {}; + if (additionalFields.bio) { + member.bio = additionalFields.bio as string; + } + if (additionalFields.birthday) { + member.birthday = moment(additionalFields.birthday as string).format('MM-DD-YYYY'); + } + if (additionalFields.company) { + member.company = additionalFields.company as string; + } + if (additionalFields.location) { + member.location = additionalFields.location as string; + } + if (additionalFields.name) { + member.name = additionalFields.name as string; + } + if (additionalFields.bio) { + member.bio = additionalFields.bio as string; + } + if (additionalFields.pronouns) { + member.pronouns = additionalFields.pronouns as string; + } + if (additionalFields.shippingAddress) { + member.shipping_address = additionalFields.shippingAddress as string; + } + if (additionalFields.slug) { + member.slug = additionalFields.slug as string; + } + if (additionalFields.tagsToAdd) { + member.tags_to_add = additionalFields.tagsToAdd as string; + } + if (additionalFields.tagList) { + member.tag_list = additionalFields.tagList as string; + } + if (additionalFields.tshirt) { + member.tshirt = additionalFields.tshirt as string; + } + if (additionalFields.hasOwnProperty('teammate')) { + member.teammate = additionalFields.teammate as boolean; + } + if (additionalFields.url) { + member.url = additionalFields.url as string; + } + + const data = (this.getNodeParameter('identityUi', i) as IDataObject).identityValue as IDataObject; + if (data) { + if (['github', 'twitter', 'discourse'].includes(data.source as string)) { + identity.source = data.source as string; + const searchBy = data.searchBy as string; + if (searchBy === 'id') { + identity.uid = data.id as string; + } else { + identity.username = data.username as string; + } + if (data.source === 'discourse') { + identity.source_host = data.host as string; + } + } else { + //it's email + identity.email = data.email as string; + } + } + + responseData = await orbitApiRequest.call(this, 'POST', `/${workspaceId}/members`, { member, identity }); + responseData = responseData.data; + } + if (operation === 'delete') { + const workspaceId = this.getNodeParameter('workspaceId', i) as string; + const memberId = this.getNodeParameter('memberId', i) as string; + responseData = await orbitApiRequest.call(this, 'DELETE', `/${workspaceId}/members/${memberId}`); + responseData = { success: true }; + } + if (operation === 'get') { + const workspaceId = this.getNodeParameter('workspaceId', i) as string; + const memberId = this.getNodeParameter('memberId', i) as string; + const resolve = this.getNodeParameter('resolveIdentities', 0) as boolean; + responseData = await orbitApiRequest.call(this, 'GET', `/${workspaceId}/members/${memberId}`); + if (resolve === true) { + resolveIdentities(responseData); + } + responseData = responseData.data; + } + if (operation === 'getAll') { + const workspaceId = this.getNodeParameter('workspaceId', i) as string; + const returnAll = this.getNodeParameter('returnAll', 0) as boolean; + const options = this.getNodeParameter('options', i) as IDataObject; + Object.assign(qs, options); + qs.resolveIdentities = this.getNodeParameter('resolveIdentities', 0) as boolean; + if (returnAll === true) { + responseData = await orbitApiRequestAllItems.call(this, 'data', 'GET', `/${workspaceId}/members`, {}, qs); + } else { + qs.limit = this.getNodeParameter('limit', 0) as boolean; + responseData = await orbitApiRequestAllItems.call(this, 'data', 'GET', `/${workspaceId}/members`, {}, qs); + responseData = responseData.splice(0, qs.limit); + } + } + if (operation === 'lookup') { + const workspaceId = this.getNodeParameter('workspaceId', i) as string; + const source = this.getNodeParameter('source', i) as string; + + if (['github', 'twitter', 'discourse'].includes(source)) { + qs.source = this.getNodeParameter('source', i) as string; + const searchBy = this.getNodeParameter('searchBy', i) as string; + if (searchBy === 'id') { + qs.uid = this.getNodeParameter('id', i) as string; + } else { + qs.username = this.getNodeParameter('username', i) as string; + } + if (source === 'discourse') { + qs.source_host = this.getNodeParameter('host', i) as string; + } + } else { + //it's email + qs.email = this.getNodeParameter('email', i) as string; + } + + responseData = await orbitApiRequest.call(this, 'GET', `/${workspaceId}/members/find`, {}, qs); + responseData = responseData.data; + } + if (operation === 'update') { + const workspaceId = this.getNodeParameter('workspaceId', i) as string; + const memberId = this.getNodeParameter('memberId', i) as string; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + const body: IDataObject = { + }; + if (updateFields.bio) { + body.bio = updateFields.bio as string; + } + if (updateFields.birthday) { + body.birthday = moment(updateFields.birthday as string).format('MM-DD-YYYY'); + } + if (updateFields.company) { + body.company = updateFields.company as string; + } + if (updateFields.location) { + body.location = updateFields.location as string; + } + if (updateFields.name) { + body.name = updateFields.name as string; + } + if (updateFields.bio) { + body.bio = updateFields.bio as string; + } + if (updateFields.pronouns) { + body.pronouns = updateFields.pronouns as string; + } + if (updateFields.shippingAddress) { + body.shipping_address = updateFields.shippingAddress as string; + } + if (updateFields.slug) { + body.slug = updateFields.slug as string; + } + if (updateFields.tagsToAdd) { + body.tags_to_add = updateFields.tagsToAdd as string; + } + if (updateFields.tagList) { + body.tag_list = updateFields.tagList as string; + } + if (updateFields.tshirt) { + body.tshirt = updateFields.tshirt as string; + } + if (updateFields.hasOwnProperty('teammate')) { + body.teammate = updateFields.teammate as boolean; + } + if (updateFields.url) { + body.url = updateFields.url as string; + } + + responseData = await orbitApiRequest.call(this, 'PUT', `/${workspaceId}/members/${memberId}`, body); + responseData = { success: true }; + } + } + if (resource === 'note') { + if (operation === 'create') { + const workspaceId = this.getNodeParameter('workspaceId', i) as string; + const memberId = this.getNodeParameter('memberId', i) as string; + const note = this.getNodeParameter('note', i) as string; + + responseData = await orbitApiRequest.call(this, 'POST', `/${workspaceId}/members/${memberId}/notes`, { body: note }); + responseData = responseData.data; + } + if (operation === 'getAll') { + const workspaceId = this.getNodeParameter('workspaceId', i) as string; + const memberId = this.getNodeParameter('memberId', i) as string; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + qs.resolveMember = this.getNodeParameter('resolveMember', 0) as boolean; + if (returnAll === true) { + responseData = await orbitApiRequestAllItems.call(this, 'data', 'GET', `/${workspaceId}/members/${memberId}/notes`, {}, qs); + } else { + qs.limit = this.getNodeParameter('limit', 0) as boolean; + responseData = await orbitApiRequestAllItems.call(this, 'data', 'GET', `/${workspaceId}/members/${memberId}/notes`, {}, qs); + responseData = responseData.splice(0, qs.limit); + } + } + if (operation === 'update') { + const workspaceId = this.getNodeParameter('workspaceId', i) as string; + const memberId = this.getNodeParameter('memberId', i) as string; + const noteId = this.getNodeParameter('noteId', i) as string; + const note = this.getNodeParameter('note', i) as string; + + responseData = await orbitApiRequest.call(this, 'PUT', `/${workspaceId}/members/${memberId}/notes/${noteId}`, { body: note }); + responseData = { success: true }; + } + } + if (resource === 'post') { + if (operation === 'create') { + const workspaceId = this.getNodeParameter('workspaceId', i) as string; + const memberId = this.getNodeParameter('memberId', i) as string; + const url = this.getNodeParameter('url', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body: IDataObject = { + url, + }; + if (additionalFields.publishedAt) { + body.published_at = additionalFields.publishedAt as string; + } + + responseData = await orbitApiRequest.call(this, 'POST', `/${workspaceId}/members/${memberId}/posts`, body); + responseData = responseData.data; + } + if (operation === 'getAll') { + const workspaceId = this.getNodeParameter('workspaceId', i) as string; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const filters = this.getNodeParameter('filters', i) as IDataObject; + let endpoint = `/${workspaceId}/posts`; + if (filters.memberId) { + endpoint = `/${workspaceId}/members/${filters.memberId}/posts`; + } + if (returnAll === true) { + responseData = await orbitApiRequestAllItems.call(this, 'data', 'GET', endpoint, {}, qs); + } else { + qs.limit = this.getNodeParameter('limit', 0) as boolean; + responseData = await orbitApiRequestAllItems.call(this, 'data', 'GET', endpoint, {}, qs); + responseData = responseData.splice(0, qs.limit); + } + } + if (operation === 'delete') { + const workspaceId = this.getNodeParameter('workspaceId', i) as string; + const memberId = this.getNodeParameter('memberId', i) as string; + const postId = this.getNodeParameter('postId', i) as string; + + responseData = await orbitApiRequest.call(this, 'DELETE', `/${workspaceId}/members/${memberId}/posts/${postId}`); + responseData = { success: true }; + } + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else { + returnData.push(responseData as IDataObject); + } + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Orbit/PostDescription.ts b/packages/nodes-base/nodes/Orbit/PostDescription.ts new file mode 100644 index 0000000000..7cb798163a --- /dev/null +++ b/packages/nodes-base/nodes/Orbit/PostDescription.ts @@ -0,0 +1,273 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const postOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'post', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a post', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all posts', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a post', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const postFields = [ + + /* -------------------------------------------------------------------------- */ + /* post:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Workspace', + name: 'workspaceId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getWorkspaces', + }, + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'post', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Member ID', + name: 'memberId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'post', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'URL', + name: 'url', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'post', + ], + operation: [ + 'create', + ], + }, + }, + description: 'Supply any URL and Orbit will do its best job to parse out a title, description, and image.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'post', + ], + operation: [ + 'create', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Published At', + name: 'publishedAt', + type: 'dateTime', + default: '', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* post:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Workspace', + name: 'workspaceId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getWorkspaces', + }, + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'post', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'post', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'post', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + resource: [ + 'post', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Member ID', + name: 'memberId', + type: 'string', + default: '', + description: 'When set the post will be filtered by the member ID.', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* post:delete */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Workspace', + name: 'workspaceId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getWorkspaces', + }, + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'post', + ], + operation: [ + 'delete', + ], + }, + }, + }, + { + displayName: 'Member ID', + name: 'memberId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'post', + ], + operation: [ + 'delete', + ], + }, + }, + }, + { + displayName: 'Post ID', + name: 'postId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource: [ + 'post', + ], + }, + }, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Orbit/orbit.svg b/packages/nodes-base/nodes/Orbit/orbit.svg new file mode 100644 index 0000000000..8811902286 --- /dev/null +++ b/packages/nodes-base/nodes/Orbit/orbit.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index af0be08da1..7daa70ae6c 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -136,6 +136,7 @@ "dist/credentials/OAuth1Api.credentials.js", "dist/credentials/OAuth2Api.credentials.js", "dist/credentials/OpenWeatherMapApi.credentials.js", + "dist/credentials/OrbitApi.credentials.js", "dist/credentials/PaddleApi.credentials.js", "dist/credentials/PagerDutyApi.credentials.js", "dist/credentials/PagerDutyOAuth2Api.credentials.js", @@ -335,6 +336,7 @@ "dist/nodes/NextCloud/NextCloud.node.js", "dist/nodes/NoOp.node.js", "dist/nodes/OpenWeatherMap.node.js", + "dist/nodes/Orbit/Orbit.node.js", "dist/nodes/Paddle/Paddle.node.js", "dist/nodes/PagerDuty/PagerDuty.node.js", "dist/nodes/PayPal/PayPal.node.js",