diff --git a/packages/nodes-base/credentials/CockpitApi.credentials.ts b/packages/nodes-base/credentials/CockpitApi.credentials.ts new file mode 100644 index 0000000000..fcc76f4ef5 --- /dev/null +++ b/packages/nodes-base/credentials/CockpitApi.credentials.ts @@ -0,0 +1,24 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class CockpitApi implements ICredentialType { + name = 'cockpitApi'; + displayName = 'Cockpit API'; + properties = [ + { + displayName: 'Cockpit URL', + name: 'url', + type: 'string' as NodePropertyTypes, + default: '', + placeholder: 'https://example.com', + }, + { + displayName: 'Access Token', + name: 'accessToken', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Cockpit/Cockpit.node.ts b/packages/nodes-base/nodes/Cockpit/Cockpit.node.ts new file mode 100644 index 0000000000..7680f85d94 --- /dev/null +++ b/packages/nodes-base/nodes/Cockpit/Cockpit.node.ts @@ -0,0 +1,130 @@ +import { IExecuteFunctions } from 'n8n-core'; +import { + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription +} from 'n8n-workflow'; +import { + collectionFields, + collectionOperations +} from './CollectionDescription'; +import { + getCollectionEntries, + saveCollectionEntry +} from './CollectionFunctions'; +import { + formFields, + formOperations +} from './FormDescription'; +import { submitForm } from './FormFunctions'; +import { singletonOperations } from "./SingletonDescription"; +import { getSingleton } from "./SingletonFunctions"; + +export class Cockpit implements INodeType { + description: INodeTypeDescription = { + displayName: 'Cockpit', + name: 'cockpit', + icon: 'file:cockpit.png', + group: ['output'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"] + "/" + $parameter["resourceName"]}}', + description: 'Consume Cockpit API', + defaults: { + name: 'Cockpit', + color: '#000000', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'cockpitApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + default: 'collections', + description: 'Resource to consume.', + options: [ + { + name: 'Collection', + value: 'collections', + }, + { + name: 'Form', + value: 'forms', + }, + { + name: 'Singleton', + value: 'singletons', + }, + ], + }, + { + displayName: 'Resource name', + name: 'resourceName', + type: 'string', + default: '', + required: true, + description: 'Name of resource to consume.' + }, + ...collectionOperations, + ...collectionFields, + ...formOperations, + ...formFields, + ...singletonOperations, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = items.length as unknown as number; + const resource = this.getNodeParameter('resource', 0) as string; + const resourceName = this.getNodeParameter('resourceName', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + let responseData; + + for (let i = 0; i < length; i++) { + if (resource === 'collections') { + if (operation === 'save') { + const data = this.getNodeParameter('data', i) as IDataObject; + + responseData = await saveCollectionEntry.call(this, resourceName, data); + } else if (operation === 'get') { + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + responseData = await getCollectionEntries.call(this, resourceName, additionalFields); + } else if (operation === 'update') { + const id = this.getNodeParameter('id', i) as string; + const data = this.getNodeParameter('data', i) as IDataObject; + + responseData = await saveCollectionEntry.call(this, resourceName, data, id); + } + } else if (resource === 'forms') { + if (operation === 'submit') { + const form = this.getNodeParameter('form', i) as IDataObject; + + responseData = await submitForm.call(this, resourceName, form); + } + } else if (resource === 'singletons') { + if (operation === 'get') { + responseData = await getSingleton.call(this, resourceName); + } + } + + 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/Cockpit/CollectionDescription.ts b/packages/nodes-base/nodes/Cockpit/CollectionDescription.ts new file mode 100644 index 0000000000..71483c3095 --- /dev/null +++ b/packages/nodes-base/nodes/Cockpit/CollectionDescription.ts @@ -0,0 +1,186 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const collectionOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'collections', + ], + }, + }, + options: [ + { + name: 'Create an entry', + value: 'save', + description: 'Create a collection entry', + }, + { + name: 'Get all entries', + value: 'get', + description: 'Get all collection entries', + }, + { + name: 'Update an entry', + value: 'update', + description: 'Update a collection entries', + }, + ], + default: 'get', + description: 'The operation to perform.', + } +] as INodeProperties[]; + +export const collectionFields = [ + // Collections:entry:save + { + displayName: 'Data', + name: 'data', + type: 'json', + required: true, + default: '', + typeOptions: { + alwaysOpenEditWindow: true, + }, + displayOptions: { + show: { + resource: [ + 'collections', + ], + operation: [ + 'save', + ] + }, + }, + description: 'The data to save.', + }, + + // Collections:entry:get + { + displayName: 'Additional fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add field', + default: {}, + displayOptions: { + show: { + resource: [ + 'collections', + ], + operation: [ + 'get', + ] + }, + }, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'json', + default: '', + typeOptions: { + alwaysOpenEditWindow: true, + }, + description: 'Fields to get.', + }, + { + displayName: 'Filter', + name: 'filter', + type: 'json', + default: '', + typeOptions: { + alwaysOpenEditWindow: true, + }, + description: 'Filter result by fields.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: '', + description: 'Limit number of returned entries.', + }, + { + displayName: 'Skip', + name: 'skip', + type: 'number', + default: '', + description: 'Skip number of entries.', + }, + { + displayName: 'Sort', + name: 'sort', + type: 'json', + default: '', + description: 'Sort result by fields.', + }, + { + displayName: 'Populate', + name: 'populate', + type: 'boolean', + required: true, + default: true, + description: 'Resolve linked collection items.', + }, + { + displayName: 'Simple', + name: 'simple', + type: 'boolean', + required: true, + default: true, + description: 'Return only result entries.', + }, + { + displayName: 'Language', + name: 'language', + type: 'string', + default: '', + description: 'Return normalized language fields.', + }, + ], + }, + + // Collections:entry:update + { + displayName: 'Entry ID', + name: 'id', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'collections', + ], + operation: [ + 'update', + ] + }, + }, + description: 'The entry ID.', + }, + { + displayName: 'Data', + name: 'data', + type: 'json', + required: true, + default: '', + typeOptions: { + alwaysOpenEditWindow: true, + }, + displayOptions: { + show: { + resource: [ + 'collections', + ], + operation: [ + 'update', + ] + }, + }, + description: 'The data to update.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Cockpit/CollectionFunctions.ts b/packages/nodes-base/nodes/Cockpit/CollectionFunctions.ts new file mode 100644 index 0000000000..fff760ba02 --- /dev/null +++ b/packages/nodes-base/nodes/Cockpit/CollectionFunctions.ts @@ -0,0 +1,61 @@ +import { + IExecuteFunctions, + IExecuteSingleFunctions, + ILoadOptionsFunctions +} from 'n8n-core'; +import { IDataObject } from 'n8n-workflow'; +import { ICollection } from './CollectionInterface'; +import { cockpitApiRequest } from './GenericFunctions'; + +export async function saveCollectionEntry(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, resourceName: string, data: IDataObject, id?: string): Promise { // tslint:disable-line:no-any + const body: ICollection = { + data: JSON.parse(data.toString()) + }; + + if (id) { + body.data = { + _id: id, + ...body.data + }; + } + + return cockpitApiRequest.call(this, 'post', `/collections/save/${resourceName}`, body); +} + +export async function getCollectionEntries(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, resourceName: string, additionalFields: IDataObject): Promise { // tslint:disable-line:no-any + const body: ICollection = {}; + + if (additionalFields.fields) { + body.fields = JSON.parse(additionalFields.fields.toString()); + } + + if (additionalFields.filter) { + body.filter = JSON.parse(additionalFields.filter.toString()); + } + + if (additionalFields.limit) { + body.limit = additionalFields.limit as number; + } + + if (additionalFields.skip) { + body.skip = additionalFields.skip as number; + } + + if (additionalFields.sort) { + body.sort = JSON.parse(additionalFields.sort.toString()); + } + + if (additionalFields.populate) { + body.populate = additionalFields.populate as boolean; + } + + if (additionalFields.simple) { + body.simple = additionalFields.simple as boolean; + } + + if (additionalFields.language) { + body.lang = additionalFields.language as string; + } + + return cockpitApiRequest.call(this, 'post', `/collections/get/${resourceName}`, body); +} diff --git a/packages/nodes-base/nodes/Cockpit/CollectionInterface.ts b/packages/nodes-base/nodes/Cockpit/CollectionInterface.ts new file mode 100644 index 0000000000..834cbbb3b7 --- /dev/null +++ b/packages/nodes-base/nodes/Cockpit/CollectionInterface.ts @@ -0,0 +1,11 @@ +export interface ICollection { + fields?: object; + filter?: object; + limit?: number; + skip?: number; + sort?: object; + populate?: boolean; + simple?: boolean; + lang?: string; + data?: object; +} diff --git a/packages/nodes-base/nodes/Cockpit/FormDescription.ts b/packages/nodes-base/nodes/Cockpit/FormDescription.ts new file mode 100644 index 0000000000..cdb1266e38 --- /dev/null +++ b/packages/nodes-base/nodes/Cockpit/FormDescription.ts @@ -0,0 +1,48 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const formOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'forms', + ], + }, + }, + options: [ + { + name: 'Submit a form', + value: 'submit', + description: 'Store submission of a form', + }, + + ], + default: 'submit', + description: 'The operation to perform.', + } +] as INodeProperties[]; + +export const formFields = [ + // Forms:submit + { + displayName: 'Form data', + name: 'form', + type: 'json', + required: true, + default: '', + typeOptions: { + alwaysOpenEditWindow: true, + }, + displayOptions: { + show: { + resource: [ + 'forms', + ], + }, + }, + description: 'The data to save.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Cockpit/FormFunctions.ts b/packages/nodes-base/nodes/Cockpit/FormFunctions.ts new file mode 100644 index 0000000000..437ed210a0 --- /dev/null +++ b/packages/nodes-base/nodes/Cockpit/FormFunctions.ts @@ -0,0 +1,16 @@ +import { + IExecuteFunctions, + IExecuteSingleFunctions, + ILoadOptionsFunctions +} from 'n8n-core'; +import { IDataObject } from 'n8n-workflow'; +import { IForm } from './FormInterface'; +import { cockpitApiRequest } from './GenericFunctions'; + +export async function submitForm(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, resourceName: string, form: IDataObject) { + const body: IForm = { + form: JSON.parse(form.toString()) + }; + + return cockpitApiRequest.call(this, 'post', `/forms/submit/${resourceName}`, body); +} diff --git a/packages/nodes-base/nodes/Cockpit/FormInterface.ts b/packages/nodes-base/nodes/Cockpit/FormInterface.ts new file mode 100644 index 0000000000..d1c218ae9d --- /dev/null +++ b/packages/nodes-base/nodes/Cockpit/FormInterface.ts @@ -0,0 +1,3 @@ +export interface IForm { + form: object; +} diff --git a/packages/nodes-base/nodes/Cockpit/GenericFunctions.ts b/packages/nodes-base/nodes/Cockpit/GenericFunctions.ts new file mode 100644 index 0000000000..db37b57ab9 --- /dev/null +++ b/packages/nodes-base/nodes/Cockpit/GenericFunctions.ts @@ -0,0 +1,43 @@ +import { + IExecuteFunctions, + IExecuteSingleFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; +import { IDataObject } from 'n8n-workflow'; +import { OptionsWithUri } from 'request'; + +export async function cockpitApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('cockpitApi'); + + if (credentials === undefined) { + throw new Error('No credentials available.'); + } + + let options: OptionsWithUri = { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + method, + qs: { + token: credentials!.accessToken + }, + body, + uri: uri || `${credentials!.url}/api${resource}`, + json: true + }; + + options = Object.assign({}, options, option); + + if (Object.keys(options.body).length === 0) { + delete options.body; + } + + try { + return await this.helpers.request!(options); + } catch (error) { + const errorMessage = error.error.message || error.error.error; + + throw new Error('Cockpit error: ' + errorMessage); + } +} diff --git a/packages/nodes-base/nodes/Cockpit/SingletonDescription.ts b/packages/nodes-base/nodes/Cockpit/SingletonDescription.ts new file mode 100644 index 0000000000..d10edafc30 --- /dev/null +++ b/packages/nodes-base/nodes/Cockpit/SingletonDescription.ts @@ -0,0 +1,25 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const singletonOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'singletons', + ], + }, + }, + options: [ + { + name: 'Get data', + value: 'get', + description: 'Get singleton data', + }, + ], + default: 'get', + description: 'The operation to perform.', + } +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Cockpit/SingletonFunctions.ts b/packages/nodes-base/nodes/Cockpit/SingletonFunctions.ts new file mode 100644 index 0000000000..5d07bb4290 --- /dev/null +++ b/packages/nodes-base/nodes/Cockpit/SingletonFunctions.ts @@ -0,0 +1,10 @@ +import { + IExecuteFunctions, + IExecuteSingleFunctions, + ILoadOptionsFunctions +} from 'n8n-core'; +import { cockpitApiRequest } from './GenericFunctions'; + +export async function getSingleton(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, resourceName: string): Promise { // tslint:disable-line:no-any + return cockpitApiRequest.call(this, 'get', `/singletons/get/${resourceName}`); +} diff --git a/packages/nodes-base/nodes/Cockpit/cockpit.png b/packages/nodes-base/nodes/Cockpit/cockpit.png new file mode 100644 index 0000000000..b99e95037c Binary files /dev/null and b/packages/nodes-base/nodes/Cockpit/cockpit.png differ diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 908b5699f9..e197bd6ca4 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -39,6 +39,7 @@ "dist/credentials/ClearbitApi.credentials.js", "dist/credentials/ClickUpApi.credentials.js", "dist/credentials/ClockifyApi.credentials.js", + "dist/credentials/CockpitApi.credentials.js", "dist/credentials/CodaApi.credentials.js", "dist/credentials/CopperApi.credentials.js", "dist/credentials/CalendlyApi.credentials.js", @@ -131,6 +132,7 @@ "dist/nodes/ClickUp/ClickUp.node.js", "dist/nodes/ClickUp/ClickUpTrigger.node.js", "dist/nodes/Clockify/ClockifyTrigger.node.js", + "dist/nodes/Cockpit/Cockpit.node.js", "dist/nodes/Coda/Coda.node.js", "dist/nodes/Copper/CopperTrigger.node.js", "dist/nodes/Cron.node.js",