n8n/packages/nodes-base/nodes/FileMaker/FileMaker.node.ts
Iván Ovejero 1d27a9e87e
Improve node error handling (#1309)
* Add path mapping and response error interfaces

* Add error handling and throwing functionality

* Refactor error handling into a single function

* Re-implement error handling in Hacker News node

* Fix linting details

* Re-implement error handling in Spotify node

* Re-implement error handling in G Suite Admin node

* 🚧 create basic setup NodeError

* 🚧 add httpCodes

* 🚧 add path priolist

* 🚧 handle statusCode in error, adjust interfaces

* 🚧 fixing type issues w/Ivan

* 🚧 add error exploration

* 👔 fix linter issues

* 🔧 improve object check

* 🚧 remove path passing from NodeApiError

* 🚧 add multi error + refactor findProperty method

* 👔 allow any

* 🔧 handle multi error message callback

*  change return type of callback

*  add customCallback to MultiError

* 🚧 refactor to use INode

* 🔨 handle arrays, continue search after first null property found

* 🚫 refactor method access

* 🚧 setup NodeErrorView

*  change timestamp to Date.now

* 📚 Add documentation for methods and constants

* 🚧 change message setting

* 🚚 move NodeErrors to workflow

*  add new ErrorView for Nodes

* 🎨 improve error notification

* 🎨 refactor interfaces

*  add WorkflowOperationError, refactor error throwing

* 👕 fix linter issues

* 🎨 rename param

* 🐛 fix handling normal errors

*  add usage of NodeApiError

* 🎨 fix throw new error instead of constructor

* 🎨 remove unnecessary code/comments

* 🎨 adjusted spacing + updated status messages

* 🎨 fix tab indentation

*  Replace current errors with custom errors (#1576)

*  Introduce NodeApiError in catch blocks

*  Introduce NodeOperationError in nodes

*  Add missing errors and remove incompatible

*  Fix NodeOperationError in incompatible nodes

* 🔧 Adjust error handling in missed nodes

PayPal, FileMaker, Reddit, Taiga and Facebook Graph API nodes

* 🔨 Adjust Strava Trigger node error handling

* 🔨 Adjust AWS nodes error handling

* 🔨 Remove duplicate instantiation of NodeApiError

* 🐛 fix strava trigger node error handling

* Add XML parsing to NodeApiError constructor (#1633)

* 🐛 Remove type annotation from catch variable

*  Add XML parsing to NodeApiError

*  Simplify error handling in Rekognition node

*  Pass in XML flag in generic functions

* 🔥 Remove try/catch wrappers at call sites

* 🔨 Refactor setting description from XML

* 🔨 Refactor let to const in resource loaders

*  Find property in parsed XML

*  Change let to const

* 🔥 Remove unneeded try/catch block

* 👕 Fix linting issues

* 🐛 Fix errors from merge conflict resolution

*  Add custom errors to latest contributions

* 👕 Fix linting issues

*  Refactor MongoDB helpers for custom errors

* 🐛 Correct custom error type

*  Apply feedback to A nodes

*  Apply feedback to missed A node

*  Apply feedback to B-D nodes

*  Apply feedback to E-F nodes

*  Apply feedback to G nodes

*  Apply feedback to H-L nodes

*  Apply feedback to M nodes

*  Apply feedback to P nodes

*  Apply feedback to R nodes

*  Apply feedback to S nodes

*  Apply feedback to T nodes

*  Apply feedback to V-Z nodes

*  Add HTTP code to iterable node error

* 🔨 Standardize e as error

* 🔨 Standardize err as error

*  Fix error handling for non-standard nodes

Co-authored-by: Ben Hesseldieck <b.hesseldieck@gmail.com>

Co-authored-by: Ben Hesseldieck <b.hesseldieck@gmail.com>
Co-authored-by: Ben Hesseldieck <1849459+BHesseldieck@users.noreply.github.com>
2021-04-16 18:33:36 +02: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 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 (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 = 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);
}
}