n8n/packages/nodes-base/nodes/FileMaker/FileMaker.node.ts
Mutasem Aldmour ce066a160f
Remove unnessasry <br/> (#2340)
* introduce analytics

* add user survey backend

* add user survey backend

* set answers on survey submit

Co-authored-by: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com>

* change name to personalization

* lint

Co-authored-by: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com>

* N8n 2495 add personalization modal (#2280)

* update modals

* add onboarding modal

* implement questions

* introduce analytics

* simplify impl

* implement survey handling

* add personalized cateogry

* update modal behavior

* add thank you view

* handle empty cases

* rename modal

* standarize modal names

* update image, add tags to headings

* remove unused file

* remove unused interfaces

* clean up footer spacing

* introduce analytics

* refactor to fix bug

* update endpoint

* set min height

* update stories

* update naming from questions to survey

* remove spacing after core categories

* fix bug in logic

* sort nodes

* rename types

* merge with be

* rename userSurvey

* clean up rest api

* use constants for keys

* use survey keys

* clean up types

* move personalization to its own file

Co-authored-by: ahsan-virani <ahsan.virani@gmail.com>

* update parameter inputs to be multiline

* update spacing

* Survey new options (#2300)

* split up options

* fix quotes

* remove unused import

* refactor node credentials

* add user created workflow event (#2301)

* update multi params

* simplify env vars

* fix versionCli on FE

* update personalization env

* clean up node detail settings

* fix event User opened Credentials panel

* fix font sizes across modals

* clean up input spacing

* fix select modal spacing

* increase spacing

* fix input copy

* fix webhook, tab spacing, retry button

* fix button sizes

* fix button size

* add mini xlarge sizes

* fix webhook spacing

* fix nodes panel event

* fix workflow id in workflow execute event

* improve telemetry error logging

* fix config and stop process events

* add flush call on n8n stop

* ready for release

* fix input error highlighting

* revert change

* update toggle spacing

* fix delete positioning

* keep tooltip while focused

* set strict size

* increase left spacing

* fix sort icons

* remove unnessasry <br/>

* remove unnessary break

* remove unnessary margin

* clean unused functionality

* remove unnessary css

* remove duplicate tracking

* only show tooltip when hovering over label

* remove extra space

* add br

* remove extra space

* clean up commas

* clean up commas

* remove extra space

* remove extra space

* rewrite desc

* add commas

* add space

* remove extra space

* add space

* add dot

* update credentials section

* use includes

Co-authored-by: ahsan-virani <ahsan.virani@gmail.com>
Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
2021-10-27 15:00:13 -05:00

919 lines
21 KiB
TypeScript

import {IExecuteFunctions} from 'n8n-core';
import {
ILoadOptionsFunctions,
INodeExecutionData, INodePropertyOptions,
INodeType,
INodeTypeDescription,
NodeOperationError,
} from 'n8n-workflow';
import {OptionsWithUri} from 'request';
import {
getFields,
getPortals,
getScripts,
getToken,
layoutsApiRequest,
logout,
parseFields,
parsePortals,
parseQuery,
parseScripts,
parseSort,
} 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 the 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 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", ...}',
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 (error) {
throw new NodeOperationError(this.getNode(), `FileMaker Error: ${error}`);
}
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 (error) {
throw new NodeOperationError(this.getNode(), `FileMaker Error: ${error}`);
}
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 (error) {
throw new NodeOperationError(this.getNode(), `FileMaker Error: ${error}`);
}
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 (error) {
throw new NodeOperationError(this.getNode(), `FileMaker Error: ${error}`);
}
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 (error) {
throw new NodeOperationError(this.getNode(), `FileMaker Error: ${error}`);
}
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 = await this.getCredentials('fileMaker');
if (credentials === undefined) {
throw new NodeOperationError(this.getNode(), 'No credentials got returned!');
}
let token;
try {
token = await getToken.call(this);
} catch (error) {
throw new NodeOperationError(this.getNode(), `Login fail: ${error}`);
}
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 NodeOperationError(this.getNode(), `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 NodeOperationError(this.getNode(), 'Response body is not valid JSON. Change "Response Format" to "String"');
}
returnData.push({json: response});
}
} catch (error) {
await logout.call(this, token);
if (error.node) {
throw error;
}
throw new NodeOperationError(this.getNode(), `The action "${error.message}" is not implemented yet!`);
}
return this.prepareOutputData(returnData);
}
}