diff --git a/packages/nodes-base/credentials/DemioApi.credentials.ts b/packages/nodes-base/credentials/DemioApi.credentials.ts new file mode 100644 index 0000000000..dc122e2f44 --- /dev/null +++ b/packages/nodes-base/credentials/DemioApi.credentials.ts @@ -0,0 +1,24 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class DemioApi implements ICredentialType { + name = 'demioApi'; + displayName = 'Demio API'; + documentationUrl = 'demio'; + properties = [ + { + displayName: 'API Key', + name: 'apiKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'API Secret', + name: 'apiSecret', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Demio/Demio.node.ts b/packages/nodes-base/nodes/Demio/Demio.node.ts new file mode 100644 index 0000000000..f0b21b483d --- /dev/null +++ b/packages/nodes-base/nodes/Demio/Demio.node.ts @@ -0,0 +1,211 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + demioApiRequest, +} from './GenericFunctions'; + +import { + eventFields, + eventOperations, +} from './EventDescription'; + +import { + reportFields, + reportOperations, +} from './ReportDescription'; + +export class Demio implements INodeType { + description: INodeTypeDescription = { + displayName: 'Demio', + name: 'demio', + icon: 'file:demio.svg', + group: ['output'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume the Demio API', + defaults: { + name: 'Demio', + color: '#02bf6f', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'demioApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Event', + value: 'event', + }, + { + name: 'Report', + value: 'report', + }, + ], + default: 'event', + description: 'Resource to consume.', + }, + // Event + ...eventOperations, + ...eventFields, + // Report + ...reportOperations, + ...reportFields, + ], + }; + + methods = { + loadOptions: { + // Get all the events to display them to user so that he can + // select them easily + async getEvents( + this: ILoadOptionsFunctions, + ): Promise { + const returnData: INodePropertyOptions[] = []; + const events = await demioApiRequest.call( + this, + 'GET', + `/events`, + {}, + { type: 'upcoming' }, + ); + for (const event of events) { + returnData.push({ + name: event.name, + value: event.id, + }); + } + return returnData; + }, + + // Get all the sessions to display them to user so that he can + // select them easily + async getEventSessions( + this: ILoadOptionsFunctions, + ): Promise { + const eventId = this.getCurrentNodeParameter('eventId') as string; + const qs: IDataObject = {}; + + const resource = this.getCurrentNodeParameter('resource') as string; + + if (resource !== 'report') { + qs.active = true; + } + + const returnData: INodePropertyOptions[] = []; + const { dates } = await demioApiRequest.call( + this, + 'GET', + `/event/${eventId}`, + {}, + ); + for (const date of dates) { + returnData.push({ + name: date.datetime, + value: date.date_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 === 'event') { + if (operation === 'get') { + const id = this.getNodeParameter('eventId', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (additionalFields.date_id !== undefined) { + responseData = await demioApiRequest.call(this, 'GET', `/event/${id}/date/${additionalFields.date_id}`); + } else { + Object.assign(qs, additionalFields); + responseData = await demioApiRequest.call(this, 'GET', `/event/${id}`, {}, qs); + } + } + if (operation === 'getAll') { + const filters = this.getNodeParameter('filters', i) as IDataObject; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + Object.assign(qs, filters); + + responseData = await demioApiRequest.call(this, 'GET', `/events`, {}, qs); + + if (returnAll === false) { + const limit = this.getNodeParameter('limit', i) as number; + responseData = responseData.splice(0, limit); + } + + } + if (operation === 'register') { + const eventId = this.getNodeParameter('eventId', i) as string; + const firstName = this.getNodeParameter('firstName', i) as string; + const email = this.getNodeParameter('email', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const body: IDataObject = { + name: firstName, + email, + id: eventId, + }; + + Object.assign(body, additionalFields); + + if (additionalFields.customFieldsUi) { + const customFields = (additionalFields.customFieldsUi as IDataObject || {}).customFieldsValues as IDataObject[] || []; + const data = customFields.reduce((obj, value) => Object.assign(obj, { [`${value.fieldId}`]: value.value }), {}); + Object.assign(body, data); + delete additionalFields.customFields; + } + + responseData = await demioApiRequest.call(this, 'PUT', `/event/register`, body); + } + } + if (resource === 'report') { + if (operation === 'get') { + const sessionId = this.getNodeParameter('dateId', i) as string; + const filters = this.getNodeParameter('filters', i) as IDataObject; + + Object.assign(qs, filters); + + responseData = await demioApiRequest.call(this, 'GET', `/report/${sessionId}/participants`, {}, qs); + responseData = responseData.participants; + } + } + 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/Demio/EventDescription.ts b/packages/nodes-base/nodes/Demio/EventDescription.ts new file mode 100644 index 0000000000..882ec45dad --- /dev/null +++ b/packages/nodes-base/nodes/Demio/EventDescription.ts @@ -0,0 +1,345 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const eventOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'event', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get an event', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all events', + }, + { + name: 'Register', + value: 'register', + description: 'Register someone to an event', + }, + ], + default: 'get', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const eventFields = [ + + /* -------------------------------------------------------------------------- */ + /* event:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'event', + ], + }, + }, + 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: [ + 'event', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Automated', + value: 'automated', + }, + { + name: 'Past', + value: 'past', + }, + { + name: 'Upcoming', + value: 'upcoming', + }, + ], + default: '', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* event:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Event ID', + name: 'eventId', + type: 'string', + default: '', + required: true, + description: 'Event ID', + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'get', + ], + }, + }, + options: [ + { + displayName: 'Active', + name: 'active', + type: 'boolean', + default: false, + description: 'Return only active dates in series', + }, + { + displayName: 'Session ID', + name: 'date_id', + type: 'string', + default: '', + description: 'Event Date ID', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* event:register */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Event ID', + name: 'eventId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getEvents', + }, + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'register', + ], + }, + }, + default: '', + description: 'Event ID', + }, + { + displayName: 'First Name', + name: 'firstName', + type: 'string', + default: '', + required: true, + description: 'The registrant\'s first name', + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'register', + ], + }, + }, + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + required: true, + description: 'The registrant\'s email address', + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'register', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'event', + ], + operation: [ + 'register', + ], + }, + }, + options: [ + { + displayName: 'Company', + name: 'company', + type: 'string', + default: '', + description: 'The value for the predefined Company field.', + }, + { + displayName: 'Custom Fields', + name: 'customFieldsUi', + placeholder: 'Add Field', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + name: 'customFieldsValues', + displayName: 'Custom Field', + values: [ + { + displayName: 'Field ID', + name: 'fieldId', + type: 'string', + default: '', + description: 'Each custom field\'s unique identifier
can be found within the Event\'s Registration block in the Customize tab.', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'The value to set on custom field.', + }, + ], + }, + ], + }, + { + displayName: 'Event Registration URL', + name: 'ref_url', + type: 'string', + default: '', + description: 'Event Registration page URL. It can be useful when you
do not know Event ID, but have Event link.', + }, + { + displayName: 'GDPR', + name: 'gdpr', + type: 'string', + default: '', + description: 'The value for the predefined GDPR field.', + }, + { + displayName: 'Last Name', + name: 'last_name', + type: 'string', + default: '', + description: 'The value for the predefined Last Name field.', + }, + { + displayName: 'Phone Number', + name: 'phone_number', + type: 'string', + default: '', + description: 'The value for the predefined Phone Number field.', + }, + { + displayName: 'Session ID', + name: 'date_id', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getEventSessions', + loadOptionsDependsOn: [ + 'eventId', + ], + }, + default: '', + description: 'Event Session ID', + }, + { + displayName: 'Website', + name: 'website', + type: 'string', + default: '', + description: 'The value for the predefined Website field.', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Demio/GenericFunctions.ts b/packages/nodes-base/nodes/Demio/GenericFunctions.ts new file mode 100644 index 0000000000..3d96faacca --- /dev/null +++ b/packages/nodes-base/nodes/Demio/GenericFunctions.ts @@ -0,0 +1,48 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + IHookFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +export async function demioApiRequest(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('demioApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + let options: OptionsWithUri = { + headers: { + 'Api-Key': credentials.apiKey, + 'Api-Secret': credentials.apiSecret, + }, + method, + qs, + body, + uri: uri || `https://my.demio.com/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(`Demio error response [${error.statusCode}]: ${errorBody.message}`); + } + + // Expected error data did not get returned so throw the actual error + throw error; + } +} diff --git a/packages/nodes-base/nodes/Demio/ReportDescription.ts b/packages/nodes-base/nodes/Demio/ReportDescription.ts new file mode 100644 index 0000000000..b83982f220 --- /dev/null +++ b/packages/nodes-base/nodes/Demio/ReportDescription.ts @@ -0,0 +1,124 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const reportOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'report', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get an event report', + }, + ], + default: 'get', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const reportFields = [ + + /* -------------------------------------------------------------------------- */ + /* report:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Event ID', + name: 'eventId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getEvents', + }, + displayOptions: { + show: { + resource: [ + 'report', + ], + operation: [ + 'get', + ], + }, + }, + default: '', + description: 'Event ID', + }, + { + displayName: 'Session ID', + name: 'dateId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getEventSessions', + loadOptionsDependsOn: [ + 'eventId', + ], + }, + default: '', + required: true, + description: 'ID of the session', + displayOptions: { + show: { + resource: [ + 'report', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'report', + ], + operation: [ + 'get', + ], + }, + }, + options: [ + { + displayName: 'Status', + name: 'status', + type: 'options', + options: [ + { + name: 'Attended', + value: 'attended', + }, + { + name: 'Banned', + value: 'banned', + }, + { + name: 'Completed', + value: 'completed', + }, + { + name: 'Did Not Attend', + value: 'did-not-attend', + }, + { + name: 'Left Early', + value: 'left-early', + }, + ], + default: '', + description: 'Filter results by participation status', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Demio/demio.svg b/packages/nodes-base/nodes/Demio/demio.svg new file mode 100644 index 0000000000..93889f1d02 --- /dev/null +++ b/packages/nodes-base/nodes/Demio/demio.svg @@ -0,0 +1 @@ + diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 0f958e35b4..f3d2dfe3a9 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -61,6 +61,7 @@ "dist/credentials/CustomerIoApi.credentials.js", "dist/credentials/S3.credentials.js", "dist/credentials/CrateDb.credentials.js", + "dist/credentials/DemioApi.credentials.js", "dist/credentials/DiscourseApi.credentials.js", "dist/credentials/DisqusApi.credentials.js", "dist/credentials/DriftApi.credentials.js", @@ -303,6 +304,7 @@ "dist/nodes/CustomerIo/CustomerIo.node.js", "dist/nodes/CustomerIo/CustomerIoTrigger.node.js", "dist/nodes/DateTime.node.js", + "dist/nodes/Demio/Demio.node.js", "dist/nodes/Discord/Discord.node.js", "dist/nodes/Discourse/Discourse.node.js", "dist/nodes/Disqus/Disqus.node.js",