From f18fc9d9102bb6a334c898cb07bacaf2067c6250 Mon Sep 17 00:00:00 2001 From: Romain Dunand Date: Sun, 10 Nov 2019 01:54:25 +0100 Subject: [PATCH 01/10] Auth & get records management --- .../credentials/FileMaker.credentials.ts | 39 ++ .../nodes/FileMaker/FileMaker.node.ts | 421 ++++++++++++++++++ .../nodes/FileMaker/GenericFunctions.ts | 185 ++++++++ .../nodes-base/nodes/FileMaker/filemaker.png | Bin 0 -> 16244 bytes packages/nodes-base/nodes/HttpRequest.node.ts | 1 - packages/nodes-base/package.json | 2 + 6 files changed, 647 insertions(+), 1 deletion(-) create mode 100644 packages/nodes-base/credentials/FileMaker.credentials.ts create mode 100644 packages/nodes-base/nodes/FileMaker/FileMaker.node.ts create mode 100644 packages/nodes-base/nodes/FileMaker/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/FileMaker/filemaker.png diff --git a/packages/nodes-base/credentials/FileMaker.credentials.ts b/packages/nodes-base/credentials/FileMaker.credentials.ts new file mode 100644 index 0000000000..3d0e67b7ca --- /dev/null +++ b/packages/nodes-base/credentials/FileMaker.credentials.ts @@ -0,0 +1,39 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + + +export class FileMaker implements ICredentialType { + name = 'FileMaker'; + displayName = 'FileMaker'; + properties = [ + { + displayName: 'Host', + name: 'host', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Database', + name: 'db', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Login', + name: 'login', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Password', + name: 'password', + type: 'string' as NodePropertyTypes, + typeOptions: { + password: true, + }, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/FileMaker/FileMaker.node.ts b/packages/nodes-base/nodes/FileMaker/FileMaker.node.ts new file mode 100644 index 0000000000..8caaca47d8 --- /dev/null +++ b/packages/nodes-base/nodes/FileMaker/FileMaker.node.ts @@ -0,0 +1,421 @@ +import {IExecuteFunctions} from 'n8n-core'; +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, INodePropertyOptions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + + +import {OptionsWithUri} from 'request'; +import {layoutsApiRequest, getFields, getToken, logout} from "./GenericFunctions"; + +export class FileMaker implements INodeType { + description: INodeTypeDescription = { + displayName: 'FileMaker', + name: 'filemaker', + icon: 'file:filemaker.png', + group: ['input'], + version: 1, + description: 'Retrieve data from FileMaker data API.', + defaults: { + name: 'FileMaker', + color: '#665533', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'FileMaker', + required: true, + }, + ], + properties: [ + { + displayName: 'Action', + name: 'action', + type: 'options', + options: [ + /*{ + name: 'Login', + value: 'login', + }, + { + name: 'Logout', + value: 'logout', + },*/ + { + name: 'Find Records', + value: 'find', + }, + { + name: 'get Records', + value: 'records', + }, + { + name: 'Get Records By Id', + value: 'record', + }, + { + name: 'Perform Script', + value: 'performscript', + }, + { + name: 'Create Record', + value: 'create', + }, + { + name: 'Edit Record', + value: 'edit', + }, + { + name: 'Duplicate Record', + value: 'duplicate', + }, + { + name: 'Delete Record', + value: 'delete', + }, + ], + default: 'login', + description: 'Action to perform.', + }, + + // ---------------------------------- + // shared + // ---------------------------------- + { + displayName: 'Layout', + name: 'layout', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getLayouts', + }, + options: [], + default: '', + required: true, + displayOptions: { + hide: { + action: [ + 'performscript' + ], + }, + }, + placeholder: 'Layout Name', + description: 'FileMaker Layout Name.', + }, + { + displayName: 'Record Id', + name: 'recid', + type: 'number', + default: '', + required: true, + displayOptions: { + show: { + action: [ + 'record', + 'edit', + 'delete', + 'duplicate', + ], + }, + }, + placeholder: 'Record ID', + description: 'Internal Record ID returned by get (recordid)', + }, + // ---------------------------------- + // find/records + // ---------------------------------- + { + displayName: 'offset', + name: 'offset', + placeholder: '0', + description: 'The record number of the first record in the range of records.', + type: 'number', + default: '1', + displayOptions: { + show: { + action: [ + 'find', + 'records', + ], + }, + } + }, + { + displayName: 'limit', + name: 'limit', + placeholder: '100', + description: 'The maximum number of records that should be returned. If not specified, the default value is 100.', + type: 'number', + default: '100', + displayOptions: { + show: { + action: [ + 'find', + 'records', + ], + }, + } + }, + { + displayName: 'Sort', + name: 'sortParametersUi', + placeholder: 'Add Sort Rules', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + action: [ + 'find', + 'records', + ], + }, + }, + description: 'Sort rules', + default: {}, + options: [ + { + name: 'rules', + displayName: 'Rules', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getFields', + }, + options: [], + description: 'Field Name.', + }, + { + displayName: 'Value', + name: 'value', + type: 'options', + default: 'ascend', + options: [ + { + name: 'Ascend', + value: 'ascend' + }, + { + name: 'Descend', + value: 'descend' + }, + ], + description: 'Sort order.', + }, + ] + }, + ], + }, + // ---------------------------------- + // create/edit + // ---------------------------------- + { + displayName: 'fieldData', + name: 'fieldData', + placeholder: '{"field1": "value", "field2": "value", ...}', + description: 'Additional fields to add.', + type: 'string', + default: '{}', + displayOptions: { + show: { + action: [ + 'create', + 'edit', + ], + }, + } + }, + { + displayName: 'Fields', + name: 'Fields', + type: 'collection', + typeOptions: { + loadOptionsMethod: 'getFields', + }, + options: [], + default: '', + required: true, + displayOptions: { + show: { + action: [ + 'create', + 'edit', + ], + }, + }, + placeholder: 'Layout Name', + description: 'FileMaker Layout Name.', + }, + // ---------------------------------- + // performscript + // ---------------------------------- + { + displayName: 'Script Name', + name: 'script', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getScripts', + }, + options: [], + default: '', + required: true, + displayOptions: { + show: { + action: [ + 'performscript' + ], + }, + }, + placeholder: 'Layout Name', + description: 'FileMaker Layout Name.', + }, + ] + }; + + methods = { + loadOptions: { + // Get all the available topics to display them to user so that he can + // select them easily + async getLayouts(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + let layouts; + try { + layouts = await layoutsApiRequest.call(this); + } catch (err) { + throw new Error(`FileMaker Error: ${err}`); + } + for (const layout of layouts) { + returnData.push({ + name: layout.name, + value: layout.name, + }); + } + return returnData; + }, + + async getFields(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + + let fields; + try { + fields = await getFields.call(this); + } catch (err) { + throw new Error(`FileMaker Error: ${err}`); + } + for (const field of fields) { + returnData.push({ + name: field.name, + value: field.name, + }); + } + return returnData; + }, + }, + }; + + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: INodeExecutionData[] = []; + + const credentials = this.getCredentials('FileMaker'); + + const action = this.getNodeParameter('action', 0) as string; + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + const staticData = this.getWorkflowStaticData('global'); + // Operations which overwrite the returned data + const overwriteDataOperations = []; + // Operations which overwrite the returned data and return arrays + // and has so to be merged with the data of other items + const overwriteDataOperationsArray = []; + + let requestOptions: OptionsWithUri; + + const host = credentials.host as string; + const database = credentials.db as string; + + //const layout = this.getNodeParameter('layout', 0, null) as string; + //const recid = this.getNodeParameter('recid', 0, null) as number; + + const url = `https://${host}/fmi/data/v1`; + //const fullOperation = `${resource}:${operation}`; + + for (let i = 0; i < items.length; i++) { + // Reset all values + requestOptions = { + uri: '', + headers: {}, + method: 'GET', + json: true + //rejectUnauthorized: !this.getNodeParameter('allowUnauthorizedCerts', itemIndex, false) as boolean, + }; + + const layout = this.getNodeParameter('layout', 0) as string; + const token = await getToken.call(this); + + if (action === 'record') { + const recid = this.getNodeParameter('recid', 0) as string; + + requestOptions.uri = url + `/databases/${database}/layouts/${layout}/records/${recid}`; + requestOptions.method = 'GET'; + requestOptions.headers = { + 'Authorization': `Bearer ${token}`, + }; + } else if (action === 'records') { + requestOptions.uri = url + `/databases/${database}/layouts/${layout}/records`; + requestOptions.method = 'GET'; + requestOptions.headers = { + 'Authorization': `Bearer ${token}`, + }; + + const sort = []; + const sortParametersUi = this.getNodeParameter('sortParametersUi', 0, {}) as IDataObject; + if (sortParametersUi.parameter !== undefined) { + // @ts-ignore + for (const parameterData of sortParametersUi!.rules as IDataObject[]) { + // @ts-ignore + sort.push({ + 'fieldName': parameterData!.name as string, + 'sortOrder': parameterData!.value + }); + } + } + requestOptions.qs = { + '_offset': this.getNodeParameter('offset', 0), + '_limit': this.getNodeParameter('limit', 0), + '_sort': JSON.stringify(sort), + }; + } else { + throw new Error(`The action "${action}" is not implemented yet!`); + } + + // Now that the options are all set make the actual http request + let response; + try { + response = await this.helpers.request(requestOptions); + } catch (error) { + response = error.response.body; + } + + if (typeof response === 'string') { + throw new Error('Response body is not valid JSON. Change "Response Format" to "String"'); + } + await logout.call(this, token); + + returnData.push({json: response}); + } + + return this.prepareOutputData(returnData); + } +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/FileMaker/GenericFunctions.ts b/packages/nodes-base/nodes/FileMaker/GenericFunctions.ts new file mode 100644 index 0000000000..c76350e1f4 --- /dev/null +++ b/packages/nodes-base/nodes/FileMaker/GenericFunctions.ts @@ -0,0 +1,185 @@ +import { + IExecuteFunctions, + ILoadOptionsFunctions, + IExecuteSingleFunctions +} from 'n8n-core'; + +import { + ICredentialDataDecryptedObject, + IDataObject, +} from 'n8n-workflow'; + +import { OptionsWithUri } from 'request'; + + +/** + * Make an API request to ActiveCampaign + * + * @param {IHookFunctions} this + * @param {string} method + * @returns {Promise} + */ +export async function layoutsApiRequest(this: ILoadOptionsFunctions | IExecuteFunctions | IExecuteSingleFunctions): Promise { // tslint:disable-line:no-any + const token = await getToken.call(this); + const credentials = this.getCredentials('FileMaker'); + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + const host = credentials.host as string; + const db = credentials.db as string; + + const url = `https://${host}/fmi/data/v1/databases/${db}/layouts`; + const options: OptionsWithUri = { + headers: { + 'Authorization': `Bearer ${token}`, + }, + method: 'GET', + uri: url, + json: true + }; + + try { + const responseData = await this.helpers.request!(options); + return responseData.response.layouts; + + } catch (error) { + // If that data does not exist for some reason return the actual error + throw error; + } +} + +/** + * Make an API request to ActiveCampaign + * + * @returns {Promise} + * @param layout + */ +export async function getFields(this: ILoadOptionsFunctions | IExecuteFunctions | IExecuteSingleFunctions): Promise { // tslint:disable-line:no-any + const token = await getToken.call(this); + const credentials = this.getCredentials('FileMaker'); + const layout = this.getCurrentNodeParameter('layout') as string; + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + const host = credentials.host as string; + const db = credentials.db as string; + + const url = `https://${host}/fmi/data/v1/databases/${db}/layouts/${layout}`; + const options: OptionsWithUri = { + headers: { + 'Authorization': `Bearer ${token}`, + }, + method: 'GET', + uri: url, + json: true + }; + + try { + const responseData = await this.helpers.request!(options); + return responseData.response.fieldMetaData; + + } catch (error) { + // If that data does not exist for some reason return the actual error + throw error; + } +} + +export async function getToken(this: ILoadOptionsFunctions | IExecuteFunctions | IExecuteSingleFunctions): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('FileMaker'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + const host = credentials.host as string; + const db = credentials.db as string; + const login = credentials.login as string; + const password = credentials.password as string; + + const url = `https://${host}/fmi/data/v1/databases/${db}/sessions`; + + let requestOptions: OptionsWithUri; + // Reset all values + requestOptions = { + uri: url, + headers: {}, + method: 'POST', + json: true + //rejectUnauthorized: !this.getNodeParameter('allowUnauthorizedCerts', itemIndex, false) as boolean, + }; + requestOptions.auth = { + user: login as string, + pass: password as string, + }; + requestOptions.body = { + "fmDataSource": [ + { + "database": host, + "username": login as string, + "password": password as string + } + ] + }; + + try { + const response = await this.helpers.request!(requestOptions); + + if (typeof response === 'string') { + throw new Error('Response body is not valid JSON. Change "Response Format" to "String"'); + } + + return response.response.token; + } catch (error) { + console.error(error); + + const errorMessage = error.response.body.messages[0].message + '(' + error.response.body.messages[0].message + ')'; + + if (errorMessage !== undefined) { + throw errorMessage; + } + throw error.response.body; + } +} + +export async function logout(this: ILoadOptionsFunctions | IExecuteFunctions | IExecuteSingleFunctions, token: string): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('FileMaker'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + const host = credentials.host as string; + const db = credentials.db as string; + + const url = `https://${host}/fmi/data/v1/databases/${db}/sessions/${token}`; + + let requestOptions: OptionsWithUri; + // Reset all values + requestOptions = { + uri: url, + headers: {}, + method: 'DELETE', + json: true + //rejectUnauthorized: !this.getNodeParameter('allowUnauthorizedCerts', itemIndex, false) as boolean, + }; + + try { + const response = await this.helpers.request!(requestOptions); + + if (typeof response === 'string') { + throw new Error('Response body is not valid JSON. Change "Response Format" to "String"'); + } + + return response; + } catch (error) { + console.error(error); + + const errorMessage = error.response.body.messages[0].message + '(' + error.response.body.messages[0].message + ')'; + + if (errorMessage !== undefined) { + throw errorMessage; + } + throw error.response.body; + } +} + diff --git a/packages/nodes-base/nodes/FileMaker/filemaker.png b/packages/nodes-base/nodes/FileMaker/filemaker.png new file mode 100644 index 0000000000000000000000000000000000000000..ec691433dac024e2f3596fc5f9c310e5acc4c3a1 GIT binary patch literal 16244 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 Date: Sun, 10 Nov 2019 03:14:10 +0100 Subject: [PATCH 02/10] Handle portal selection & improve UX --- .../nodes/FileMaker/FileMaker.node.ts | 112 +++++++++++++++--- .../nodes/FileMaker/GenericFunctions.ts | 40 ++++++- 2 files changed, 134 insertions(+), 18 deletions(-) diff --git a/packages/nodes-base/nodes/FileMaker/FileMaker.node.ts b/packages/nodes-base/nodes/FileMaker/FileMaker.node.ts index 8caaca47d8..e7435fe15f 100644 --- a/packages/nodes-base/nodes/FileMaker/FileMaker.node.ts +++ b/packages/nodes-base/nodes/FileMaker/FileMaker.node.ts @@ -9,7 +9,7 @@ import { import {OptionsWithUri} from 'request'; -import {layoutsApiRequest, getFields, getToken, logout} from "./GenericFunctions"; +import {layoutsApiRequest, getFields, getPortals, getToken, logout} from "./GenericFunctions"; export class FileMaker implements INodeType { description: INodeTypeDescription = { @@ -36,6 +36,7 @@ export class FileMaker implements INodeType { displayName: 'Action', name: 'action', type: 'options', + default: 'record', options: [ /*{ name: 'Login', @@ -78,7 +79,6 @@ export class FileMaker implements INodeType { value: 'delete', }, ], - default: 'login', description: 'Action to perform.', }, @@ -124,9 +124,7 @@ export class FileMaker implements INodeType { placeholder: 'Record ID', description: 'Internal Record ID returned by get (recordid)', }, - // ---------------------------------- - // find/records - // ---------------------------------- + { displayName: 'offset', name: 'offset', @@ -159,6 +157,49 @@ export class FileMaker implements INodeType { }, } }, + { + displayName: 'Get portals', + name: 'getPortals', + type: 'boolean', + default: false, + description: 'Should we get portal data as well ?', + }, + { + displayName: 'Portals', + name: 'portals', + type: 'options', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add portal', + loadOptionsMethod: 'getPortals', + }, + options: [], + default: [], + displayOptions: { + show: { + action: [ + 'record', + 'records', + 'find', + ], + getPortals: [ + true, + ], + }, + }, + placeholder: 'Portals', + description: 'The portal result set to return. Use the portal object name or portal table name. If this parameter is omitted, the API will return all portal objects and records in the layout. For best performance, pass the portal object name or portal table name.', + }, + // ---------------------------------- + // find/records + // ---------------------------------- + { + displayName: 'Sort data ?', + name: 'setSort', + type: 'boolean', + default: false, + description: 'Should we sort data ?', + }, { displayName: 'Sort', name: 'sortParametersUi', @@ -169,6 +210,9 @@ export class FileMaker implements INodeType { }, displayOptions: { show: { + setSort: [ + true, + ], action: [ 'find', 'records', @@ -183,7 +227,7 @@ export class FileMaker implements INodeType { displayName: 'Rules', values: [ { - displayName: 'Name', + displayName: 'Field', name: 'name', type: 'options', default: '', @@ -194,7 +238,7 @@ export class FileMaker implements INodeType { description: 'Field Name.', }, { - displayName: 'Value', + displayName: 'Order', name: 'value', type: 'options', default: 'ascend', @@ -318,6 +362,25 @@ export class FileMaker implements INodeType { } return returnData; }, + + async getPortals(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + + let portals; + try { + portals = await getPortals.call(this); + } catch (err) { + throw new Error(`FileMaker Error: ${err}`); + } + Object.keys(portals).forEach((portal) => { + returnData.push({ + name: portal, + value: portal, + }); + }); + + return returnData; + }, }, }; @@ -379,22 +442,39 @@ export class FileMaker implements INodeType { 'Authorization': `Bearer ${token}`, }; - const sort = []; - const sortParametersUi = this.getNodeParameter('sortParametersUi', 0, {}) as IDataObject; - if (sortParametersUi.parameter !== undefined) { - // @ts-ignore - for (const parameterData of sortParametersUi!.rules as IDataObject[]) { + //Handle Sort + let sort; + const setSort = this.getNodeParameter('setSort', 0, false); + if (setSort) { + sort = null; + } else { + const sortParametersUi = this.getNodeParameter('sortParametersUi', 0, {}) as IDataObject; + if (sortParametersUi.rules !== undefined) { // @ts-ignore - sort.push({ - 'fieldName': parameterData!.name as string, - 'sortOrder': parameterData!.value - }); + for (const parameterData of sortParametersUi!.rules as IDataObject[]) { + // @ts-ignore + sort.push({ + 'fieldName': parameterData!.name as string, + 'sortOrder': parameterData!.value + }); + } } } + + //handle portals + let portals; + const getPortals = this.getNodeParameter('getPortals', 0); + if (!getPortals) { + portals = []; + } else { + portals = this.getNodeParameter('portals', 0); + } + requestOptions.qs = { '_offset': this.getNodeParameter('offset', 0), '_limit': this.getNodeParameter('limit', 0), '_sort': JSON.stringify(sort), + 'portal': JSON.stringify(portals), }; } else { throw new Error(`The action "${action}" is not implemented yet!`); diff --git a/packages/nodes-base/nodes/FileMaker/GenericFunctions.ts b/packages/nodes-base/nodes/FileMaker/GenericFunctions.ts index c76350e1f4..5fc1763b79 100644 --- a/packages/nodes-base/nodes/FileMaker/GenericFunctions.ts +++ b/packages/nodes-base/nodes/FileMaker/GenericFunctions.ts @@ -53,9 +53,8 @@ export async function layoutsApiRequest(this: ILoadOptionsFunctions | IExecuteFu * Make an API request to ActiveCampaign * * @returns {Promise} - * @param layout */ -export async function getFields(this: ILoadOptionsFunctions | IExecuteFunctions | IExecuteSingleFunctions): Promise { // tslint:disable-line:no-any +export async function getFields(this: ILoadOptionsFunctions): Promise { // tslint:disable-line:no-any const token = await getToken.call(this); const credentials = this.getCredentials('FileMaker'); const layout = this.getCurrentNodeParameter('layout') as string; @@ -86,6 +85,43 @@ export async function getFields(this: ILoadOptionsFunctions | IExecuteFunctions } } + +/** + * Make an API request to ActiveCampaign + * + * @returns {Promise} + */ +export async function getPortals(this: ILoadOptionsFunctions): Promise { // tslint:disable-line:no-any + const token = await getToken.call(this); + const credentials = this.getCredentials('FileMaker'); + const layout = this.getCurrentNodeParameter('layout') as string; + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + const host = credentials.host as string; + const db = credentials.db as string; + + const url = `https://${host}/fmi/data/v1/databases/${db}/layouts/${layout}`; + const options: OptionsWithUri = { + headers: { + 'Authorization': `Bearer ${token}`, + }, + method: 'GET', + uri: url, + json: true + }; + + try { + const responseData = await this.helpers.request!(options); + return responseData.response.portalMetaData; + + } catch (error) { + // If that data does not exist for some reason return the actual error + throw error; + } +} + export async function getToken(this: ILoadOptionsFunctions | IExecuteFunctions | IExecuteSingleFunctions): Promise { // tslint:disable-line:no-any const credentials = this.getCredentials('FileMaker'); if (credentials === undefined) { From 0852a74469f5c5eca9dd844bfeef106de95e9a74 Mon Sep 17 00:00:00 2001 From: Romain Dunand Date: Tue, 12 Nov 2019 02:01:43 +0100 Subject: [PATCH 03/10] Handle Perform script Handle Perform script in queries Handle create/edit --- .../nodes/FileMaker/FileMaker.node.ts | 511 +++++++++++++++--- .../nodes/FileMaker/GenericFunctions.ts | 139 +++++ 2 files changed, 581 insertions(+), 69 deletions(-) diff --git a/packages/nodes-base/nodes/FileMaker/FileMaker.node.ts b/packages/nodes-base/nodes/FileMaker/FileMaker.node.ts index e7435fe15f..c747dc7c3e 100644 --- a/packages/nodes-base/nodes/FileMaker/FileMaker.node.ts +++ b/packages/nodes-base/nodes/FileMaker/FileMaker.node.ts @@ -9,7 +9,19 @@ import { import {OptionsWithUri} from 'request'; -import {layoutsApiRequest, getFields, getPortals, getToken, logout} from "./GenericFunctions"; +import { + layoutsApiRequest, + getFields, + getPortals, + getScripts, + getToken, + parseSort, + parsePortals, + parseQuery, + parseScripts, + parseFields, + logout +} from "./GenericFunctions"; export class FileMaker implements INodeType { description: INodeTypeDescription = { @@ -96,11 +108,6 @@ export class FileMaker implements INodeType { default: '', required: true, displayOptions: { - hide: { - action: [ - 'performscript' - ], - }, }, placeholder: 'Layout Name', description: 'FileMaker Layout Name.', @@ -124,7 +131,6 @@ export class FileMaker implements INodeType { placeholder: 'Record ID', description: 'Internal Record ID returned by get (recordid)', }, - { displayName: 'offset', name: 'offset', @@ -163,6 +169,15 @@ export class FileMaker implements INodeType { type: 'boolean', default: false, description: 'Should we get portal data as well ?', + displayOptions: { + show: { + action: [ + 'record', + 'records', + 'find', + ], + }, + }, }, { displayName: 'Portals', @@ -193,12 +208,107 @@ export class FileMaker implements INodeType { // ---------------------------------- // find/records // ---------------------------------- + { + displayName: 'Response Layout', + name: 'responseLayout', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getLayouts', + }, + options: [], + default: '', + required: false, + displayOptions: { + show: { + action: [ + 'find' + ], + }, + }, + }, + { + displayName: 'Queries', + name: 'queries', + placeholder: 'Add query', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + action: [ + 'find', + ], + }, + }, + description: 'Queries ', + default: {}, + options: [ + { + name: 'query', + displayName: 'Query', + values: [ + { + displayName: 'Fields', + name: 'fields', + placeholder: 'Add field', + type: 'fixedCollection', + default: {}, + typeOptions: { + multipleValues: true, + }, + options: [{ + name: 'field', + displayName: 'Field', + values: [ + { + displayName: 'Field', + name: 'name', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getFields', + }, + options: [], + description: 'Search Field', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value to search', + }, + ] + } + ], + description: 'Field Name', + }, + { + displayName: 'Omit', + name: 'omit', + type: 'boolean', + default: false + }, + ] + }, + ], + }, { displayName: 'Sort data ?', name: 'setSort', type: 'boolean', default: false, description: 'Should we sort data ?', + displayOptions: { + show: { + action: [ + 'find', + 'record', + 'records', + ], + }, + }, }, { displayName: 'Sort', @@ -258,6 +368,192 @@ export class FileMaker implements INodeType { }, ], }, + { + displayName: 'Before find script', + name: 'setScriptBefore', + type: 'boolean', + default: false, + description: 'Define a script to be run before the action specified by the API call and after the subsequent sort.', + displayOptions: { + show: { + action: [ + 'find', + 'record', + 'records', + ], + } + }, + }, + { + displayName: 'Script Name', + name: 'scriptBefore', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getScripts', + }, + options: [], + default: '', + required: true, + displayOptions: { + show: { + action: [ + 'find', + 'record', + 'records', + ], + setScriptBefore: [ + true + ], + }, + }, + placeholder: 'Script Name', + description: 'The name of the FileMaker script to be run after the action specified by the API call and after the subsequent sort.', + }, + { + displayName: 'Script Parameter', + name: 'scriptBeforeParam', + type: 'string', + default: '', + required: false, + displayOptions: { + show: { + action: [ + 'find', + 'record', + 'records', + ], + setScriptBefore: [ + true + ], + }, + }, + placeholder: 'Script Parameters', + description: 'A parameter for the FileMaker script.', + }, + { + displayName: 'Before sort script', + name: 'setScriptSort', + type: 'boolean', + default: false, + description: 'Define a script to be run after the action specified by the API call but before the subsequent sort.', + displayOptions: { + show: { + action: [ + 'find', + 'record', + 'records', + ], + } + }, + }, + { + displayName: 'Script Name', + name: 'scriptSort', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getScripts', + }, + options: [], + default: '', + required: true, + displayOptions: { + show: { + action: [ + 'find', + 'record', + 'records', + ], + setScriptSort: [ + true + ], + }, + }, + placeholder: 'Script Name', + description: 'The name of the FileMaker script to be run after the action specified by the API call but before the subsequent sort.', + }, + { + displayName: 'Script Parameter', + name: 'scriptSortParam', + type: 'string', + default: '', + required: false, + displayOptions: { + show: { + action: [ + 'find', + 'record', + 'records', + ], + setScriptSort: [ + true + ], + }, + }, + placeholder: 'Script Parameters', + description: 'A parameter for the FileMaker script.', + }, + { + displayName: 'After sort script', + name: 'setScriptAfter', + type: 'boolean', + default: false, + description: 'Define a script to be run after the action specified by the API call but before the subsequent sort.', + displayOptions: { + show: { + action: [ + 'find', + 'record', + 'records', + ], + } + }, + }, + { + displayName: 'Script Name', + name: 'scriptAfter', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getScripts', + }, + options: [], + default: '', + required: true, + displayOptions: { + show: { + action: [ + 'find', + 'record', + 'records', + ], + setScriptAfter: [ + true + ], + }, + }, + placeholder: 'Script Name', + description: 'The name of the FileMaker script to be run after the action specified by the API call and after the subsequent sort.', + }, + { + displayName: 'Script Parameter', + name: 'scriptAfterParam', + type: 'string', + default: '', + required: false, + displayOptions: { + show: { + action: [ + 'find', + 'record', + 'records', + ], + setScriptAfter: [ + true + ], + }, + }, + placeholder: 'Script Parameters', + description: 'A parameter for the FileMaker script.', + }, // ---------------------------------- // create/edit // ---------------------------------- @@ -278,15 +574,27 @@ export class FileMaker implements INodeType { } }, { - displayName: 'Fields', - name: 'Fields', - type: 'collection', - typeOptions: { - loadOptionsMethod: 'getFields', - }, - options: [], + displayName: 'Mod Id', + name: 'modId', + description: 'The last modification ID. When you use modId, a record is edited only when the modId matches.', + type: 'number', default: '', - required: true, + displayOptions: { + show: { + action: [ + 'edit', + ], + }, + } + }, + { + displayName: 'Fields', + name: 'fieldsParametersUi', + placeholder: 'Add field', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, displayOptions: { show: { action: [ @@ -295,8 +603,33 @@ export class FileMaker implements INodeType { ], }, }, - placeholder: 'Layout Name', - description: 'FileMaker Layout Name.', + description: 'Fields to define', + default: {}, + options: [ + { + name: 'fields', + displayName: 'Fields', + values: [ + { + displayName: 'Field', + name: 'name', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getFields', + }, + options: [], + description: 'Field Name.', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + }, + ] + }, + ], }, // ---------------------------------- // performscript @@ -318,8 +651,24 @@ export class FileMaker implements INodeType { ], }, }, - placeholder: 'Layout Name', - description: 'FileMaker Layout Name.', + placeholder: 'Script Name', + description: 'The name of the FileMaker script to be run.', + }, + { + displayName: 'Script Parameter', + name: 'scriptParam', + type: 'string', + default: '', + required: false, + displayOptions: { + show: { + action: [ + 'performscript' + ], + }, + }, + placeholder: 'Script Parameters', + description: 'A parameter for the FileMaker script.', }, ] }; @@ -363,6 +712,26 @@ export class FileMaker implements INodeType { return returnData; }, + async getScripts(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + + let scripts; + try { + scripts = await getScripts.call(this); + } catch (err) { + throw new Error(`FileMaker Error: ${err}`); + } + for (const script of scripts) { + if (!script.isFolder) { + returnData.push({ + name: script.name, + value: script.name, + }); + } + } + return returnData; + }, + async getPortals(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; @@ -396,85 +765,89 @@ export class FileMaker implements INodeType { if (credentials === undefined) { throw new Error('No credentials got returned!'); } + const token = await getToken.call(this); + const staticData = this.getWorkflowStaticData('global'); - // Operations which overwrite the returned data - const overwriteDataOperations = []; - // Operations which overwrite the returned data and return arrays - // and has so to be merged with the data of other items - const overwriteDataOperationsArray = []; let requestOptions: OptionsWithUri; const host = credentials.host as string; const database = credentials.db as string; - //const layout = this.getNodeParameter('layout', 0, null) as string; - //const recid = this.getNodeParameter('recid', 0, null) as number; - const url = `https://${host}/fmi/data/v1`; - //const fullOperation = `${resource}:${operation}`; for (let i = 0; i < items.length; i++) { // Reset all values requestOptions = { uri: '', - headers: {}, + headers: { + 'Authorization': `Bearer ${token}`, + }, method: 'GET', json: true //rejectUnauthorized: !this.getNodeParameter('allowUnauthorizedCerts', itemIndex, false) as boolean, }; const layout = this.getNodeParameter('layout', 0) as string; - const token = await getToken.call(this); if (action === 'record') { const recid = this.getNodeParameter('recid', 0) as string; - requestOptions.uri = url + `/databases/${database}/layouts/${layout}/records/${recid}`; - requestOptions.method = 'GET'; - requestOptions.headers = { - 'Authorization': `Bearer ${token}`, + requestOptions.qs = { + 'portal': JSON.stringify(parsePortals.call(this)), + ...parseScripts.call(this) }; } else if (action === 'records') { requestOptions.uri = url + `/databases/${database}/layouts/${layout}/records`; - requestOptions.method = 'GET'; - requestOptions.headers = { - 'Authorization': `Bearer ${token}`, - }; - - //Handle Sort - let sort; - const setSort = this.getNodeParameter('setSort', 0, false); - if (setSort) { - sort = null; - } else { - const sortParametersUi = this.getNodeParameter('sortParametersUi', 0, {}) as IDataObject; - if (sortParametersUi.rules !== undefined) { - // @ts-ignore - for (const parameterData of sortParametersUi!.rules as IDataObject[]) { - // @ts-ignore - sort.push({ - 'fieldName': parameterData!.name as string, - 'sortOrder': parameterData!.value - }); - } - } - } - - //handle portals - let portals; - const getPortals = this.getNodeParameter('getPortals', 0); - if (!getPortals) { - portals = []; - } else { - portals = this.getNodeParameter('portals', 0); - } - requestOptions.qs = { '_offset': this.getNodeParameter('offset', 0), '_limit': this.getNodeParameter('limit', 0), - '_sort': JSON.stringify(sort), - 'portal': JSON.stringify(portals), + '_sort': JSON.stringify(parseSort.call(this)), + 'portal': JSON.stringify(parsePortals.call(this)), + ...parseScripts.call(this) + }; + } else if (action === 'find') { + requestOptions.uri = url + `/databases/${database}/layouts/${layout}/_find`; + requestOptions.method = 'POST'; + requestOptions.body = { + 'query': parseQuery.call(this), + 'offset': this.getNodeParameter('offset', 0), + 'limit': this.getNodeParameter('limit', 0), + 'layout.response': this.getNodeParameter('responseLayout', 0), + ...parseScripts.call(this) + }; + const sort = parseSort.call(this); + if (sort) { + requestOptions.body.sort = sort; + } + } else if (action === 'create') { + requestOptions.uri = url + `/databases/${database}/layouts/${layout}/records`; + requestOptions.method = 'POST'; + requestOptions.headers['Content-Type'] = 'application/json'; + + //TODO: handle portalData + requestOptions.body = { + fieldData: {...parseFields.call(this)}, + portalData: {}, + ...parseScripts.call(this) + }; + } else if (action === 'edit') { + const recid = this.getNodeParameter('recid', 0) as string; + requestOptions.uri = url + `/databases/${database}/layouts/${layout}/records/${recid}`; + requestOptions.method = 'PATCH'; + requestOptions.headers['Content-Type'] = 'application/json'; + + //TODO: handle portalData + requestOptions.body = { + fieldData: {...parseFields.call(this)}, + portalData: {}, + ...parseScripts.call(this) + }; + } else if (action === 'performscript') { + const scriptName = this.getNodeParameter('script', 0) as string; + requestOptions.uri = url + `/databases/${database}/layouts/${layout}/script/${scriptName}`; + requestOptions.qs = { + 'script.param': this.getNodeParameter('scriptParam', 0), }; } else { throw new Error(`The action "${action}" is not implemented yet!`); diff --git a/packages/nodes-base/nodes/FileMaker/GenericFunctions.ts b/packages/nodes-base/nodes/FileMaker/GenericFunctions.ts index 5fc1763b79..5e24fe978b 100644 --- a/packages/nodes-base/nodes/FileMaker/GenericFunctions.ts +++ b/packages/nodes-base/nodes/FileMaker/GenericFunctions.ts @@ -122,6 +122,42 @@ export async function getPortals(this: ILoadOptionsFunctions): Promise { // } } +/** + * Make an API request to ActiveCampaign + * + * @returns {Promise} + */ +export async function getScripts(this: ILoadOptionsFunctions): Promise { // tslint:disable-line:no-any + const token = await getToken.call(this); + const credentials = this.getCredentials('FileMaker'); + const layout = this.getCurrentNodeParameter('layout') as string; + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + const host = credentials.host as string; + const db = credentials.db as string; + + const url = `https://${host}/fmi/data/v1/databases/${db}/scripts`; + const options: OptionsWithUri = { + headers: { + 'Authorization': `Bearer ${token}`, + }, + method: 'GET', + uri: url, + json: true + }; + + try { + const responseData = await this.helpers.request!(options); + return responseData.response.scripts; + + } catch (error) { + // If that data does not exist for some reason return the actual error + throw error; + } +} + export async function getToken(this: ILoadOptionsFunctions | IExecuteFunctions | IExecuteSingleFunctions): Promise { // tslint:disable-line:no-any const credentials = this.getCredentials('FileMaker'); if (credentials === undefined) { @@ -219,3 +255,106 @@ export async function logout(this: ILoadOptionsFunctions | IExecuteFunctions | I } } +export function parseSort(this: IExecuteFunctions): object | null { + let sort; + const setSort = this.getNodeParameter('setSort', 0, false); + if (!setSort) { + sort = null; + } else { + sort = []; + const sortParametersUi = this.getNodeParameter('sortParametersUi', 0, {}) as IDataObject; + if (sortParametersUi.rules !== undefined) { + // @ts-ignore + for (const parameterData of sortParametersUi!.rules as IDataObject[]) { + // @ts-ignore + sort.push({ + 'fieldName': parameterData!.name as string, + 'sortOrder': parameterData!.value + }); + } + } + } + return sort; +} + + +export function parseScripts(this: IExecuteFunctions): object | null { + const setScriptAfter = this.getNodeParameter('setScriptAfter', 0, false); + const setScriptBefore = this.getNodeParameter('setScriptBefore', 0, false); + const setScriptSort = this.getNodeParameter('setScriptSort', 0, false); + + if (!setScriptAfter && setScriptBefore && setScriptSort) { + return {}; + } else { + const scripts = { + }; + if (setScriptAfter) { + scripts.script = this.getNodeParameter('scriptAfter', 0); + scripts['script.param'] = this.getNodeParameter('scriptAfter', 0); + } + if (setScriptBefore) { + scripts['script.prerequest'] = this.getNodeParameter('scriptBefore', 0); + scripts['script.prerequest.param'] = this.getNodeParameter('scriptBeforeParam', 0); + } + if (setScriptSort) { + scripts['script.presort'] = this.getNodeParameter('scriptSort', 0); + scripts['script.presort.param'] = this.getNodeParameter('scriptSortParam', 0); + } + return scripts; + } +} + +export function parsePortals(this: IExecuteFunctions): object | null { + let portals; + const getPortals = this.getNodeParameter('getPortals', 0); + if (!getPortals) { + portals = []; + } else { + portals = this.getNodeParameter('portals', 0); + } + // @ts-ignore + return portals; +} + + +export function parseQuery(this: IExecuteFunctions): object | null { + let queries; + const queriesParamUi = this.getNodeParameter('queries', 0, {}) as IDataObject; + if (queriesParamUi.query !== undefined) { + // @ts-ignore + queries = []; + for (const queryParam of queriesParamUi!.query as IDataObject[]) { + const query = { + 'omit': queryParam.omit ? 'true' : 'false', + }; + // @ts-ignore + for (const field of queryParam!.fields!.field as IDataObject[]) { + // @ts-ignore + query[field.name] =field!.value; + } + queries.push(query); + } + } else { + queries = null; + } + // @ts-ignore + return queries; +} + +export function parseFields(this: IExecuteFunctions): object | null { + let fieldData; + const fieldsParametersUi = this.getNodeParameter('fieldsParametersUi', 0, {}) as IDataObject; + if (fieldsParametersUi.fields !== undefined) { + // @ts-ignore + fieldData = {}; + for (const field of fieldsParametersUi!.fields as IDataObject[]) { + // @ts-ignore + fieldData[field.name] =field!.value; + } + } else { + fieldData = null; + } + // @ts-ignore + return fieldData; +} + From 44b1f52d4eec320a51dcfa5bb62c171aeb637ebd Mon Sep 17 00:00:00 2001 From: Romain Dunand Date: Wed, 13 Nov 2019 00:00:58 +0100 Subject: [PATCH 04/10] Handle duplicate/delete actions Code cleanup Improve error handling --- .../nodes/FileMaker/FileMaker.node.ts | 65 +++++++++++++++---- .../nodes/FileMaker/GenericFunctions.ts | 25 +++++-- 2 files changed, 72 insertions(+), 18 deletions(-) diff --git a/packages/nodes-base/nodes/FileMaker/FileMaker.node.ts b/packages/nodes-base/nodes/FileMaker/FileMaker.node.ts index c747dc7c3e..ff8b893fa5 100644 --- a/packages/nodes-base/nodes/FileMaker/FileMaker.node.ts +++ b/packages/nodes-base/nodes/FileMaker/FileMaker.node.ts @@ -213,7 +213,7 @@ export class FileMaker implements INodeType { name: 'responseLayout', type: 'options', typeOptions: { - loadOptionsMethod: 'getLayouts', + loadOptionsMethod: 'getResponseLayouts', }, options: [], default: '', @@ -557,7 +557,7 @@ export class FileMaker implements INodeType { // ---------------------------------- // create/edit // ---------------------------------- - { + /*{ displayName: 'fieldData', name: 'fieldData', placeholder: '{"field1": "value", "field2": "value", ...}', @@ -572,7 +572,7 @@ export class FileMaker implements INodeType { ], }, } - }, + },*/ { displayName: 'Mod Id', name: 'modId', @@ -679,6 +679,28 @@ export class FileMaker implements INodeType { // select them easily async getLayouts(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; + + let layouts; + try { + layouts = await layoutsApiRequest.call(this); + } catch (err) { + throw new Error(`FileMaker Error: ${err}`); + } + for (const layout of layouts) { + returnData.push({ + name: layout.name, + value: layout.name, + }); + } + return returnData; + }, + async getResponseLayouts(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + returnData.push({ + name: 'Use main layout', + value: '', + }); + let layouts; try { layouts = await layoutsApiRequest.call(this); @@ -760,14 +782,16 @@ export class FileMaker implements INodeType { const credentials = this.getCredentials('FileMaker'); - const action = this.getNodeParameter('action', 0) as string; - if (credentials === undefined) { throw new Error('No credentials got returned!'); } - const token = await getToken.call(this); - const staticData = this.getWorkflowStaticData('global'); + let token; + try { + token = await getToken.call(this); + } catch (e) { + throw new Error(`Login fail: ${e}`); + } let requestOptions: OptionsWithUri; @@ -776,6 +800,8 @@ export class FileMaker implements INodeType { const url = `https://${host}/fmi/data/v1`; + const action = this.getNodeParameter('action', 0) as string; + for (let i = 0; i < items.length; i++) { // Reset all values requestOptions = { @@ -785,7 +811,6 @@ export class FileMaker implements INodeType { }, method: 'GET', json: true - //rejectUnauthorized: !this.getNodeParameter('allowUnauthorizedCerts', itemIndex, false) as boolean, }; const layout = this.getNodeParameter('layout', 0) as string; @@ -802,10 +827,13 @@ export class FileMaker implements INodeType { requestOptions.qs = { '_offset': this.getNodeParameter('offset', 0), '_limit': this.getNodeParameter('limit', 0), - '_sort': JSON.stringify(parseSort.call(this)), 'portal': JSON.stringify(parsePortals.call(this)), ...parseScripts.call(this) }; + const sort = parseSort.call(this); + if (sort) { + requestOptions.body.sort = sort; + } } else if (action === 'find') { requestOptions.uri = url + `/databases/${database}/layouts/${layout}/_find`; requestOptions.method = 'POST'; @@ -823,7 +851,7 @@ export class FileMaker implements INodeType { } else if (action === 'create') { requestOptions.uri = url + `/databases/${database}/layouts/${layout}/records`; requestOptions.method = 'POST'; - requestOptions.headers['Content-Type'] = 'application/json'; + requestOptions.headers!['Content-Type'] = 'application/json'; //TODO: handle portalData requestOptions.body = { @@ -835,7 +863,7 @@ export class FileMaker implements INodeType { const recid = this.getNodeParameter('recid', 0) as string; requestOptions.uri = url + `/databases/${database}/layouts/${layout}/records/${recid}`; requestOptions.method = 'PATCH'; - requestOptions.headers['Content-Type'] = 'application/json'; + requestOptions.headers!['Content-Type'] = 'application/json'; //TODO: handle portalData requestOptions.body = { @@ -849,6 +877,21 @@ export class FileMaker implements INodeType { requestOptions.qs = { 'script.param': this.getNodeParameter('scriptParam', 0), }; + } else if (action === 'duplicate') { + const recid = this.getNodeParameter('recid', 0) as string; + requestOptions.uri = url + `/databases/${database}/layouts/${layout}/records/${recid}`; + requestOptions.method = 'POST'; + requestOptions.headers!['Content-Type'] = 'application/json'; + requestOptions.qs = { + ...parseScripts.call(this) + }; + } else if (action === 'delete') { + const recid = this.getNodeParameter('recid', 0) as string; + requestOptions.uri = url + `/databases/${database}/layouts/${layout}/records/${recid}`; + requestOptions.method = 'DELETE'; + requestOptions.qs = { + ...parseScripts.call(this) + }; } else { throw new Error(`The action "${action}" is not implemented yet!`); } diff --git a/packages/nodes-base/nodes/FileMaker/GenericFunctions.ts b/packages/nodes-base/nodes/FileMaker/GenericFunctions.ts index 5e24fe978b..a0470bcb10 100644 --- a/packages/nodes-base/nodes/FileMaker/GenericFunctions.ts +++ b/packages/nodes-base/nodes/FileMaker/GenericFunctions.ts @@ -10,8 +10,16 @@ import { } from 'n8n-workflow'; import { OptionsWithUri } from 'request'; +import {Url} from "url"; - +interface ScriptsOptions { + script?: any; //tslint:disable-line:no-any + 'script.param'?: any; //tslint:disable-line:no-any + 'script.prerequest'?: any; //tslint:disable-line:no-any + 'script.prerequest.param'?: any; //tslint:disable-line:no-any + 'script.presort'?: any; //tslint:disable-line:no-any + 'script.presort.param'?: any; //tslint:disable-line:no-any +} /** * Make an API request to ActiveCampaign * @@ -205,12 +213,16 @@ export async function getToken(this: ILoadOptionsFunctions | IExecuteFunctions | } catch (error) { console.error(error); - const errorMessage = error.response.body.messages[0].message + '(' + error.response.body.messages[0].message + ')'; - + let errorMessage; + if (error.response) { + errorMessage = error.response.body.messages[0].message + '(' + error.response.body.messages[0].message + ')'; + } else { + errorMessage = `${error.message} (${error.name})`; + } if (errorMessage !== undefined) { throw errorMessage; } - throw error.response.body; + throw error.message; } } @@ -286,11 +298,10 @@ export function parseScripts(this: IExecuteFunctions): object | null { if (!setScriptAfter && setScriptBefore && setScriptSort) { return {}; } else { - const scripts = { - }; + const scripts = {} as ScriptsOptions; if (setScriptAfter) { scripts.script = this.getNodeParameter('scriptAfter', 0); - scripts['script.param'] = this.getNodeParameter('scriptAfter', 0); + scripts!['script.param'] = this.getNodeParameter('scriptAfter', 0); } if (setScriptBefore) { scripts['script.prerequest'] = this.getNodeParameter('scriptBefore', 0); From cfa871322692a06fdd9f51ac8b7a3b5945488221 Mon Sep 17 00:00:00 2001 From: Romain Dunand Date: Sun, 10 Nov 2019 01:54:25 +0100 Subject: [PATCH 05/10] Auth & get records management --- .../credentials/FileMaker.credentials.ts | 39 ++ .../nodes/FileMaker/FileMaker.node.ts | 421 ++++++++++++++++++ .../nodes/FileMaker/GenericFunctions.ts | 185 ++++++++ .../nodes-base/nodes/FileMaker/filemaker.png | Bin 0 -> 16244 bytes packages/nodes-base/nodes/HttpRequest.node.ts | 1 - packages/nodes-base/package.json | 2 + 6 files changed, 647 insertions(+), 1 deletion(-) create mode 100644 packages/nodes-base/credentials/FileMaker.credentials.ts create mode 100644 packages/nodes-base/nodes/FileMaker/FileMaker.node.ts create mode 100644 packages/nodes-base/nodes/FileMaker/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/FileMaker/filemaker.png diff --git a/packages/nodes-base/credentials/FileMaker.credentials.ts b/packages/nodes-base/credentials/FileMaker.credentials.ts new file mode 100644 index 0000000000..3d0e67b7ca --- /dev/null +++ b/packages/nodes-base/credentials/FileMaker.credentials.ts @@ -0,0 +1,39 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + + +export class FileMaker implements ICredentialType { + name = 'FileMaker'; + displayName = 'FileMaker'; + properties = [ + { + displayName: 'Host', + name: 'host', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Database', + name: 'db', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Login', + name: 'login', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Password', + name: 'password', + type: 'string' as NodePropertyTypes, + typeOptions: { + password: true, + }, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/FileMaker/FileMaker.node.ts b/packages/nodes-base/nodes/FileMaker/FileMaker.node.ts new file mode 100644 index 0000000000..8caaca47d8 --- /dev/null +++ b/packages/nodes-base/nodes/FileMaker/FileMaker.node.ts @@ -0,0 +1,421 @@ +import {IExecuteFunctions} from 'n8n-core'; +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, INodePropertyOptions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + + +import {OptionsWithUri} from 'request'; +import {layoutsApiRequest, getFields, getToken, logout} from "./GenericFunctions"; + +export class FileMaker implements INodeType { + description: INodeTypeDescription = { + displayName: 'FileMaker', + name: 'filemaker', + icon: 'file:filemaker.png', + group: ['input'], + version: 1, + description: 'Retrieve data from FileMaker data API.', + defaults: { + name: 'FileMaker', + color: '#665533', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'FileMaker', + required: true, + }, + ], + properties: [ + { + displayName: 'Action', + name: 'action', + type: 'options', + options: [ + /*{ + name: 'Login', + value: 'login', + }, + { + name: 'Logout', + value: 'logout', + },*/ + { + name: 'Find Records', + value: 'find', + }, + { + name: 'get Records', + value: 'records', + }, + { + name: 'Get Records By Id', + value: 'record', + }, + { + name: 'Perform Script', + value: 'performscript', + }, + { + name: 'Create Record', + value: 'create', + }, + { + name: 'Edit Record', + value: 'edit', + }, + { + name: 'Duplicate Record', + value: 'duplicate', + }, + { + name: 'Delete Record', + value: 'delete', + }, + ], + default: 'login', + description: 'Action to perform.', + }, + + // ---------------------------------- + // shared + // ---------------------------------- + { + displayName: 'Layout', + name: 'layout', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getLayouts', + }, + options: [], + default: '', + required: true, + displayOptions: { + hide: { + action: [ + 'performscript' + ], + }, + }, + placeholder: 'Layout Name', + description: 'FileMaker Layout Name.', + }, + { + displayName: 'Record Id', + name: 'recid', + type: 'number', + default: '', + required: true, + displayOptions: { + show: { + action: [ + 'record', + 'edit', + 'delete', + 'duplicate', + ], + }, + }, + placeholder: 'Record ID', + description: 'Internal Record ID returned by get (recordid)', + }, + // ---------------------------------- + // find/records + // ---------------------------------- + { + displayName: 'offset', + name: 'offset', + placeholder: '0', + description: 'The record number of the first record in the range of records.', + type: 'number', + default: '1', + displayOptions: { + show: { + action: [ + 'find', + 'records', + ], + }, + } + }, + { + displayName: 'limit', + name: 'limit', + placeholder: '100', + description: 'The maximum number of records that should be returned. If not specified, the default value is 100.', + type: 'number', + default: '100', + displayOptions: { + show: { + action: [ + 'find', + 'records', + ], + }, + } + }, + { + displayName: 'Sort', + name: 'sortParametersUi', + placeholder: 'Add Sort Rules', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + action: [ + 'find', + 'records', + ], + }, + }, + description: 'Sort rules', + default: {}, + options: [ + { + name: 'rules', + displayName: 'Rules', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getFields', + }, + options: [], + description: 'Field Name.', + }, + { + displayName: 'Value', + name: 'value', + type: 'options', + default: 'ascend', + options: [ + { + name: 'Ascend', + value: 'ascend' + }, + { + name: 'Descend', + value: 'descend' + }, + ], + description: 'Sort order.', + }, + ] + }, + ], + }, + // ---------------------------------- + // create/edit + // ---------------------------------- + { + displayName: 'fieldData', + name: 'fieldData', + placeholder: '{"field1": "value", "field2": "value", ...}', + description: 'Additional fields to add.', + type: 'string', + default: '{}', + displayOptions: { + show: { + action: [ + 'create', + 'edit', + ], + }, + } + }, + { + displayName: 'Fields', + name: 'Fields', + type: 'collection', + typeOptions: { + loadOptionsMethod: 'getFields', + }, + options: [], + default: '', + required: true, + displayOptions: { + show: { + action: [ + 'create', + 'edit', + ], + }, + }, + placeholder: 'Layout Name', + description: 'FileMaker Layout Name.', + }, + // ---------------------------------- + // performscript + // ---------------------------------- + { + displayName: 'Script Name', + name: 'script', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getScripts', + }, + options: [], + default: '', + required: true, + displayOptions: { + show: { + action: [ + 'performscript' + ], + }, + }, + placeholder: 'Layout Name', + description: 'FileMaker Layout Name.', + }, + ] + }; + + methods = { + loadOptions: { + // Get all the available topics to display them to user so that he can + // select them easily + async getLayouts(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + let layouts; + try { + layouts = await layoutsApiRequest.call(this); + } catch (err) { + throw new Error(`FileMaker Error: ${err}`); + } + for (const layout of layouts) { + returnData.push({ + name: layout.name, + value: layout.name, + }); + } + return returnData; + }, + + async getFields(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + + let fields; + try { + fields = await getFields.call(this); + } catch (err) { + throw new Error(`FileMaker Error: ${err}`); + } + for (const field of fields) { + returnData.push({ + name: field.name, + value: field.name, + }); + } + return returnData; + }, + }, + }; + + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: INodeExecutionData[] = []; + + const credentials = this.getCredentials('FileMaker'); + + const action = this.getNodeParameter('action', 0) as string; + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + const staticData = this.getWorkflowStaticData('global'); + // Operations which overwrite the returned data + const overwriteDataOperations = []; + // Operations which overwrite the returned data and return arrays + // and has so to be merged with the data of other items + const overwriteDataOperationsArray = []; + + let requestOptions: OptionsWithUri; + + const host = credentials.host as string; + const database = credentials.db as string; + + //const layout = this.getNodeParameter('layout', 0, null) as string; + //const recid = this.getNodeParameter('recid', 0, null) as number; + + const url = `https://${host}/fmi/data/v1`; + //const fullOperation = `${resource}:${operation}`; + + for (let i = 0; i < items.length; i++) { + // Reset all values + requestOptions = { + uri: '', + headers: {}, + method: 'GET', + json: true + //rejectUnauthorized: !this.getNodeParameter('allowUnauthorizedCerts', itemIndex, false) as boolean, + }; + + const layout = this.getNodeParameter('layout', 0) as string; + const token = await getToken.call(this); + + if (action === 'record') { + const recid = this.getNodeParameter('recid', 0) as string; + + requestOptions.uri = url + `/databases/${database}/layouts/${layout}/records/${recid}`; + requestOptions.method = 'GET'; + requestOptions.headers = { + 'Authorization': `Bearer ${token}`, + }; + } else if (action === 'records') { + requestOptions.uri = url + `/databases/${database}/layouts/${layout}/records`; + requestOptions.method = 'GET'; + requestOptions.headers = { + 'Authorization': `Bearer ${token}`, + }; + + const sort = []; + const sortParametersUi = this.getNodeParameter('sortParametersUi', 0, {}) as IDataObject; + if (sortParametersUi.parameter !== undefined) { + // @ts-ignore + for (const parameterData of sortParametersUi!.rules as IDataObject[]) { + // @ts-ignore + sort.push({ + 'fieldName': parameterData!.name as string, + 'sortOrder': parameterData!.value + }); + } + } + requestOptions.qs = { + '_offset': this.getNodeParameter('offset', 0), + '_limit': this.getNodeParameter('limit', 0), + '_sort': JSON.stringify(sort), + }; + } else { + throw new Error(`The action "${action}" is not implemented yet!`); + } + + // Now that the options are all set make the actual http request + let response; + try { + response = await this.helpers.request(requestOptions); + } catch (error) { + response = error.response.body; + } + + if (typeof response === 'string') { + throw new Error('Response body is not valid JSON. Change "Response Format" to "String"'); + } + await logout.call(this, token); + + returnData.push({json: response}); + } + + return this.prepareOutputData(returnData); + } +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/FileMaker/GenericFunctions.ts b/packages/nodes-base/nodes/FileMaker/GenericFunctions.ts new file mode 100644 index 0000000000..c76350e1f4 --- /dev/null +++ b/packages/nodes-base/nodes/FileMaker/GenericFunctions.ts @@ -0,0 +1,185 @@ +import { + IExecuteFunctions, + ILoadOptionsFunctions, + IExecuteSingleFunctions +} from 'n8n-core'; + +import { + ICredentialDataDecryptedObject, + IDataObject, +} from 'n8n-workflow'; + +import { OptionsWithUri } from 'request'; + + +/** + * Make an API request to ActiveCampaign + * + * @param {IHookFunctions} this + * @param {string} method + * @returns {Promise} + */ +export async function layoutsApiRequest(this: ILoadOptionsFunctions | IExecuteFunctions | IExecuteSingleFunctions): Promise { // tslint:disable-line:no-any + const token = await getToken.call(this); + const credentials = this.getCredentials('FileMaker'); + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + const host = credentials.host as string; + const db = credentials.db as string; + + const url = `https://${host}/fmi/data/v1/databases/${db}/layouts`; + const options: OptionsWithUri = { + headers: { + 'Authorization': `Bearer ${token}`, + }, + method: 'GET', + uri: url, + json: true + }; + + try { + const responseData = await this.helpers.request!(options); + return responseData.response.layouts; + + } catch (error) { + // If that data does not exist for some reason return the actual error + throw error; + } +} + +/** + * Make an API request to ActiveCampaign + * + * @returns {Promise} + * @param layout + */ +export async function getFields(this: ILoadOptionsFunctions | IExecuteFunctions | IExecuteSingleFunctions): Promise { // tslint:disable-line:no-any + const token = await getToken.call(this); + const credentials = this.getCredentials('FileMaker'); + const layout = this.getCurrentNodeParameter('layout') as string; + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + const host = credentials.host as string; + const db = credentials.db as string; + + const url = `https://${host}/fmi/data/v1/databases/${db}/layouts/${layout}`; + const options: OptionsWithUri = { + headers: { + 'Authorization': `Bearer ${token}`, + }, + method: 'GET', + uri: url, + json: true + }; + + try { + const responseData = await this.helpers.request!(options); + return responseData.response.fieldMetaData; + + } catch (error) { + // If that data does not exist for some reason return the actual error + throw error; + } +} + +export async function getToken(this: ILoadOptionsFunctions | IExecuteFunctions | IExecuteSingleFunctions): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('FileMaker'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + const host = credentials.host as string; + const db = credentials.db as string; + const login = credentials.login as string; + const password = credentials.password as string; + + const url = `https://${host}/fmi/data/v1/databases/${db}/sessions`; + + let requestOptions: OptionsWithUri; + // Reset all values + requestOptions = { + uri: url, + headers: {}, + method: 'POST', + json: true + //rejectUnauthorized: !this.getNodeParameter('allowUnauthorizedCerts', itemIndex, false) as boolean, + }; + requestOptions.auth = { + user: login as string, + pass: password as string, + }; + requestOptions.body = { + "fmDataSource": [ + { + "database": host, + "username": login as string, + "password": password as string + } + ] + }; + + try { + const response = await this.helpers.request!(requestOptions); + + if (typeof response === 'string') { + throw new Error('Response body is not valid JSON. Change "Response Format" to "String"'); + } + + return response.response.token; + } catch (error) { + console.error(error); + + const errorMessage = error.response.body.messages[0].message + '(' + error.response.body.messages[0].message + ')'; + + if (errorMessage !== undefined) { + throw errorMessage; + } + throw error.response.body; + } +} + +export async function logout(this: ILoadOptionsFunctions | IExecuteFunctions | IExecuteSingleFunctions, token: string): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('FileMaker'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + const host = credentials.host as string; + const db = credentials.db as string; + + const url = `https://${host}/fmi/data/v1/databases/${db}/sessions/${token}`; + + let requestOptions: OptionsWithUri; + // Reset all values + requestOptions = { + uri: url, + headers: {}, + method: 'DELETE', + json: true + //rejectUnauthorized: !this.getNodeParameter('allowUnauthorizedCerts', itemIndex, false) as boolean, + }; + + try { + const response = await this.helpers.request!(requestOptions); + + if (typeof response === 'string') { + throw new Error('Response body is not valid JSON. Change "Response Format" to "String"'); + } + + return response; + } catch (error) { + console.error(error); + + const errorMessage = error.response.body.messages[0].message + '(' + error.response.body.messages[0].message + ')'; + + if (errorMessage !== undefined) { + throw errorMessage; + } + throw error.response.body; + } +} + diff --git a/packages/nodes-base/nodes/FileMaker/filemaker.png b/packages/nodes-base/nodes/FileMaker/filemaker.png new file mode 100644 index 0000000000000000000000000000000000000000..ec691433dac024e2f3596fc5f9c310e5acc4c3a1 GIT binary patch literal 16244 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 Date: Sun, 10 Nov 2019 03:14:10 +0100 Subject: [PATCH 06/10] Handle portal selection & improve UX --- .../nodes/FileMaker/FileMaker.node.ts | 112 +++++++++++++++--- .../nodes/FileMaker/GenericFunctions.ts | 40 ++++++- 2 files changed, 134 insertions(+), 18 deletions(-) diff --git a/packages/nodes-base/nodes/FileMaker/FileMaker.node.ts b/packages/nodes-base/nodes/FileMaker/FileMaker.node.ts index 8caaca47d8..e7435fe15f 100644 --- a/packages/nodes-base/nodes/FileMaker/FileMaker.node.ts +++ b/packages/nodes-base/nodes/FileMaker/FileMaker.node.ts @@ -9,7 +9,7 @@ import { import {OptionsWithUri} from 'request'; -import {layoutsApiRequest, getFields, getToken, logout} from "./GenericFunctions"; +import {layoutsApiRequest, getFields, getPortals, getToken, logout} from "./GenericFunctions"; export class FileMaker implements INodeType { description: INodeTypeDescription = { @@ -36,6 +36,7 @@ export class FileMaker implements INodeType { displayName: 'Action', name: 'action', type: 'options', + default: 'record', options: [ /*{ name: 'Login', @@ -78,7 +79,6 @@ export class FileMaker implements INodeType { value: 'delete', }, ], - default: 'login', description: 'Action to perform.', }, @@ -124,9 +124,7 @@ export class FileMaker implements INodeType { placeholder: 'Record ID', description: 'Internal Record ID returned by get (recordid)', }, - // ---------------------------------- - // find/records - // ---------------------------------- + { displayName: 'offset', name: 'offset', @@ -159,6 +157,49 @@ export class FileMaker implements INodeType { }, } }, + { + displayName: 'Get portals', + name: 'getPortals', + type: 'boolean', + default: false, + description: 'Should we get portal data as well ?', + }, + { + displayName: 'Portals', + name: 'portals', + type: 'options', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add portal', + loadOptionsMethod: 'getPortals', + }, + options: [], + default: [], + displayOptions: { + show: { + action: [ + 'record', + 'records', + 'find', + ], + getPortals: [ + true, + ], + }, + }, + placeholder: 'Portals', + description: 'The portal result set to return. Use the portal object name or portal table name. If this parameter is omitted, the API will return all portal objects and records in the layout. For best performance, pass the portal object name or portal table name.', + }, + // ---------------------------------- + // find/records + // ---------------------------------- + { + displayName: 'Sort data ?', + name: 'setSort', + type: 'boolean', + default: false, + description: 'Should we sort data ?', + }, { displayName: 'Sort', name: 'sortParametersUi', @@ -169,6 +210,9 @@ export class FileMaker implements INodeType { }, displayOptions: { show: { + setSort: [ + true, + ], action: [ 'find', 'records', @@ -183,7 +227,7 @@ export class FileMaker implements INodeType { displayName: 'Rules', values: [ { - displayName: 'Name', + displayName: 'Field', name: 'name', type: 'options', default: '', @@ -194,7 +238,7 @@ export class FileMaker implements INodeType { description: 'Field Name.', }, { - displayName: 'Value', + displayName: 'Order', name: 'value', type: 'options', default: 'ascend', @@ -318,6 +362,25 @@ export class FileMaker implements INodeType { } return returnData; }, + + async getPortals(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + + let portals; + try { + portals = await getPortals.call(this); + } catch (err) { + throw new Error(`FileMaker Error: ${err}`); + } + Object.keys(portals).forEach((portal) => { + returnData.push({ + name: portal, + value: portal, + }); + }); + + return returnData; + }, }, }; @@ -379,22 +442,39 @@ export class FileMaker implements INodeType { 'Authorization': `Bearer ${token}`, }; - const sort = []; - const sortParametersUi = this.getNodeParameter('sortParametersUi', 0, {}) as IDataObject; - if (sortParametersUi.parameter !== undefined) { - // @ts-ignore - for (const parameterData of sortParametersUi!.rules as IDataObject[]) { + //Handle Sort + let sort; + const setSort = this.getNodeParameter('setSort', 0, false); + if (setSort) { + sort = null; + } else { + const sortParametersUi = this.getNodeParameter('sortParametersUi', 0, {}) as IDataObject; + if (sortParametersUi.rules !== undefined) { // @ts-ignore - sort.push({ - 'fieldName': parameterData!.name as string, - 'sortOrder': parameterData!.value - }); + for (const parameterData of sortParametersUi!.rules as IDataObject[]) { + // @ts-ignore + sort.push({ + 'fieldName': parameterData!.name as string, + 'sortOrder': parameterData!.value + }); + } } } + + //handle portals + let portals; + const getPortals = this.getNodeParameter('getPortals', 0); + if (!getPortals) { + portals = []; + } else { + portals = this.getNodeParameter('portals', 0); + } + requestOptions.qs = { '_offset': this.getNodeParameter('offset', 0), '_limit': this.getNodeParameter('limit', 0), '_sort': JSON.stringify(sort), + 'portal': JSON.stringify(portals), }; } else { throw new Error(`The action "${action}" is not implemented yet!`); diff --git a/packages/nodes-base/nodes/FileMaker/GenericFunctions.ts b/packages/nodes-base/nodes/FileMaker/GenericFunctions.ts index c76350e1f4..5fc1763b79 100644 --- a/packages/nodes-base/nodes/FileMaker/GenericFunctions.ts +++ b/packages/nodes-base/nodes/FileMaker/GenericFunctions.ts @@ -53,9 +53,8 @@ export async function layoutsApiRequest(this: ILoadOptionsFunctions | IExecuteFu * Make an API request to ActiveCampaign * * @returns {Promise} - * @param layout */ -export async function getFields(this: ILoadOptionsFunctions | IExecuteFunctions | IExecuteSingleFunctions): Promise { // tslint:disable-line:no-any +export async function getFields(this: ILoadOptionsFunctions): Promise { // tslint:disable-line:no-any const token = await getToken.call(this); const credentials = this.getCredentials('FileMaker'); const layout = this.getCurrentNodeParameter('layout') as string; @@ -86,6 +85,43 @@ export async function getFields(this: ILoadOptionsFunctions | IExecuteFunctions } } + +/** + * Make an API request to ActiveCampaign + * + * @returns {Promise} + */ +export async function getPortals(this: ILoadOptionsFunctions): Promise { // tslint:disable-line:no-any + const token = await getToken.call(this); + const credentials = this.getCredentials('FileMaker'); + const layout = this.getCurrentNodeParameter('layout') as string; + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + const host = credentials.host as string; + const db = credentials.db as string; + + const url = `https://${host}/fmi/data/v1/databases/${db}/layouts/${layout}`; + const options: OptionsWithUri = { + headers: { + 'Authorization': `Bearer ${token}`, + }, + method: 'GET', + uri: url, + json: true + }; + + try { + const responseData = await this.helpers.request!(options); + return responseData.response.portalMetaData; + + } catch (error) { + // If that data does not exist for some reason return the actual error + throw error; + } +} + export async function getToken(this: ILoadOptionsFunctions | IExecuteFunctions | IExecuteSingleFunctions): Promise { // tslint:disable-line:no-any const credentials = this.getCredentials('FileMaker'); if (credentials === undefined) { From bcfb7afe73a28d6339f438c4380fff70edf940a3 Mon Sep 17 00:00:00 2001 From: Romain Dunand Date: Tue, 12 Nov 2019 02:01:43 +0100 Subject: [PATCH 07/10] Handle Perform script Handle Perform script in queries Handle create/edit --- .../nodes/FileMaker/FileMaker.node.ts | 511 +++++++++++++++--- .../nodes/FileMaker/GenericFunctions.ts | 139 +++++ 2 files changed, 581 insertions(+), 69 deletions(-) diff --git a/packages/nodes-base/nodes/FileMaker/FileMaker.node.ts b/packages/nodes-base/nodes/FileMaker/FileMaker.node.ts index e7435fe15f..c747dc7c3e 100644 --- a/packages/nodes-base/nodes/FileMaker/FileMaker.node.ts +++ b/packages/nodes-base/nodes/FileMaker/FileMaker.node.ts @@ -9,7 +9,19 @@ import { import {OptionsWithUri} from 'request'; -import {layoutsApiRequest, getFields, getPortals, getToken, logout} from "./GenericFunctions"; +import { + layoutsApiRequest, + getFields, + getPortals, + getScripts, + getToken, + parseSort, + parsePortals, + parseQuery, + parseScripts, + parseFields, + logout +} from "./GenericFunctions"; export class FileMaker implements INodeType { description: INodeTypeDescription = { @@ -96,11 +108,6 @@ export class FileMaker implements INodeType { default: '', required: true, displayOptions: { - hide: { - action: [ - 'performscript' - ], - }, }, placeholder: 'Layout Name', description: 'FileMaker Layout Name.', @@ -124,7 +131,6 @@ export class FileMaker implements INodeType { placeholder: 'Record ID', description: 'Internal Record ID returned by get (recordid)', }, - { displayName: 'offset', name: 'offset', @@ -163,6 +169,15 @@ export class FileMaker implements INodeType { type: 'boolean', default: false, description: 'Should we get portal data as well ?', + displayOptions: { + show: { + action: [ + 'record', + 'records', + 'find', + ], + }, + }, }, { displayName: 'Portals', @@ -193,12 +208,107 @@ export class FileMaker implements INodeType { // ---------------------------------- // find/records // ---------------------------------- + { + displayName: 'Response Layout', + name: 'responseLayout', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getLayouts', + }, + options: [], + default: '', + required: false, + displayOptions: { + show: { + action: [ + 'find' + ], + }, + }, + }, + { + displayName: 'Queries', + name: 'queries', + placeholder: 'Add query', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + action: [ + 'find', + ], + }, + }, + description: 'Queries ', + default: {}, + options: [ + { + name: 'query', + displayName: 'Query', + values: [ + { + displayName: 'Fields', + name: 'fields', + placeholder: 'Add field', + type: 'fixedCollection', + default: {}, + typeOptions: { + multipleValues: true, + }, + options: [{ + name: 'field', + displayName: 'Field', + values: [ + { + displayName: 'Field', + name: 'name', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getFields', + }, + options: [], + description: 'Search Field', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value to search', + }, + ] + } + ], + description: 'Field Name', + }, + { + displayName: 'Omit', + name: 'omit', + type: 'boolean', + default: false + }, + ] + }, + ], + }, { displayName: 'Sort data ?', name: 'setSort', type: 'boolean', default: false, description: 'Should we sort data ?', + displayOptions: { + show: { + action: [ + 'find', + 'record', + 'records', + ], + }, + }, }, { displayName: 'Sort', @@ -258,6 +368,192 @@ export class FileMaker implements INodeType { }, ], }, + { + displayName: 'Before find script', + name: 'setScriptBefore', + type: 'boolean', + default: false, + description: 'Define a script to be run before the action specified by the API call and after the subsequent sort.', + displayOptions: { + show: { + action: [ + 'find', + 'record', + 'records', + ], + } + }, + }, + { + displayName: 'Script Name', + name: 'scriptBefore', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getScripts', + }, + options: [], + default: '', + required: true, + displayOptions: { + show: { + action: [ + 'find', + 'record', + 'records', + ], + setScriptBefore: [ + true + ], + }, + }, + placeholder: 'Script Name', + description: 'The name of the FileMaker script to be run after the action specified by the API call and after the subsequent sort.', + }, + { + displayName: 'Script Parameter', + name: 'scriptBeforeParam', + type: 'string', + default: '', + required: false, + displayOptions: { + show: { + action: [ + 'find', + 'record', + 'records', + ], + setScriptBefore: [ + true + ], + }, + }, + placeholder: 'Script Parameters', + description: 'A parameter for the FileMaker script.', + }, + { + displayName: 'Before sort script', + name: 'setScriptSort', + type: 'boolean', + default: false, + description: 'Define a script to be run after the action specified by the API call but before the subsequent sort.', + displayOptions: { + show: { + action: [ + 'find', + 'record', + 'records', + ], + } + }, + }, + { + displayName: 'Script Name', + name: 'scriptSort', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getScripts', + }, + options: [], + default: '', + required: true, + displayOptions: { + show: { + action: [ + 'find', + 'record', + 'records', + ], + setScriptSort: [ + true + ], + }, + }, + placeholder: 'Script Name', + description: 'The name of the FileMaker script to be run after the action specified by the API call but before the subsequent sort.', + }, + { + displayName: 'Script Parameter', + name: 'scriptSortParam', + type: 'string', + default: '', + required: false, + displayOptions: { + show: { + action: [ + 'find', + 'record', + 'records', + ], + setScriptSort: [ + true + ], + }, + }, + placeholder: 'Script Parameters', + description: 'A parameter for the FileMaker script.', + }, + { + displayName: 'After sort script', + name: 'setScriptAfter', + type: 'boolean', + default: false, + description: 'Define a script to be run after the action specified by the API call but before the subsequent sort.', + displayOptions: { + show: { + action: [ + 'find', + 'record', + 'records', + ], + } + }, + }, + { + displayName: 'Script Name', + name: 'scriptAfter', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getScripts', + }, + options: [], + default: '', + required: true, + displayOptions: { + show: { + action: [ + 'find', + 'record', + 'records', + ], + setScriptAfter: [ + true + ], + }, + }, + placeholder: 'Script Name', + description: 'The name of the FileMaker script to be run after the action specified by the API call and after the subsequent sort.', + }, + { + displayName: 'Script Parameter', + name: 'scriptAfterParam', + type: 'string', + default: '', + required: false, + displayOptions: { + show: { + action: [ + 'find', + 'record', + 'records', + ], + setScriptAfter: [ + true + ], + }, + }, + placeholder: 'Script Parameters', + description: 'A parameter for the FileMaker script.', + }, // ---------------------------------- // create/edit // ---------------------------------- @@ -278,15 +574,27 @@ export class FileMaker implements INodeType { } }, { - displayName: 'Fields', - name: 'Fields', - type: 'collection', - typeOptions: { - loadOptionsMethod: 'getFields', - }, - options: [], + displayName: 'Mod Id', + name: 'modId', + description: 'The last modification ID. When you use modId, a record is edited only when the modId matches.', + type: 'number', default: '', - required: true, + displayOptions: { + show: { + action: [ + 'edit', + ], + }, + } + }, + { + displayName: 'Fields', + name: 'fieldsParametersUi', + placeholder: 'Add field', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, displayOptions: { show: { action: [ @@ -295,8 +603,33 @@ export class FileMaker implements INodeType { ], }, }, - placeholder: 'Layout Name', - description: 'FileMaker Layout Name.', + description: 'Fields to define', + default: {}, + options: [ + { + name: 'fields', + displayName: 'Fields', + values: [ + { + displayName: 'Field', + name: 'name', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getFields', + }, + options: [], + description: 'Field Name.', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + }, + ] + }, + ], }, // ---------------------------------- // performscript @@ -318,8 +651,24 @@ export class FileMaker implements INodeType { ], }, }, - placeholder: 'Layout Name', - description: 'FileMaker Layout Name.', + placeholder: 'Script Name', + description: 'The name of the FileMaker script to be run.', + }, + { + displayName: 'Script Parameter', + name: 'scriptParam', + type: 'string', + default: '', + required: false, + displayOptions: { + show: { + action: [ + 'performscript' + ], + }, + }, + placeholder: 'Script Parameters', + description: 'A parameter for the FileMaker script.', }, ] }; @@ -363,6 +712,26 @@ export class FileMaker implements INodeType { return returnData; }, + async getScripts(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + + let scripts; + try { + scripts = await getScripts.call(this); + } catch (err) { + throw new Error(`FileMaker Error: ${err}`); + } + for (const script of scripts) { + if (!script.isFolder) { + returnData.push({ + name: script.name, + value: script.name, + }); + } + } + return returnData; + }, + async getPortals(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; @@ -396,85 +765,89 @@ export class FileMaker implements INodeType { if (credentials === undefined) { throw new Error('No credentials got returned!'); } + const token = await getToken.call(this); + const staticData = this.getWorkflowStaticData('global'); - // Operations which overwrite the returned data - const overwriteDataOperations = []; - // Operations which overwrite the returned data and return arrays - // and has so to be merged with the data of other items - const overwriteDataOperationsArray = []; let requestOptions: OptionsWithUri; const host = credentials.host as string; const database = credentials.db as string; - //const layout = this.getNodeParameter('layout', 0, null) as string; - //const recid = this.getNodeParameter('recid', 0, null) as number; - const url = `https://${host}/fmi/data/v1`; - //const fullOperation = `${resource}:${operation}`; for (let i = 0; i < items.length; i++) { // Reset all values requestOptions = { uri: '', - headers: {}, + headers: { + 'Authorization': `Bearer ${token}`, + }, method: 'GET', json: true //rejectUnauthorized: !this.getNodeParameter('allowUnauthorizedCerts', itemIndex, false) as boolean, }; const layout = this.getNodeParameter('layout', 0) as string; - const token = await getToken.call(this); if (action === 'record') { const recid = this.getNodeParameter('recid', 0) as string; - requestOptions.uri = url + `/databases/${database}/layouts/${layout}/records/${recid}`; - requestOptions.method = 'GET'; - requestOptions.headers = { - 'Authorization': `Bearer ${token}`, + requestOptions.qs = { + 'portal': JSON.stringify(parsePortals.call(this)), + ...parseScripts.call(this) }; } else if (action === 'records') { requestOptions.uri = url + `/databases/${database}/layouts/${layout}/records`; - requestOptions.method = 'GET'; - requestOptions.headers = { - 'Authorization': `Bearer ${token}`, - }; - - //Handle Sort - let sort; - const setSort = this.getNodeParameter('setSort', 0, false); - if (setSort) { - sort = null; - } else { - const sortParametersUi = this.getNodeParameter('sortParametersUi', 0, {}) as IDataObject; - if (sortParametersUi.rules !== undefined) { - // @ts-ignore - for (const parameterData of sortParametersUi!.rules as IDataObject[]) { - // @ts-ignore - sort.push({ - 'fieldName': parameterData!.name as string, - 'sortOrder': parameterData!.value - }); - } - } - } - - //handle portals - let portals; - const getPortals = this.getNodeParameter('getPortals', 0); - if (!getPortals) { - portals = []; - } else { - portals = this.getNodeParameter('portals', 0); - } - requestOptions.qs = { '_offset': this.getNodeParameter('offset', 0), '_limit': this.getNodeParameter('limit', 0), - '_sort': JSON.stringify(sort), - 'portal': JSON.stringify(portals), + '_sort': JSON.stringify(parseSort.call(this)), + 'portal': JSON.stringify(parsePortals.call(this)), + ...parseScripts.call(this) + }; + } else if (action === 'find') { + requestOptions.uri = url + `/databases/${database}/layouts/${layout}/_find`; + requestOptions.method = 'POST'; + requestOptions.body = { + 'query': parseQuery.call(this), + 'offset': this.getNodeParameter('offset', 0), + 'limit': this.getNodeParameter('limit', 0), + 'layout.response': this.getNodeParameter('responseLayout', 0), + ...parseScripts.call(this) + }; + const sort = parseSort.call(this); + if (sort) { + requestOptions.body.sort = sort; + } + } else if (action === 'create') { + requestOptions.uri = url + `/databases/${database}/layouts/${layout}/records`; + requestOptions.method = 'POST'; + requestOptions.headers['Content-Type'] = 'application/json'; + + //TODO: handle portalData + requestOptions.body = { + fieldData: {...parseFields.call(this)}, + portalData: {}, + ...parseScripts.call(this) + }; + } else if (action === 'edit') { + const recid = this.getNodeParameter('recid', 0) as string; + requestOptions.uri = url + `/databases/${database}/layouts/${layout}/records/${recid}`; + requestOptions.method = 'PATCH'; + requestOptions.headers['Content-Type'] = 'application/json'; + + //TODO: handle portalData + requestOptions.body = { + fieldData: {...parseFields.call(this)}, + portalData: {}, + ...parseScripts.call(this) + }; + } else if (action === 'performscript') { + const scriptName = this.getNodeParameter('script', 0) as string; + requestOptions.uri = url + `/databases/${database}/layouts/${layout}/script/${scriptName}`; + requestOptions.qs = { + 'script.param': this.getNodeParameter('scriptParam', 0), }; } else { throw new Error(`The action "${action}" is not implemented yet!`); diff --git a/packages/nodes-base/nodes/FileMaker/GenericFunctions.ts b/packages/nodes-base/nodes/FileMaker/GenericFunctions.ts index 5fc1763b79..5e24fe978b 100644 --- a/packages/nodes-base/nodes/FileMaker/GenericFunctions.ts +++ b/packages/nodes-base/nodes/FileMaker/GenericFunctions.ts @@ -122,6 +122,42 @@ export async function getPortals(this: ILoadOptionsFunctions): Promise { // } } +/** + * Make an API request to ActiveCampaign + * + * @returns {Promise} + */ +export async function getScripts(this: ILoadOptionsFunctions): Promise { // tslint:disable-line:no-any + const token = await getToken.call(this); + const credentials = this.getCredentials('FileMaker'); + const layout = this.getCurrentNodeParameter('layout') as string; + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + const host = credentials.host as string; + const db = credentials.db as string; + + const url = `https://${host}/fmi/data/v1/databases/${db}/scripts`; + const options: OptionsWithUri = { + headers: { + 'Authorization': `Bearer ${token}`, + }, + method: 'GET', + uri: url, + json: true + }; + + try { + const responseData = await this.helpers.request!(options); + return responseData.response.scripts; + + } catch (error) { + // If that data does not exist for some reason return the actual error + throw error; + } +} + export async function getToken(this: ILoadOptionsFunctions | IExecuteFunctions | IExecuteSingleFunctions): Promise { // tslint:disable-line:no-any const credentials = this.getCredentials('FileMaker'); if (credentials === undefined) { @@ -219,3 +255,106 @@ export async function logout(this: ILoadOptionsFunctions | IExecuteFunctions | I } } +export function parseSort(this: IExecuteFunctions): object | null { + let sort; + const setSort = this.getNodeParameter('setSort', 0, false); + if (!setSort) { + sort = null; + } else { + sort = []; + const sortParametersUi = this.getNodeParameter('sortParametersUi', 0, {}) as IDataObject; + if (sortParametersUi.rules !== undefined) { + // @ts-ignore + for (const parameterData of sortParametersUi!.rules as IDataObject[]) { + // @ts-ignore + sort.push({ + 'fieldName': parameterData!.name as string, + 'sortOrder': parameterData!.value + }); + } + } + } + return sort; +} + + +export function parseScripts(this: IExecuteFunctions): object | null { + const setScriptAfter = this.getNodeParameter('setScriptAfter', 0, false); + const setScriptBefore = this.getNodeParameter('setScriptBefore', 0, false); + const setScriptSort = this.getNodeParameter('setScriptSort', 0, false); + + if (!setScriptAfter && setScriptBefore && setScriptSort) { + return {}; + } else { + const scripts = { + }; + if (setScriptAfter) { + scripts.script = this.getNodeParameter('scriptAfter', 0); + scripts['script.param'] = this.getNodeParameter('scriptAfter', 0); + } + if (setScriptBefore) { + scripts['script.prerequest'] = this.getNodeParameter('scriptBefore', 0); + scripts['script.prerequest.param'] = this.getNodeParameter('scriptBeforeParam', 0); + } + if (setScriptSort) { + scripts['script.presort'] = this.getNodeParameter('scriptSort', 0); + scripts['script.presort.param'] = this.getNodeParameter('scriptSortParam', 0); + } + return scripts; + } +} + +export function parsePortals(this: IExecuteFunctions): object | null { + let portals; + const getPortals = this.getNodeParameter('getPortals', 0); + if (!getPortals) { + portals = []; + } else { + portals = this.getNodeParameter('portals', 0); + } + // @ts-ignore + return portals; +} + + +export function parseQuery(this: IExecuteFunctions): object | null { + let queries; + const queriesParamUi = this.getNodeParameter('queries', 0, {}) as IDataObject; + if (queriesParamUi.query !== undefined) { + // @ts-ignore + queries = []; + for (const queryParam of queriesParamUi!.query as IDataObject[]) { + const query = { + 'omit': queryParam.omit ? 'true' : 'false', + }; + // @ts-ignore + for (const field of queryParam!.fields!.field as IDataObject[]) { + // @ts-ignore + query[field.name] =field!.value; + } + queries.push(query); + } + } else { + queries = null; + } + // @ts-ignore + return queries; +} + +export function parseFields(this: IExecuteFunctions): object | null { + let fieldData; + const fieldsParametersUi = this.getNodeParameter('fieldsParametersUi', 0, {}) as IDataObject; + if (fieldsParametersUi.fields !== undefined) { + // @ts-ignore + fieldData = {}; + for (const field of fieldsParametersUi!.fields as IDataObject[]) { + // @ts-ignore + fieldData[field.name] =field!.value; + } + } else { + fieldData = null; + } + // @ts-ignore + return fieldData; +} + From 78070497cccb5f9b06bc6d413a25da2a8d19dae8 Mon Sep 17 00:00:00 2001 From: Romain Dunand Date: Wed, 13 Nov 2019 00:00:58 +0100 Subject: [PATCH 08/10] Handle duplicate/delete actions Code cleanup Improve error handling --- .../nodes/FileMaker/FileMaker.node.ts | 65 +++++++++++++++---- .../nodes/FileMaker/GenericFunctions.ts | 25 +++++-- 2 files changed, 72 insertions(+), 18 deletions(-) diff --git a/packages/nodes-base/nodes/FileMaker/FileMaker.node.ts b/packages/nodes-base/nodes/FileMaker/FileMaker.node.ts index c747dc7c3e..ff8b893fa5 100644 --- a/packages/nodes-base/nodes/FileMaker/FileMaker.node.ts +++ b/packages/nodes-base/nodes/FileMaker/FileMaker.node.ts @@ -213,7 +213,7 @@ export class FileMaker implements INodeType { name: 'responseLayout', type: 'options', typeOptions: { - loadOptionsMethod: 'getLayouts', + loadOptionsMethod: 'getResponseLayouts', }, options: [], default: '', @@ -557,7 +557,7 @@ export class FileMaker implements INodeType { // ---------------------------------- // create/edit // ---------------------------------- - { + /*{ displayName: 'fieldData', name: 'fieldData', placeholder: '{"field1": "value", "field2": "value", ...}', @@ -572,7 +572,7 @@ export class FileMaker implements INodeType { ], }, } - }, + },*/ { displayName: 'Mod Id', name: 'modId', @@ -679,6 +679,28 @@ export class FileMaker implements INodeType { // select them easily async getLayouts(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; + + let layouts; + try { + layouts = await layoutsApiRequest.call(this); + } catch (err) { + throw new Error(`FileMaker Error: ${err}`); + } + for (const layout of layouts) { + returnData.push({ + name: layout.name, + value: layout.name, + }); + } + return returnData; + }, + async getResponseLayouts(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + returnData.push({ + name: 'Use main layout', + value: '', + }); + let layouts; try { layouts = await layoutsApiRequest.call(this); @@ -760,14 +782,16 @@ export class FileMaker implements INodeType { const credentials = this.getCredentials('FileMaker'); - const action = this.getNodeParameter('action', 0) as string; - if (credentials === undefined) { throw new Error('No credentials got returned!'); } - const token = await getToken.call(this); - const staticData = this.getWorkflowStaticData('global'); + let token; + try { + token = await getToken.call(this); + } catch (e) { + throw new Error(`Login fail: ${e}`); + } let requestOptions: OptionsWithUri; @@ -776,6 +800,8 @@ export class FileMaker implements INodeType { const url = `https://${host}/fmi/data/v1`; + const action = this.getNodeParameter('action', 0) as string; + for (let i = 0; i < items.length; i++) { // Reset all values requestOptions = { @@ -785,7 +811,6 @@ export class FileMaker implements INodeType { }, method: 'GET', json: true - //rejectUnauthorized: !this.getNodeParameter('allowUnauthorizedCerts', itemIndex, false) as boolean, }; const layout = this.getNodeParameter('layout', 0) as string; @@ -802,10 +827,13 @@ export class FileMaker implements INodeType { requestOptions.qs = { '_offset': this.getNodeParameter('offset', 0), '_limit': this.getNodeParameter('limit', 0), - '_sort': JSON.stringify(parseSort.call(this)), 'portal': JSON.stringify(parsePortals.call(this)), ...parseScripts.call(this) }; + const sort = parseSort.call(this); + if (sort) { + requestOptions.body.sort = sort; + } } else if (action === 'find') { requestOptions.uri = url + `/databases/${database}/layouts/${layout}/_find`; requestOptions.method = 'POST'; @@ -823,7 +851,7 @@ export class FileMaker implements INodeType { } else if (action === 'create') { requestOptions.uri = url + `/databases/${database}/layouts/${layout}/records`; requestOptions.method = 'POST'; - requestOptions.headers['Content-Type'] = 'application/json'; + requestOptions.headers!['Content-Type'] = 'application/json'; //TODO: handle portalData requestOptions.body = { @@ -835,7 +863,7 @@ export class FileMaker implements INodeType { const recid = this.getNodeParameter('recid', 0) as string; requestOptions.uri = url + `/databases/${database}/layouts/${layout}/records/${recid}`; requestOptions.method = 'PATCH'; - requestOptions.headers['Content-Type'] = 'application/json'; + requestOptions.headers!['Content-Type'] = 'application/json'; //TODO: handle portalData requestOptions.body = { @@ -849,6 +877,21 @@ export class FileMaker implements INodeType { requestOptions.qs = { 'script.param': this.getNodeParameter('scriptParam', 0), }; + } else if (action === 'duplicate') { + const recid = this.getNodeParameter('recid', 0) as string; + requestOptions.uri = url + `/databases/${database}/layouts/${layout}/records/${recid}`; + requestOptions.method = 'POST'; + requestOptions.headers!['Content-Type'] = 'application/json'; + requestOptions.qs = { + ...parseScripts.call(this) + }; + } else if (action === 'delete') { + const recid = this.getNodeParameter('recid', 0) as string; + requestOptions.uri = url + `/databases/${database}/layouts/${layout}/records/${recid}`; + requestOptions.method = 'DELETE'; + requestOptions.qs = { + ...parseScripts.call(this) + }; } else { throw new Error(`The action "${action}" is not implemented yet!`); } diff --git a/packages/nodes-base/nodes/FileMaker/GenericFunctions.ts b/packages/nodes-base/nodes/FileMaker/GenericFunctions.ts index 5e24fe978b..a0470bcb10 100644 --- a/packages/nodes-base/nodes/FileMaker/GenericFunctions.ts +++ b/packages/nodes-base/nodes/FileMaker/GenericFunctions.ts @@ -10,8 +10,16 @@ import { } from 'n8n-workflow'; import { OptionsWithUri } from 'request'; +import {Url} from "url"; - +interface ScriptsOptions { + script?: any; //tslint:disable-line:no-any + 'script.param'?: any; //tslint:disable-line:no-any + 'script.prerequest'?: any; //tslint:disable-line:no-any + 'script.prerequest.param'?: any; //tslint:disable-line:no-any + 'script.presort'?: any; //tslint:disable-line:no-any + 'script.presort.param'?: any; //tslint:disable-line:no-any +} /** * Make an API request to ActiveCampaign * @@ -205,12 +213,16 @@ export async function getToken(this: ILoadOptionsFunctions | IExecuteFunctions | } catch (error) { console.error(error); - const errorMessage = error.response.body.messages[0].message + '(' + error.response.body.messages[0].message + ')'; - + let errorMessage; + if (error.response) { + errorMessage = error.response.body.messages[0].message + '(' + error.response.body.messages[0].message + ')'; + } else { + errorMessage = `${error.message} (${error.name})`; + } if (errorMessage !== undefined) { throw errorMessage; } - throw error.response.body; + throw error.message; } } @@ -286,11 +298,10 @@ export function parseScripts(this: IExecuteFunctions): object | null { if (!setScriptAfter && setScriptBefore && setScriptSort) { return {}; } else { - const scripts = { - }; + const scripts = {} as ScriptsOptions; if (setScriptAfter) { scripts.script = this.getNodeParameter('scriptAfter', 0); - scripts['script.param'] = this.getNodeParameter('scriptAfter', 0); + scripts!['script.param'] = this.getNodeParameter('scriptAfter', 0); } if (setScriptBefore) { scripts['script.prerequest'] = this.getNodeParameter('scriptBefore', 0); From 894a41e6d9ee025aa52f934a06a32ff8dd391848 Mon Sep 17 00:00:00 2001 From: Romain Dunand Date: Thu, 14 Nov 2019 13:02:57 +0100 Subject: [PATCH 09/10] Fix code indent style --- .../nodes/FileMaker/FileMaker.node.ts | 4 ++-- .../nodes/FileMaker/GenericFunctions.ts | 21 ++++++++++--------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/nodes-base/nodes/FileMaker/FileMaker.node.ts b/packages/nodes-base/nodes/FileMaker/FileMaker.node.ts index ff8b893fa5..6cd02a9bcb 100644 --- a/packages/nodes-base/nodes/FileMaker/FileMaker.node.ts +++ b/packages/nodes-base/nodes/FileMaker/FileMaker.node.ts @@ -889,7 +889,7 @@ export class FileMaker implements INodeType { const recid = this.getNodeParameter('recid', 0) as string; requestOptions.uri = url + `/databases/${database}/layouts/${layout}/records/${recid}`; requestOptions.method = 'DELETE'; - requestOptions.qs = { + requestOptions.qs = { ...parseScripts.call(this) }; } else { @@ -914,4 +914,4 @@ export class FileMaker implements INodeType { return this.prepareOutputData(returnData); } -} \ No newline at end of file +} diff --git a/packages/nodes-base/nodes/FileMaker/GenericFunctions.ts b/packages/nodes-base/nodes/FileMaker/GenericFunctions.ts index a0470bcb10..f2d9ff9ca3 100644 --- a/packages/nodes-base/nodes/FileMaker/GenericFunctions.ts +++ b/packages/nodes-base/nodes/FileMaker/GenericFunctions.ts @@ -9,7 +9,7 @@ import { IDataObject, } from 'n8n-workflow'; -import { OptionsWithUri } from 'request'; +import {OptionsWithUri} from 'request'; import {Url} from "url"; interface ScriptsOptions { @@ -20,6 +20,7 @@ interface ScriptsOptions { 'script.presort'?: any; //tslint:disable-line:no-any 'script.presort.param'?: any; //tslint:disable-line:no-any } + /** * Make an API request to ActiveCampaign * @@ -269,12 +270,12 @@ export async function logout(this: ILoadOptionsFunctions | IExecuteFunctions | I export function parseSort(this: IExecuteFunctions): object | null { let sort; - const setSort = this.getNodeParameter('setSort', 0, false); + const setSort = this.getNodeParameter('setSort', 0, false); if (!setSort) { sort = null; } else { sort = []; - const sortParametersUi = this.getNodeParameter('sortParametersUi', 0, {}) as IDataObject; + const sortParametersUi = this.getNodeParameter('sortParametersUi', 0, {}) as IDataObject; if (sortParametersUi.rules !== undefined) { // @ts-ignore for (const parameterData of sortParametersUi!.rules as IDataObject[]) { @@ -291,9 +292,9 @@ export function parseSort(this: IExecuteFunctions): object | null { export function parseScripts(this: IExecuteFunctions): object | null { - const setScriptAfter = this.getNodeParameter('setScriptAfter', 0, false); - const setScriptBefore = this.getNodeParameter('setScriptBefore', 0, false); - const setScriptSort = this.getNodeParameter('setScriptSort', 0, false); + const setScriptAfter = this.getNodeParameter('setScriptAfter', 0, false); + const setScriptBefore = this.getNodeParameter('setScriptBefore', 0, false); + const setScriptSort = this.getNodeParameter('setScriptSort', 0, false); if (!setScriptAfter && setScriptBefore && setScriptSort) { return {}; @@ -330,7 +331,7 @@ export function parsePortals(this: IExecuteFunctions): object | null { export function parseQuery(this: IExecuteFunctions): object | null { let queries; - const queriesParamUi = this.getNodeParameter('queries', 0, {}) as IDataObject; + const queriesParamUi = this.getNodeParameter('queries', 0, {}) as IDataObject; if (queriesParamUi.query !== undefined) { // @ts-ignore queries = []; @@ -341,7 +342,7 @@ export function parseQuery(this: IExecuteFunctions): object | null { // @ts-ignore for (const field of queryParam!.fields!.field as IDataObject[]) { // @ts-ignore - query[field.name] =field!.value; + query[field.name] = field!.value; } queries.push(query); } @@ -354,13 +355,13 @@ export function parseQuery(this: IExecuteFunctions): object | null { export function parseFields(this: IExecuteFunctions): object | null { let fieldData; - const fieldsParametersUi = this.getNodeParameter('fieldsParametersUi', 0, {}) as IDataObject; + const fieldsParametersUi = this.getNodeParameter('fieldsParametersUi', 0, {}) as IDataObject; if (fieldsParametersUi.fields !== undefined) { // @ts-ignore fieldData = {}; for (const field of fieldsParametersUi!.fields as IDataObject[]) { // @ts-ignore - fieldData[field.name] =field!.value; + fieldData[field.name] = field!.value; } } else { fieldData = null; From 5fae305cf040a7318cc1e33770a77ca54da1676e Mon Sep 17 00:00:00 2001 From: Romain Dunand Date: Fri, 22 Nov 2019 00:42:26 +0100 Subject: [PATCH 10/10] Fix code indent style --- .../nodes/FileMaker/FileMaker.node.ts | 1711 ++++++++--------- 1 file changed, 855 insertions(+), 856 deletions(-) diff --git a/packages/nodes-base/nodes/FileMaker/FileMaker.node.ts b/packages/nodes-base/nodes/FileMaker/FileMaker.node.ts index 6cd02a9bcb..4a8f384573 100644 --- a/packages/nodes-base/nodes/FileMaker/FileMaker.node.ts +++ b/packages/nodes-base/nodes/FileMaker/FileMaker.node.ts @@ -1,56 +1,56 @@ import {IExecuteFunctions} from 'n8n-core'; import { - IDataObject, - ILoadOptionsFunctions, - INodeExecutionData, INodePropertyOptions, - INodeType, - INodeTypeDescription, + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, INodePropertyOptions, + INodeType, + INodeTypeDescription, } from 'n8n-workflow'; import {OptionsWithUri} from 'request'; import { - layoutsApiRequest, - getFields, - getPortals, - getScripts, - getToken, - parseSort, - parsePortals, - parseQuery, - parseScripts, - parseFields, - logout + layoutsApiRequest, + getFields, + getPortals, + getScripts, + getToken, + parseSort, + parsePortals, + parseQuery, + parseScripts, + parseFields, + logout } from "./GenericFunctions"; export class FileMaker implements INodeType { - description: INodeTypeDescription = { - displayName: 'FileMaker', - name: 'filemaker', - icon: 'file:filemaker.png', - group: ['input'], - version: 1, - description: 'Retrieve data from FileMaker data API.', - defaults: { - name: 'FileMaker', - color: '#665533', - }, - inputs: ['main'], - outputs: ['main'], - credentials: [ - { - name: 'FileMaker', - required: true, - }, - ], - properties: [ - { - displayName: 'Action', - name: 'action', - type: 'options', - default: 'record', - options: [ - /*{ + description: INodeTypeDescription = { + displayName: 'FileMaker', + name: 'filemaker', + icon: 'file:filemaker.png', + group: ['input'], + version: 1, + description: 'Retrieve data from FileMaker data API.', + defaults: { + name: 'FileMaker', + color: '#665533', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'FileMaker', + required: true, + }, + ], + properties: [ + { + displayName: 'Action', + name: 'action', + type: 'options', + default: 'record', + options: [ + /*{ name: 'Login', value: 'login', }, @@ -58,506 +58,505 @@ export class FileMaker implements INodeType { name: 'Logout', value: 'logout', },*/ - { - name: 'Find Records', - value: 'find', - }, - { - name: 'get Records', - value: 'records', - }, - { - name: 'Get Records By Id', - value: 'record', - }, - { - name: 'Perform Script', - value: 'performscript', - }, - { - name: 'Create Record', - value: 'create', - }, - { - name: 'Edit Record', - value: 'edit', - }, - { - name: 'Duplicate Record', - value: 'duplicate', - }, - { - name: 'Delete Record', - value: 'delete', - }, - ], - description: 'Action to perform.', - }, + { + name: 'Find Records', + value: 'find', + }, + { + name: 'get Records', + value: 'records', + }, + { + name: 'Get Records By Id', + value: 'record', + }, + { + name: 'Perform Script', + value: 'performscript', + }, + { + name: 'Create Record', + value: 'create', + }, + { + name: 'Edit Record', + value: 'edit', + }, + { + name: 'Duplicate Record', + value: 'duplicate', + }, + { + name: 'Delete Record', + value: 'delete', + }, + ], + description: 'Action to perform.', + }, - // ---------------------------------- - // shared - // ---------------------------------- - { - displayName: 'Layout', - name: 'layout', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getLayouts', - }, - options: [], - default: '', - required: true, - displayOptions: { - }, - placeholder: 'Layout Name', - description: 'FileMaker Layout Name.', - }, - { - displayName: 'Record Id', - name: 'recid', - type: 'number', - default: '', - required: true, - displayOptions: { - show: { - action: [ - 'record', - 'edit', - 'delete', - 'duplicate', - ], - }, - }, - placeholder: 'Record ID', - description: 'Internal Record ID returned by get (recordid)', - }, - { - displayName: 'offset', - name: 'offset', - placeholder: '0', - description: 'The record number of the first record in the range of records.', - type: 'number', - default: '1', - displayOptions: { - show: { - action: [ - 'find', - 'records', - ], - }, - } - }, - { - displayName: 'limit', - name: 'limit', - placeholder: '100', - description: 'The maximum number of records that should be returned. If not specified, the default value is 100.', - type: 'number', - default: '100', - displayOptions: { - show: { - action: [ - 'find', - 'records', - ], - }, - } - }, - { - displayName: 'Get portals', - name: 'getPortals', - type: 'boolean', - default: false, - description: 'Should we get portal data as well ?', - displayOptions: { - show: { - action: [ - 'record', - 'records', - 'find', - ], - }, - }, - }, - { - displayName: 'Portals', - name: 'portals', - type: 'options', - typeOptions: { - multipleValues: true, - multipleValueButtonText: 'Add portal', - loadOptionsMethod: 'getPortals', - }, - options: [], - default: [], - displayOptions: { - show: { - action: [ - 'record', - 'records', - 'find', - ], - getPortals: [ - true, - ], - }, - }, - placeholder: 'Portals', - description: 'The portal result set to return. Use the portal object name or portal table name. If this parameter is omitted, the API will return all portal objects and records in the layout. For best performance, pass the portal object name or portal table name.', - }, - // ---------------------------------- - // find/records - // ---------------------------------- - { - displayName: 'Response Layout', - name: 'responseLayout', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getResponseLayouts', - }, - options: [], - default: '', - required: false, - displayOptions: { - show: { - action: [ - 'find' - ], - }, - }, - }, - { - displayName: 'Queries', - name: 'queries', - placeholder: 'Add query', - type: 'fixedCollection', - typeOptions: { - multipleValues: true, - }, - displayOptions: { - show: { - action: [ - 'find', - ], - }, - }, - description: 'Queries ', - default: {}, - options: [ - { - name: 'query', - displayName: 'Query', - values: [ - { - displayName: 'Fields', - name: 'fields', - placeholder: 'Add field', - type: 'fixedCollection', - default: {}, - typeOptions: { - multipleValues: true, - }, - options: [{ - name: 'field', - displayName: 'Field', - values: [ - { - displayName: 'Field', - name: 'name', - type: 'options', - default: '', - typeOptions: { - loadOptionsMethod: 'getFields', - }, - options: [], - description: 'Search Field', - }, - { - displayName: 'Value', - name: 'value', - type: 'string', - default: '', - description: 'Value to search', - }, - ] - } - ], - description: 'Field Name', - }, - { - displayName: 'Omit', - name: 'omit', - type: 'boolean', - default: false - }, - ] - }, - ], - }, - { - displayName: 'Sort data ?', - name: 'setSort', - type: 'boolean', - default: false, - description: 'Should we sort data ?', - displayOptions: { - show: { - action: [ - 'find', - 'record', - 'records', - ], - }, - }, - }, - { - displayName: 'Sort', - name: 'sortParametersUi', - placeholder: 'Add Sort Rules', - type: 'fixedCollection', - typeOptions: { - multipleValues: true, - }, - displayOptions: { - show: { - setSort: [ - true, - ], - action: [ - 'find', - 'records', - ], - }, - }, - description: 'Sort rules', - default: {}, - options: [ - { - name: 'rules', - displayName: 'Rules', - values: [ - { - displayName: 'Field', - name: 'name', - type: 'options', - default: '', - typeOptions: { - loadOptionsMethod: 'getFields', - }, - options: [], - description: 'Field Name.', - }, - { - displayName: 'Order', - name: 'value', - type: 'options', - default: 'ascend', - options: [ - { - name: 'Ascend', - value: 'ascend' - }, - { - name: 'Descend', - value: 'descend' - }, - ], - description: 'Sort order.', - }, - ] - }, - ], - }, - { - displayName: 'Before find script', - name: 'setScriptBefore', - type: 'boolean', - default: false, - description: 'Define a script to be run before the action specified by the API call and after the subsequent sort.', - displayOptions: { - show: { - action: [ - 'find', - 'record', - 'records', - ], - } - }, - }, - { - displayName: 'Script Name', - name: 'scriptBefore', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getScripts', - }, - options: [], - default: '', - required: true, - displayOptions: { - show: { - action: [ - 'find', - 'record', - 'records', - ], - setScriptBefore: [ - true - ], - }, - }, - placeholder: 'Script Name', - description: 'The name of the FileMaker script to be run after the action specified by the API call and after the subsequent sort.', - }, - { - displayName: 'Script Parameter', - name: 'scriptBeforeParam', - type: 'string', - default: '', - required: false, - displayOptions: { - show: { - action: [ - 'find', - 'record', - 'records', - ], - setScriptBefore: [ - true - ], - }, - }, - placeholder: 'Script Parameters', - description: 'A parameter for the FileMaker script.', - }, - { - displayName: 'Before sort script', - name: 'setScriptSort', - type: 'boolean', - default: false, - description: 'Define a script to be run after the action specified by the API call but before the subsequent sort.', - displayOptions: { - show: { - action: [ - 'find', - 'record', - 'records', - ], - } - }, - }, - { - displayName: 'Script Name', - name: 'scriptSort', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getScripts', - }, - options: [], - default: '', - required: true, - displayOptions: { - show: { - action: [ - 'find', - 'record', - 'records', - ], - setScriptSort: [ - true - ], - }, - }, - placeholder: 'Script Name', - description: 'The name of the FileMaker script to be run after the action specified by the API call but before the subsequent sort.', - }, - { - displayName: 'Script Parameter', - name: 'scriptSortParam', - type: 'string', - default: '', - required: false, - displayOptions: { - show: { - action: [ - 'find', - 'record', - 'records', - ], - setScriptSort: [ - true - ], - }, - }, - placeholder: 'Script Parameters', - description: 'A parameter for the FileMaker script.', - }, - { - displayName: 'After sort script', - name: 'setScriptAfter', - type: 'boolean', - default: false, - description: 'Define a script to be run after the action specified by the API call but before the subsequent sort.', - displayOptions: { - show: { - action: [ - 'find', - 'record', - 'records', - ], - } - }, - }, - { - displayName: 'Script Name', - name: 'scriptAfter', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getScripts', - }, - options: [], - default: '', - required: true, - displayOptions: { - show: { - action: [ - 'find', - 'record', - 'records', - ], - setScriptAfter: [ - true - ], - }, - }, - placeholder: 'Script Name', - description: 'The name of the FileMaker script to be run after the action specified by the API call and after the subsequent sort.', - }, - { - displayName: 'Script Parameter', - name: 'scriptAfterParam', - type: 'string', - default: '', - required: false, - displayOptions: { - show: { - action: [ - 'find', - 'record', - 'records', - ], - setScriptAfter: [ - true - ], - }, - }, - placeholder: 'Script Parameters', - description: 'A parameter for the FileMaker script.', - }, - // ---------------------------------- - // create/edit - // ---------------------------------- - /*{ + // ---------------------------------- + // shared + // ---------------------------------- + { + displayName: 'Layout', + name: 'layout', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getLayouts', + }, + options: [], + default: '', + required: true, + displayOptions: {}, + placeholder: 'Layout Name', + description: 'FileMaker Layout Name.', + }, + { + displayName: 'Record Id', + name: 'recid', + type: 'number', + default: '', + required: true, + displayOptions: { + show: { + action: [ + 'record', + 'edit', + 'delete', + 'duplicate', + ], + }, + }, + placeholder: 'Record ID', + description: 'Internal Record ID returned by get (recordid)', + }, + { + displayName: 'offset', + name: 'offset', + placeholder: '0', + description: 'The record number of the first record in the range of records.', + type: 'number', + default: '1', + displayOptions: { + show: { + action: [ + 'find', + 'records', + ], + }, + } + }, + { + displayName: 'limit', + name: 'limit', + placeholder: '100', + description: 'The maximum number of records that should be returned. If not specified, the default value is 100.', + type: 'number', + default: '100', + displayOptions: { + show: { + action: [ + 'find', + 'records', + ], + }, + } + }, + { + displayName: 'Get portals', + name: 'getPortals', + type: 'boolean', + default: false, + description: 'Should we get portal data as well ?', + displayOptions: { + show: { + action: [ + 'record', + 'records', + 'find', + ], + }, + }, + }, + { + displayName: 'Portals', + name: 'portals', + type: 'options', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add portal', + loadOptionsMethod: 'getPortals', + }, + options: [], + default: [], + displayOptions: { + show: { + action: [ + 'record', + 'records', + 'find', + ], + getPortals: [ + true, + ], + }, + }, + placeholder: 'Portals', + description: 'The portal result set to return. Use the portal object name or portal table name. If this parameter is omitted, the API will return all portal objects and records in the layout. For best performance, pass the portal object name or portal table name.', + }, + // ---------------------------------- + // find/records + // ---------------------------------- + { + displayName: 'Response Layout', + name: 'responseLayout', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getResponseLayouts', + }, + options: [], + default: '', + required: false, + displayOptions: { + show: { + action: [ + 'find' + ], + }, + }, + }, + { + displayName: 'Queries', + name: 'queries', + placeholder: 'Add query', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + action: [ + 'find', + ], + }, + }, + description: 'Queries ', + default: {}, + options: [ + { + name: 'query', + displayName: 'Query', + values: [ + { + displayName: 'Fields', + name: 'fields', + placeholder: 'Add field', + type: 'fixedCollection', + default: {}, + typeOptions: { + multipleValues: true, + }, + options: [{ + name: 'field', + displayName: 'Field', + values: [ + { + displayName: 'Field', + name: 'name', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getFields', + }, + options: [], + description: 'Search Field', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value to search', + }, + ] + } + ], + description: 'Field Name', + }, + { + displayName: 'Omit', + name: 'omit', + type: 'boolean', + default: false + }, + ] + }, + ], + }, + { + displayName: 'Sort data ?', + name: 'setSort', + type: 'boolean', + default: false, + description: 'Should we sort data ?', + displayOptions: { + show: { + action: [ + 'find', + 'record', + 'records', + ], + }, + }, + }, + { + displayName: 'Sort', + name: 'sortParametersUi', + placeholder: 'Add Sort Rules', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + setSort: [ + true, + ], + action: [ + 'find', + 'records', + ], + }, + }, + description: 'Sort rules', + default: {}, + options: [ + { + name: 'rules', + displayName: 'Rules', + values: [ + { + displayName: 'Field', + name: 'name', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getFields', + }, + options: [], + description: 'Field Name.', + }, + { + displayName: 'Order', + name: 'value', + type: 'options', + default: 'ascend', + options: [ + { + name: 'Ascend', + value: 'ascend' + }, + { + name: 'Descend', + value: 'descend' + }, + ], + description: 'Sort order.', + }, + ] + }, + ], + }, + { + displayName: 'Before find script', + name: 'setScriptBefore', + type: 'boolean', + default: false, + description: 'Define a script to be run before the action specified by the API call and after the subsequent sort.', + displayOptions: { + show: { + action: [ + 'find', + 'record', + 'records', + ], + } + }, + }, + { + displayName: 'Script Name', + name: 'scriptBefore', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getScripts', + }, + options: [], + default: '', + required: true, + displayOptions: { + show: { + action: [ + 'find', + 'record', + 'records', + ], + setScriptBefore: [ + true + ], + }, + }, + placeholder: 'Script Name', + description: 'The name of the FileMaker script to be run after the action specified by the API call and after the subsequent sort.', + }, + { + displayName: 'Script Parameter', + name: 'scriptBeforeParam', + type: 'string', + default: '', + required: false, + displayOptions: { + show: { + action: [ + 'find', + 'record', + 'records', + ], + setScriptBefore: [ + true + ], + }, + }, + placeholder: 'Script Parameters', + description: 'A parameter for the FileMaker script.', + }, + { + displayName: 'Before sort script', + name: 'setScriptSort', + type: 'boolean', + default: false, + description: 'Define a script to be run after the action specified by the API call but before the subsequent sort.', + displayOptions: { + show: { + action: [ + 'find', + 'record', + 'records', + ], + } + }, + }, + { + displayName: 'Script Name', + name: 'scriptSort', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getScripts', + }, + options: [], + default: '', + required: true, + displayOptions: { + show: { + action: [ + 'find', + 'record', + 'records', + ], + setScriptSort: [ + true + ], + }, + }, + placeholder: 'Script Name', + description: 'The name of the FileMaker script to be run after the action specified by the API call but before the subsequent sort.', + }, + { + displayName: 'Script Parameter', + name: 'scriptSortParam', + type: 'string', + default: '', + required: false, + displayOptions: { + show: { + action: [ + 'find', + 'record', + 'records', + ], + setScriptSort: [ + true + ], + }, + }, + placeholder: 'Script Parameters', + description: 'A parameter for the FileMaker script.', + }, + { + displayName: 'After sort script', + name: 'setScriptAfter', + type: 'boolean', + default: false, + description: 'Define a script to be run after the action specified by the API call but before the subsequent sort.', + displayOptions: { + show: { + action: [ + 'find', + 'record', + 'records', + ], + } + }, + }, + { + displayName: 'Script Name', + name: 'scriptAfter', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getScripts', + }, + options: [], + default: '', + required: true, + displayOptions: { + show: { + action: [ + 'find', + 'record', + 'records', + ], + setScriptAfter: [ + true + ], + }, + }, + placeholder: 'Script Name', + description: 'The name of the FileMaker script to be run after the action specified by the API call and after the subsequent sort.', + }, + { + displayName: 'Script Parameter', + name: 'scriptAfterParam', + type: 'string', + default: '', + required: false, + displayOptions: { + show: { + action: [ + 'find', + 'record', + 'records', + ], + setScriptAfter: [ + true + ], + }, + }, + placeholder: 'Script Parameters', + description: 'A parameter for the FileMaker script.', + }, + // ---------------------------------- + // create/edit + // ---------------------------------- + /*{ displayName: 'fieldData', name: 'fieldData', placeholder: '{"field1": "value", "field2": "value", ...}', @@ -573,345 +572,345 @@ export class FileMaker implements INodeType { }, } },*/ - { - displayName: 'Mod Id', - name: 'modId', - description: 'The last modification ID. When you use modId, a record is edited only when the modId matches.', - type: 'number', - default: '', - displayOptions: { - show: { - action: [ - 'edit', - ], - }, - } - }, - { - displayName: 'Fields', - name: 'fieldsParametersUi', - placeholder: 'Add field', - type: 'fixedCollection', - typeOptions: { - multipleValues: true, - }, - displayOptions: { - show: { - action: [ - 'create', - 'edit', - ], - }, - }, - description: 'Fields to define', - default: {}, - options: [ - { - name: 'fields', - displayName: 'Fields', - values: [ - { - displayName: 'Field', - name: 'name', - type: 'options', - default: '', - typeOptions: { - loadOptionsMethod: 'getFields', - }, - options: [], - description: 'Field Name.', - }, - { - displayName: 'Value', - name: 'value', - type: 'string', - default: '', - }, - ] - }, - ], - }, - // ---------------------------------- - // performscript - // ---------------------------------- - { - displayName: 'Script Name', - name: 'script', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getScripts', - }, - options: [], - default: '', - required: true, - displayOptions: { - show: { - action: [ - 'performscript' - ], - }, - }, - placeholder: 'Script Name', - description: 'The name of the FileMaker script to be run.', - }, - { - displayName: 'Script Parameter', - name: 'scriptParam', - type: 'string', - default: '', - required: false, - displayOptions: { - show: { - action: [ - 'performscript' - ], - }, - }, - placeholder: 'Script Parameters', - description: 'A parameter for the FileMaker script.', - }, - ] - }; + { + displayName: 'Mod Id', + name: 'modId', + description: 'The last modification ID. When you use modId, a record is edited only when the modId matches.', + type: 'number', + default: '', + displayOptions: { + show: { + action: [ + 'edit', + ], + }, + } + }, + { + displayName: 'Fields', + name: 'fieldsParametersUi', + placeholder: 'Add field', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + action: [ + 'create', + 'edit', + ], + }, + }, + description: 'Fields to define', + default: {}, + options: [ + { + name: 'fields', + displayName: 'Fields', + values: [ + { + displayName: 'Field', + name: 'name', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getFields', + }, + options: [], + description: 'Field Name.', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + }, + ] + }, + ], + }, + // ---------------------------------- + // performscript + // ---------------------------------- + { + displayName: 'Script Name', + name: 'script', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getScripts', + }, + options: [], + default: '', + required: true, + displayOptions: { + show: { + action: [ + 'performscript' + ], + }, + }, + placeholder: 'Script Name', + description: 'The name of the FileMaker script to be run.', + }, + { + displayName: 'Script Parameter', + name: 'scriptParam', + type: 'string', + default: '', + required: false, + displayOptions: { + show: { + action: [ + 'performscript' + ], + }, + }, + placeholder: 'Script Parameters', + description: 'A parameter for the FileMaker script.', + }, + ] + }; - methods = { - loadOptions: { - // Get all the available topics to display them to user so that he can - // select them easily - async getLayouts(this: ILoadOptionsFunctions): Promise { - const returnData: INodePropertyOptions[] = []; + methods = { + loadOptions: { + // Get all the available topics to display them to user so that he can + // select them easily + async getLayouts(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; - let layouts; - try { - layouts = await layoutsApiRequest.call(this); - } catch (err) { - throw new Error(`FileMaker Error: ${err}`); - } - for (const layout of layouts) { - returnData.push({ - name: layout.name, - value: layout.name, - }); - } - return returnData; - }, - async getResponseLayouts(this: ILoadOptionsFunctions): Promise { - const returnData: INodePropertyOptions[] = []; - returnData.push({ - name: 'Use main layout', - value: '', - }); + let layouts; + try { + layouts = await layoutsApiRequest.call(this); + } catch (err) { + throw new Error(`FileMaker Error: ${err}`); + } + for (const layout of layouts) { + returnData.push({ + name: layout.name, + value: layout.name, + }); + } + return returnData; + }, + async getResponseLayouts(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + returnData.push({ + name: 'Use main layout', + value: '', + }); - let layouts; - try { - layouts = await layoutsApiRequest.call(this); - } catch (err) { - throw new Error(`FileMaker Error: ${err}`); - } - for (const layout of layouts) { - returnData.push({ - name: layout.name, - value: layout.name, - }); - } - return returnData; - }, + let layouts; + try { + layouts = await layoutsApiRequest.call(this); + } catch (err) { + throw new Error(`FileMaker Error: ${err}`); + } + for (const layout of layouts) { + returnData.push({ + name: layout.name, + value: layout.name, + }); + } + return returnData; + }, - async getFields(this: ILoadOptionsFunctions): Promise { - const returnData: INodePropertyOptions[] = []; + async getFields(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; - let fields; - try { - fields = await getFields.call(this); - } catch (err) { - throw new Error(`FileMaker Error: ${err}`); - } - for (const field of fields) { - returnData.push({ - name: field.name, - value: field.name, - }); - } - return returnData; - }, + let fields; + try { + fields = await getFields.call(this); + } catch (err) { + throw new Error(`FileMaker Error: ${err}`); + } + for (const field of fields) { + returnData.push({ + name: field.name, + value: field.name, + }); + } + return returnData; + }, - async getScripts(this: ILoadOptionsFunctions): Promise { - const returnData: INodePropertyOptions[] = []; + async getScripts(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; - let scripts; - try { - scripts = await getScripts.call(this); - } catch (err) { - throw new Error(`FileMaker Error: ${err}`); - } - for (const script of scripts) { - if (!script.isFolder) { - returnData.push({ - name: script.name, - value: script.name, - }); - } - } - return returnData; - }, + let scripts; + try { + scripts = await getScripts.call(this); + } catch (err) { + throw new Error(`FileMaker Error: ${err}`); + } + for (const script of scripts) { + if (!script.isFolder) { + returnData.push({ + name: script.name, + value: script.name, + }); + } + } + return returnData; + }, - async getPortals(this: ILoadOptionsFunctions): Promise { - const returnData: INodePropertyOptions[] = []; + async getPortals(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; - let portals; - try { - portals = await getPortals.call(this); - } catch (err) { - throw new Error(`FileMaker Error: ${err}`); - } - Object.keys(portals).forEach((portal) => { - returnData.push({ - name: portal, - value: portal, - }); - }); + let portals; + try { + portals = await getPortals.call(this); + } catch (err) { + throw new Error(`FileMaker Error: ${err}`); + } + Object.keys(portals).forEach((portal) => { + returnData.push({ + name: portal, + value: portal, + }); + }); - return returnData; - }, - }, - }; + return returnData; + }, + }, + }; - async execute(this: IExecuteFunctions): Promise { - const items = this.getInputData(); - const returnData: INodeExecutionData[] = []; + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: INodeExecutionData[] = []; - const credentials = this.getCredentials('FileMaker'); + const credentials = this.getCredentials('FileMaker'); - if (credentials === undefined) { - throw new Error('No credentials got returned!'); - } + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } - let token; - try { - token = await getToken.call(this); - } catch (e) { - throw new Error(`Login fail: ${e}`); - } + let token; + try { + token = await getToken.call(this); + } catch (e) { + throw new Error(`Login fail: ${e}`); + } - let requestOptions: OptionsWithUri; + let requestOptions: OptionsWithUri; - const host = credentials.host as string; - const database = credentials.db as string; + const host = credentials.host as string; + const database = credentials.db as string; - const url = `https://${host}/fmi/data/v1`; + const url = `https://${host}/fmi/data/v1`; - const action = this.getNodeParameter('action', 0) as string; + const action = this.getNodeParameter('action', 0) as string; - for (let i = 0; i < items.length; i++) { - // Reset all values - requestOptions = { - uri: '', - headers: { - 'Authorization': `Bearer ${token}`, - }, - method: 'GET', - json: true - }; + for (let i = 0; i < items.length; i++) { + // Reset all values + requestOptions = { + uri: '', + headers: { + 'Authorization': `Bearer ${token}`, + }, + method: 'GET', + json: true + }; - const layout = this.getNodeParameter('layout', 0) as string; + const layout = this.getNodeParameter('layout', 0) as string; - if (action === 'record') { - const recid = this.getNodeParameter('recid', 0) as string; - requestOptions.uri = url + `/databases/${database}/layouts/${layout}/records/${recid}`; - requestOptions.qs = { - 'portal': JSON.stringify(parsePortals.call(this)), - ...parseScripts.call(this) - }; - } else if (action === 'records') { - requestOptions.uri = url + `/databases/${database}/layouts/${layout}/records`; - requestOptions.qs = { - '_offset': this.getNodeParameter('offset', 0), - '_limit': this.getNodeParameter('limit', 0), - 'portal': JSON.stringify(parsePortals.call(this)), - ...parseScripts.call(this) - }; - const sort = parseSort.call(this); - if (sort) { - requestOptions.body.sort = sort; - } - } else if (action === 'find') { - requestOptions.uri = url + `/databases/${database}/layouts/${layout}/_find`; - requestOptions.method = 'POST'; - requestOptions.body = { - 'query': parseQuery.call(this), - 'offset': this.getNodeParameter('offset', 0), - 'limit': this.getNodeParameter('limit', 0), - 'layout.response': this.getNodeParameter('responseLayout', 0), - ...parseScripts.call(this) - }; - const sort = parseSort.call(this); - if (sort) { - requestOptions.body.sort = sort; - } - } else if (action === 'create') { - requestOptions.uri = url + `/databases/${database}/layouts/${layout}/records`; - requestOptions.method = 'POST'; - requestOptions.headers!['Content-Type'] = 'application/json'; - - //TODO: handle portalData - requestOptions.body = { - fieldData: {...parseFields.call(this)}, - portalData: {}, - ...parseScripts.call(this) - }; - } else if (action === 'edit') { - const recid = this.getNodeParameter('recid', 0) as string; - requestOptions.uri = url + `/databases/${database}/layouts/${layout}/records/${recid}`; - requestOptions.method = 'PATCH'; - requestOptions.headers!['Content-Type'] = 'application/json'; - - //TODO: handle portalData - requestOptions.body = { - fieldData: {...parseFields.call(this)}, - portalData: {}, - ...parseScripts.call(this) - }; - } else if (action === 'performscript') { - const scriptName = this.getNodeParameter('script', 0) as string; - requestOptions.uri = url + `/databases/${database}/layouts/${layout}/script/${scriptName}`; - requestOptions.qs = { - 'script.param': this.getNodeParameter('scriptParam', 0), - }; - } else if (action === 'duplicate') { - const recid = this.getNodeParameter('recid', 0) as string; - requestOptions.uri = url + `/databases/${database}/layouts/${layout}/records/${recid}`; - requestOptions.method = 'POST'; - requestOptions.headers!['Content-Type'] = 'application/json'; - requestOptions.qs = { - ...parseScripts.call(this) - }; - } else if (action === 'delete') { - const recid = this.getNodeParameter('recid', 0) as string; - requestOptions.uri = url + `/databases/${database}/layouts/${layout}/records/${recid}`; - requestOptions.method = 'DELETE'; + if (action === 'record') { + const recid = this.getNodeParameter('recid', 0) as string; + requestOptions.uri = url + `/databases/${database}/layouts/${layout}/records/${recid}`; requestOptions.qs = { - ...parseScripts.call(this) - }; - } else { - throw new Error(`The action "${action}" is not implemented yet!`); - } + 'portal': JSON.stringify(parsePortals.call(this)), + ...parseScripts.call(this) + }; + } else if (action === 'records') { + requestOptions.uri = url + `/databases/${database}/layouts/${layout}/records`; + requestOptions.qs = { + '_offset': this.getNodeParameter('offset', 0), + '_limit': this.getNodeParameter('limit', 0), + 'portal': JSON.stringify(parsePortals.call(this)), + ...parseScripts.call(this) + }; + const sort = parseSort.call(this); + if (sort) { + requestOptions.body.sort = sort; + } + } else if (action === 'find') { + requestOptions.uri = url + `/databases/${database}/layouts/${layout}/_find`; + requestOptions.method = 'POST'; + requestOptions.body = { + 'query': parseQuery.call(this), + 'offset': this.getNodeParameter('offset', 0), + 'limit': this.getNodeParameter('limit', 0), + 'layout.response': this.getNodeParameter('responseLayout', 0), + ...parseScripts.call(this) + }; + const sort = parseSort.call(this); + if (sort) { + requestOptions.body.sort = sort; + } + } else if (action === 'create') { + requestOptions.uri = url + `/databases/${database}/layouts/${layout}/records`; + requestOptions.method = 'POST'; + requestOptions.headers!['Content-Type'] = 'application/json'; - // Now that the options are all set make the actual http request - let response; - try { - response = await this.helpers.request(requestOptions); - } catch (error) { - response = error.response.body; - } + //TODO: handle portalData + requestOptions.body = { + fieldData: {...parseFields.call(this)}, + portalData: {}, + ...parseScripts.call(this) + }; + } else if (action === 'edit') { + const recid = this.getNodeParameter('recid', 0) as string; + requestOptions.uri = url + `/databases/${database}/layouts/${layout}/records/${recid}`; + requestOptions.method = 'PATCH'; + requestOptions.headers!['Content-Type'] = 'application/json'; - if (typeof response === 'string') { - throw new Error('Response body is not valid JSON. Change "Response Format" to "String"'); - } - await logout.call(this, token); + //TODO: handle portalData + requestOptions.body = { + fieldData: {...parseFields.call(this)}, + portalData: {}, + ...parseScripts.call(this) + }; + } else if (action === 'performscript') { + const scriptName = this.getNodeParameter('script', 0) as string; + requestOptions.uri = url + `/databases/${database}/layouts/${layout}/script/${scriptName}`; + requestOptions.qs = { + 'script.param': this.getNodeParameter('scriptParam', 0), + }; + } else if (action === 'duplicate') { + const recid = this.getNodeParameter('recid', 0) as string; + requestOptions.uri = url + `/databases/${database}/layouts/${layout}/records/${recid}`; + requestOptions.method = 'POST'; + requestOptions.headers!['Content-Type'] = 'application/json'; + requestOptions.qs = { + ...parseScripts.call(this) + }; + } else if (action === 'delete') { + const recid = this.getNodeParameter('recid', 0) as string; + requestOptions.uri = url + `/databases/${database}/layouts/${layout}/records/${recid}`; + requestOptions.method = 'DELETE'; + requestOptions.qs = { + ...parseScripts.call(this) + }; + } else { + throw new Error(`The action "${action}" is not implemented yet!`); + } - returnData.push({json: response}); - } + // Now that the options are all set make the actual http request + let response; + try { + response = await this.helpers.request(requestOptions); + } catch (error) { + response = error.response.body; + } - return this.prepareOutputData(returnData); - } + if (typeof response === 'string') { + throw new Error('Response body is not valid JSON. Change "Response Format" to "String"'); + } + await logout.call(this, token); + + returnData.push({json: response}); + } + + return this.prepareOutputData(returnData); + } }