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..350531f78f --- /dev/null +++ b/packages/nodes-base/nodes/Cockpit/Cockpit.node.ts @@ -0,0 +1,163 @@ +import { IExecuteFunctions } from 'n8n-core'; +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; +import { + collectionFields, + collectionOperations, +} from './CollectionDescription'; +import { + createCollectionEntry, + getAllCollectionEntries, + getAllCollectionNames, +} from './CollectionFunctions'; +import { + formFields, + formOperations +} from './FormDescription'; +import { submitForm } from './FormFunctions'; +import { + singletonFields, + singletonOperations, +} from './SingletonDescription'; +import { + getAllSingleton, + getAllSingletonNames, +} 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"] }}', + 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', + }, + ], + }, + + + ...collectionOperations, + ...collectionFields, + ...formOperations, + ...formFields, + ...singletonOperations, + ...singletonFields, + ], + }; + + + methods = { + loadOptions: { + async getCollections(this: ILoadOptionsFunctions): Promise { + const collections = await getAllCollectionNames.call(this); + + return collections.map(itemName => { + return { + name: itemName, + value: itemName, + } + }); + }, + + async getSingletons(this: ILoadOptionsFunctions): Promise { + const singletons = await getAllSingletonNames.call(this); + + return singletons.map(itemName => { + return { + name: itemName, + value: itemName, + } + }); + }, + }, + }; + + 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 operation = this.getNodeParameter('operation', 0) as string; + + let responseData; + + for (let i = 0; i < length; i++) { + if (resource === 'collections') { + const collectionName = this.getNodeParameter('collection', i) as string; + if (operation === 'create') { + const data = this.getNodeParameter('data', i) as IDataObject; + + responseData = await createCollectionEntry.call(this, collectionName, data); + } else if (operation === 'getAll') { + const options = this.getNodeParameter('options', i) as IDataObject; + + responseData = await getAllCollectionEntries.call(this, collectionName, options); + } else if (operation === 'update') { + const id = this.getNodeParameter('id', i) as string; + const data = this.getNodeParameter('data', i) as IDataObject; + + responseData = await createCollectionEntry.call(this, collectionName, data, id); + } + } else if (resource === 'forms') { + const formName = this.getNodeParameter('form', i) as string; + if (operation === 'submit') { + const form = this.getNodeParameter('form', i) as IDataObject; + + responseData = await submitForm.call(this, formName, form); + } + } else if (resource === 'singletons') { + const singletonName = this.getNodeParameter('singleton', i) as string; + if (operation === 'getAll') { + responseData = await getAllSingleton.call(this, singletonName); + } + } + + 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..cdcba40d2c --- /dev/null +++ b/packages/nodes-base/nodes/Cockpit/CollectionDescription.ts @@ -0,0 +1,212 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const collectionOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'collections', + ], + }, + }, + options: [ + { + name: 'Create an entry', + value: 'create', + description: 'Create a collection entry', + }, + { + name: 'Get all entries', + value: 'getAll', + description: 'Get all collection entries', + }, + { + name: 'Update an entry', + value: 'update', + description: 'Update a collection entries', + }, + ], + default: 'getAll', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const collectionFields = [ + { + displayName: 'Collection', + name: 'collection', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getCollections', + }, + displayOptions: { + show: { + resource: [ + 'collections', + ], + }, + }, + required: true, + description: 'Name of the collection to operate on.' + }, + + // Collections:entry:create + { + displayName: 'Data', + name: 'data', + type: 'json', + required: true, + default: '', + typeOptions: { + alwaysOpenEditWindow: true, + }, + displayOptions: { + show: { + resource: [ + 'collections', + ], + operation: [ + 'create', + ] + }, + }, + description: 'The data to create.', + }, + + // Collections:entry:getAll + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource: [ + 'collections', + ], + operation: [ + 'getAll', + ] + }, + }, + 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: 'Language', + name: 'language', + type: 'string', + default: '', + description: 'Return normalized language fields.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: '', + description: 'Limit number of returned entries.', + }, + { + displayName: 'Populate', + name: 'populate', + type: 'boolean', + required: true, + default: true, + description: 'Resolve linked collection items.', + }, + { + displayName: 'RAW Data', + name: 'rawData', + type: 'boolean', + default: false, + description: `Returns the data exactly in the way it got received from the API.`, + }, + { + displayName: 'Simple', + name: 'simple', + type: 'boolean', + required: true, + default: true, + description: 'Return only result 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.', + }, + ], + }, + + // 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..b2b8531b0f --- /dev/null +++ b/packages/nodes-base/nodes/Cockpit/CollectionFunctions.ts @@ -0,0 +1,73 @@ +import { + IExecuteFunctions, + IExecuteSingleFunctions, + ILoadOptionsFunctions +} from 'n8n-core'; +import { IDataObject } from 'n8n-workflow'; +import { ICollection } from './CollectionInterface'; +import { cockpitApiRequest } from './GenericFunctions'; + +export async function createCollectionEntry(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 getAllCollectionEntries(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, resourceName: string, options: IDataObject): Promise { // tslint:disable-line:no-any + const body: ICollection = {}; + + if (options.fields) { + body.fields = JSON.parse(options.fields.toString()); + } + + if (options.filter) { + body.filter = JSON.parse(options.filter.toString()); + } + + if (options.limit) { + body.limit = options.limit as number; + } + + if (options.skip) { + body.skip = options.skip as number; + } + + if (options.sort) { + body.sort = JSON.parse(options.sort.toString()); + } + + if (options.populate) { + body.populate = options.populate as boolean; + } + + if (options.simple) { + body.simple = options.simple as boolean; + } + + if (options.language) { + body.lang = options.language as string; + } + + const resultData = await cockpitApiRequest.call(this, 'post', `/collections/get/${resourceName}`, body); + + if (options.rawData === true) { + return resultData; + } + + return (resultData as unknown as IDataObject).entries; +} + + +export async function getAllCollectionNames(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions): Promise { + return cockpitApiRequest.call(this, 'GET', `/collections/listCollections`, {}); +} \ No newline at end of file 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..8a1e6284c7 --- /dev/null +++ b/packages/nodes-base/nodes/Cockpit/FormDescription.ts @@ -0,0 +1,64 @@ +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 = [ + { + displayName: 'Form', + name: 'form', + type: 'string', + displayOptions: { + show: { + resource: [ + 'forms', + ], + }, + }, + default: '', + required: true, + description: 'Name of the form to operate on.' + }, + + // 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..ed923d3bda --- /dev/null +++ b/packages/nodes-base/nodes/Cockpit/GenericFunctions.ts @@ -0,0 +1,46 @@ +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) { + let errorMessage = error.message; + if (error.error) { + errorMessage = error.error.message || error.error.error; + } + + throw new Error(`Cockpit error [${error.statusCode}]: ` + 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..402e23747a --- /dev/null +++ b/packages/nodes-base/nodes/Cockpit/SingletonDescription.ts @@ -0,0 +1,46 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const singletonOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'singletons', + ], + }, + }, + options: [ + { + name: 'Get All', + value: 'getAll', + description: 'Get all singletons', + }, + ], + default: 'getAll', + description: 'The operation to perform.', + } +] as INodeProperties[]; + +export const singletonFields = [ + { + displayName: 'Singleton', + name: 'singleton', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getSingletons', + }, + displayOptions: { + show: { + resource: [ + 'singletons', + ], + }, + }, + required: true, + description: 'Name of the singleton to operate on.' + }, +] as INodeProperties[]; \ No newline at end of file diff --git a/packages/nodes-base/nodes/Cockpit/SingletonFunctions.ts b/packages/nodes-base/nodes/Cockpit/SingletonFunctions.ts new file mode 100644 index 0000000000..5a5cf2da0e --- /dev/null +++ b/packages/nodes-base/nodes/Cockpit/SingletonFunctions.ts @@ -0,0 +1,14 @@ +import { + IExecuteFunctions, + IExecuteSingleFunctions, + ILoadOptionsFunctions +} from 'n8n-core'; +import { cockpitApiRequest } from './GenericFunctions'; + +export async function getAllSingleton(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, resourceName: string): Promise { // tslint:disable-line:no-any + return cockpitApiRequest.call(this, 'get', `/singletons/get/${resourceName}`); +} + +export async function getAllSingletonNames(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions): Promise { + return cockpitApiRequest.call(this, 'GET', `/singletons/listSingletons`, {}); +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/Cockpit/cockpit.png b/packages/nodes-base/nodes/Cockpit/cockpit.png new file mode 100644 index 0000000000..ddbe6ead67 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",