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 0000000000..ec691433da Binary files /dev/null and b/packages/nodes-base/nodes/FileMaker/filemaker.png differ diff --git a/packages/nodes-base/nodes/HttpRequest.node.ts b/packages/nodes-base/nodes/HttpRequest.node.ts index 0f6ab7499b..3795c2772f 100644 --- a/packages/nodes-base/nodes/HttpRequest.node.ts +++ b/packages/nodes-base/nodes/HttpRequest.node.ts @@ -360,7 +360,6 @@ export class HttpRequest implements INodeType { }, ], }, - // Body Parameter { displayName: 'Body Parameters', diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 0343726121..326a6439c7 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -34,6 +34,7 @@ "dist/credentials/ChargebeeApi.credentials.js", "dist/credentials/DropboxApi.credentials.js", "dist/credentials/FreshdeskApi.credentials.js", + "dist/credentials/FileMaker.credentials.js", "dist/credentials/GithubApi.credentials.js", "dist/credentials/GitlabApi.credentials.js", "dist/credentials/GoogleApi.credentials.js", @@ -79,6 +80,7 @@ "dist/nodes/EmailSend.node.js", "dist/nodes/ErrorTrigger.node.js", "dist/nodes/ExecuteCommand.node.js", + "dist/nodes/FileMaker/FileMaker.node.js", "dist/nodes/Freshdesk/Freshdesk.node.js", "dist/nodes/Function.node.js", "dist/nodes/FunctionItem.node.js",