import {IExecuteFunctions} from 'n8n-core'; import { ILoadOptionsFunctions, INodeExecutionData, INodePropertyOptions, INodeType, INodeTypeDescription, } from 'n8n-workflow'; import {OptionsWithUri} from 'request'; import { 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: [ /*{ 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', }, ], 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<br />table name. If this parameter is omitted, the API will return all<br />portal objects and records in the layout. For best performance<br />, 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", ...}', description: 'Additional fields to add.', type: 'string', default: '{}', displayOptions: { show: { action: [ 'create', 'edit', ], }, } },*/ { 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<INodePropertyOptions[]> { let returnData: INodePropertyOptions[]; try { returnData = await layoutsApiRequest.call(this); } catch (err) { throw new Error(`FileMaker Error: ${err}`); } return returnData; }, async getResponseLayouts(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> { 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 getFields(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> { 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 getScripts(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> { 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<INodePropertyOptions[]> { 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; }, }, }; async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { const items = this.getInputData(); const returnData: INodeExecutionData[] = []; const credentials = this.getCredentials('fileMaker'); 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 requestOptions: OptionsWithUri; const host = credentials.host as string; const database = credentials.db as string; const url = `https://${host}/fmi/data/v1`; const action = this.getNodeParameter('action', 0) as string; try { 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', i) as string; if (action === 'record') { const recid = this.getNodeParameter('recid', i) as string; requestOptions.uri = url + `/databases/${database}/layouts/${layout}/records/${recid}`; requestOptions.qs = { 'portal': JSON.stringify(parsePortals.call(this, i)), ...parseScripts.call(this, i) }; } else if (action === 'records') { requestOptions.uri = url + `/databases/${database}/layouts/${layout}/records`; requestOptions.qs = { '_offset': this.getNodeParameter('offset', i), '_limit': this.getNodeParameter('limit', i), 'portal': JSON.stringify(parsePortals.call(this, i)), ...parseScripts.call(this, i) }; const sort = parseSort.call(this, i); 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, i), 'offset': this.getNodeParameter('offset', i), 'limit': this.getNodeParameter('limit', i), 'layout.response': this.getNodeParameter('responseLayout', i), ...parseScripts.call(this, i) }; const sort = parseSort.call(this, i); 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, i)}, portalData: {}, ...parseScripts.call(this, i) }; } else if (action === 'edit') { const recid = this.getNodeParameter('recid', i) 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, i)}, portalData: {}, ...parseScripts.call(this, i) }; } else if (action === 'performscript') { const scriptName = this.getNodeParameter('script', i) as string; requestOptions.uri = url + `/databases/${database}/layouts/${layout}/script/${scriptName}`; requestOptions.qs = { 'script.param': this.getNodeParameter('scriptParam', i), }; } else if (action === 'duplicate') { const recid = this.getNodeParameter('recid', i) 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, i) }; } else if (action === 'delete') { const recid = this.getNodeParameter('recid', i) as string; requestOptions.uri = url + `/databases/${database}/layouts/${layout}/records/${recid}`; requestOptions.method = 'DELETE'; requestOptions.qs = { ...parseScripts.call(this, i) }; } 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"'); } returnData.push({json: response}); } } catch (error) { await logout.call(this, token); throw new Error(`The action "${error.message}" is not implemented yet!`); } return this.prepareOutputData(returnData); } }