Files
Automata/packages/workflow/src/Expression.ts
Milorad FIlipović 04cfa548af feat(editor): Implement Resource Mapper component (#6207)
*  scaffolding
*  finished scaffolding
*  renamed types
*  updated subtitle
*  renamed functions file, UI updates
*  query parameters fixes, ui updates, refactoring
*  fixes for credentials test, setup for error parsing
*  rlc for schema and table, error handling tweaks
*  delete operation, new options
*  columns loader
*  linter fixes
*  where clauses setup
*  logic for processing where clauses
*  select operation
*  refactoring
*  data mode for insert and update, wip
*  data mapping, insert update, skip on conflict option
*  select columns with spaces fix
*  update operation update, wip
*  finished update operation
*  upsert operation
*  ui fixes
* Copy updates.
* Copy updates.
*  option to convert empty strings to nulls, schema checks
*  UI requested updates
*  ssh setup WIP
*  fixes, ssh WIP
*  ssh fixes, credentials
*  credentials testing update
*  uncaught error fix
*  clean up
*  address in use fix
*  improved error message
*  tests setup
*  unit tests wip
*  config files clean up
*  utils unit tests
*  refactoring
*  setup for testing operations, tests for deleteTable operation
*  executeQuery and insert operations tests
*  select, update, upsert operations tests
*  runQueries tests setup
*  hint to query
* Copy updates.
*  ui fixes
*  clean up
*  error message update
*  ui update
* Minor tweaks to query params decription.
* feat(Google Sheets Node): Implement Resource mapper in Google Sheets node (#5752)
*  Added initial resource mapping support in google sheets node
*  Wired mapping API endpoint with node-specific logic for fetching mapping fields
*  Implementing mapping fields logic for google sheets
*  Updating Google Sheets execute methods to support resource mapper fields
* 🚧 Added initial version of `ResourceLocator` component
* 👌 Added `update` mode to resource mapper modes
* 👌 Addressing PR feedback
* 👌 Removing leftover const reference
* 👕 Fixing lint errors
*  singlton for conections
*  credentials test fix, clean up
* feat(Postgres Node): Add resource mapper to new version of Postgres node (#5814)
*  scaffolding
*  finished scaffolding
*  renamed types
*  updated subtitle
*  renamed functions file, UI updates
*  query parameters fixes, ui updates, refactoring
*  fixes for credentials test, setup for error parsing
*  rlc for schema and table, error handling tweaks
*  delete operation, new options
*  columns loader
*  linter fixes
*  where clauses setup
*  logic for processing where clauses
*  select operation
*  refactoring
*  data mode for insert and update, wip
*  data mapping, insert update, skip on conflict option
*  select columns with spaces fix
*  update operation update, wip
*  finished update operation
*  upsert operation
*  ui fixes
* Copy updates.
* Copy updates.
*  option to convert empty strings to nulls, schema checks
*  UI requested updates
*  ssh setup WIP
*  fixes, ssh WIP
*  ssh fixes, credentials
*  credentials testing update
*  uncaught error fix
*  clean up
*  address in use fix
*  improved error message
*  tests setup
*  unit tests wip
*  config files clean up
*  utils unit tests
*  refactoring
*  setup for testing operations, tests for deleteTable operation
*  executeQuery and insert operations tests
*  select, update, upsert operations tests
*  runQueries tests setup
*  hint to query
* Copy updates.
*  ui fixes
*  clean up
*  error message update
*  ui update
* Minor tweaks to query params decription.
*  Updated Postgres node to use resource mapper component
*  Implemented postgres <-> resource mapper type mapping
*  Updated Postgres node execution to use resource mapper fields in v3
* 🔥 Removing unused import
---------
Co-authored-by: Michael Kret <michael.k@radency.com>
Co-authored-by: Giulio Andreini <g.andreini@gmail.com>

* feat(core): Resource editor componend P0 (#5970)
*  Added inital value of mapping mode dropdown
*  Finished mapping mode selector
*  Finished implementing mapping mode selector
*  Implemented 'Columns to match on' dropdown
*  Implemented `loadOptionsDependOn` support in resource mapper
*  Implemented initial version of mapping fields
*  Implementing dependant fields watcher in new component setup
*  Generating correct resource mapper field types. Added `supportAutoMap` to node specification and UI. Not showing fields with `display=false`. Pre-selecting matching columns if it's the only one
*  Handling matching columns correctly in UI
*  Saving and loading resourceMapper values in component
*  Implemented proper data saving and loading
*  ResourceMapper component refactor, fixing value save/load
*  Refactoring MatchingColumnSelect component. Updating Sheets node to use single key match and Postgres to use multi key
*  Updated Google Sheets node to work with the new UI
*  Updating Postgres Node to work with new UI
*  Additional loading indicator that shown if there is no mapping mode selector
*  Removing hard-coded values, fixing matching columns ordering, refactoring
*  Updating field names in nodes
*  Fixing minor UI issues
*  Implemented matching fields filter logic
*  Moving loading label outside of fields list
*  Added initial unit tests for resource mapper
*  Finished default rendering test
*  Test refactoring
*  Finished unit tests
* 🔨 Updating the way i18n is used in resource mapper components
* ✔️ Fixing value to match on logic for postgres node
*  Hiding mapping fields when auto-map mode is selected
*  Syncing selected mapping mode between components
*  Fixing dateTime input rendering and adding update check to Postgres node
*  Properly handling database connections. Sending null for empty string values.
* 💄 Updated wording in the error message for non-existing rows
*  Fixing issues with selected matching values
* ✔️ Updating unit tests after matching logic update
*  Updating matching columns when new fields are loaded
*  Defaulting to null for empty parameter values
*  Allowing zero as valid value for number imputs
*  Updated list of types that use datepicker as widger
*  Using text inputs for time types
*  Initial mapping field rework
*  Added new component for mapping fields, moved bit of logic from root component to matching selector, fixing some lint errors
*  Added tooltip for columns that cannot be deleted
*  Saving deleted values in parameter value
*  Implemented control to add/remove mapping fields
*  Syncing field list with add field dropdown when changing dependent values
*  Not showing removed fields in matching columns selector. Updating wording in matching columns selector description
*  Implementing disabled states for add/remove all fields options
*  Saving removed columns separately, updating copy
*  Implemented resource mapper values validation
*  Updated validation logic and error input styling
*  Validating resource mapper fields when new nodes are added
*  Using node field words in validation, refactoring resource mapper component
*  Implemented schema syncing and add/remove all fields
*  Implemented custom parameter actions
*  Implemented loading indicator in parameter options
* 🔨 Removing unnecessary constants and vue props
*  Handling default values properly
*  Fixing validation logic
* 👕 Fixing lint errors
*  Fixing type issues
*  Not showing fields by default if `addAllFields` is set to `false`
*  Implemented field type validation in resource mapper
*  Updated casing in copy, removed all/remove all option from bottom menu
*  Added auto mapping mode notice
*  Added support for more types in validation
*  Added support for enumerated values
*  Fixing imports after merging
*  Not showing removed fields in matching columns selector. Refactoring validation logic.
* 👕 Fixing imports
* ✔️ Updating unit tests
*  Added resource mapper schema tests
*  Removing `match` from resource mapper field definition, fixing matching columns loading
*  Fixed schema merging
*  update operation return data fix
*  review
* 🐛 Added missing import
* 💄 Updating parameter actions icon based on the ui review
* 💄 Updating word capitalisation in tooltips
* 💄 Added empty state to mapping fields list
* 💄 Removing asterisk from fields, updating tooltips for matching fields
*  Preventing matching fields from being removed by 'Remove All option'
*  Not showing hidden fields in the `Add field` dropdown
*  Added support for custom matching columns labels
*  query optimization
*  fix
*  Optimizing Postgres node enumeration logic
*  Added empty state for matching columns
*  Only fully loading fields if there is no schema fetched
*  Hiding mapping fields if there is no matching columns available in the schema
* ✔️ Fixing minor issues
*  Implemented runtime type validation
* 🔨 Refactoring validation logic
*  Implemented required check, added more custom messages
*  Skipping boolean type in required check
* Type check improvements
*  Only reloading fields if dependent values actually change
*  Adding item index to validation error title
*  Updating Postgres fetching logic, using resource mapper mode to determine if a field can be deleted
*  Resetting field values when adding them via the addAll option
*  Using minor version (2.2) for new Postgres node
*  Implemented proper date validation and type casting
* 👕 Consolidating typing
*  Added unit tests for type validations
* 👌 Addressing front-end review comments
*  More refactoring to address review changes
*  Updating leftover props
*  Added fallback for ISO dates with invalid timezones
* Added timestamp to datetime test cases
*  Reseting matching columns if operation changes
*  Not forcing auto-increment fields to be filled in in Postgres node. Handling null values
* 💄 Added a custom message for invalid dates
*  Better handling of JSON values
*  Updating codemirror readonly stauts based on component property, handling objects in json validation
* Deleting leftover console.log
*  Better time validation
*  Fixing build error after merging
* 👕 Fixing lint error
*  Updating node configuration values
*  Handling postgres arrays better
*  Handling SQL array syntax
*  Updating time validation rules to include timezone
*  Sending expressions that resolve to `null` or `undefined` by the resource mapper to delete cell content in Google Sheets
*  Allowing removed fields to be selected for match
*  Updated the query for fetching unique columns and primary keys
*  Optimizing the unique query
*  Setting timezone to all parsed dates
*  Addressing PR review feedback
*  Configuring Sheets node for production, minor vue component update
* New cases added to the TypeValidation test.
*  Tweaking validation rules for arrays/objects and updating test cases
---------
Co-authored-by: Michael Kret <michael.k@radency.com>
Co-authored-by: Giulio Andreini <g.andreini@gmail.com>
2023-05-31 11:56:09 +02:00

586 lines
15 KiB
TypeScript

import * as tmpl from '@n8n_io/riot-tmpl';
import { DateTime, Duration, Interval } from 'luxon';
import type {
IExecuteData,
INode,
INodeExecutionData,
INodeParameterResourceLocator,
INodeParameters,
IRunExecutionData,
IWorkflowDataProxyAdditionalKeys,
IWorkflowDataProxyData,
NodeParameterValue,
NodeParameterValueType,
WorkflowExecuteMode,
} from './Interfaces';
import { ExpressionError } from './ExpressionError';
import { WorkflowDataProxy } from './WorkflowDataProxy';
import type { Workflow } from './Workflow';
// eslint-disable-next-line import/no-cycle
import { extend, extendOptional } from './Extensions';
import { extendedFunctions } from './Extensions/ExtendedFunctions';
import { extendSyntax } from './Extensions/ExpressionExtension';
// Set it to use double curly brackets instead of single ones
tmpl.brackets.set('{{ }}');
// Make sure that error get forwarded
tmpl.tmpl.errorHandler = (error: Error) => {
if (error instanceof ExpressionError) {
if (error.context.failExecution) {
throw error;
}
if (typeof process === 'undefined' && error.clientOnly) {
throw error;
}
}
};
export class Expression {
workflow: Workflow;
constructor(workflow: Workflow) {
this.workflow = workflow;
}
static resolveWithoutWorkflow(expression: string) {
return tmpl.tmpl(expression, {});
}
/**
* Converts an object to a string in a way to make it clear that
* the value comes from an object
*
*/
convertObjectValueToString(value: object): string {
const typeName = Array.isArray(value) ? 'Array' : 'Object';
if (value instanceof DateTime && value.invalidReason !== null) {
throw new Error('invalid DateTime');
}
let result = '';
if (value instanceof Date) {
// We don't want to use JSON.stringify for dates since it disregards workflow timezone
result = DateTime.fromJSDate(value, {
zone: this.workflow.settings?.timezone ?? 'default',
}).toISO();
} else {
result = JSON.stringify(value);
}
result = result
.replace(/,"/g, ', "') // spacing for
.replace(/":/g, '": '); // readability
return `[${typeName}: ${result}]`;
}
/**
* Resolves the parameter value. If it is an expression it will execute it and
* return the result. For everything simply the supplied value will be returned.
*
* @param {(IRunExecutionData | null)} runExecutionData
* @param {boolean} [returnObjectAsString=false]
*/
resolveSimpleParameterValue(
parameterValue: NodeParameterValue,
siblingParameters: INodeParameters,
runExecutionData: IRunExecutionData | null,
runIndex: number,
itemIndex: number,
activeNodeName: string,
connectionInputData: INodeExecutionData[],
mode: WorkflowExecuteMode,
timezone: string,
additionalKeys: IWorkflowDataProxyAdditionalKeys,
executeData?: IExecuteData,
returnObjectAsString = false,
selfData = {},
): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] {
// Check if it is an expression
if (typeof parameterValue !== 'string' || parameterValue.charAt(0) !== '=') {
// Is no expression so return value
return parameterValue;
}
// Is an expression
// Remove the equal sign
// eslint-disable-next-line no-param-reassign
parameterValue = parameterValue.substr(1);
// Generate a data proxy which allows to query workflow data
const dataProxy = new WorkflowDataProxy(
this.workflow,
runExecutionData,
runIndex,
itemIndex,
activeNodeName,
connectionInputData,
siblingParameters,
mode,
timezone,
additionalKeys,
executeData,
-1,
selfData,
);
const data = dataProxy.getDataProxy();
// Support only a subset of process properties
data.process =
typeof process !== 'undefined'
? {
arch: process.arch,
env: process.env.N8N_BLOCK_ENV_ACCESS_IN_NODE === 'true' ? {} : process.env,
platform: process.platform,
pid: process.pid,
ppid: process.ppid,
release: process.release,
version: process.pid,
versions: process.versions,
}
: {};
/**
* Denylist
*/
data.document = {};
data.global = {};
data.window = {};
data.Window = {};
data.this = {};
data.globalThis = {};
data.self = {};
// Alerts
data.alert = {};
data.prompt = {};
data.confirm = {};
// Prevent Remote Code Execution
data.eval = {};
data.uneval = {};
data.setTimeout = {};
data.setInterval = {};
data.Function = {};
// Prevent requests
data.fetch = {};
data.XMLHttpRequest = {};
// Prevent control abstraction
data.Promise = {};
data.Generator = {};
data.GeneratorFunction = {};
data.AsyncFunction = {};
data.AsyncGenerator = {};
data.AsyncGeneratorFunction = {};
// Prevent WASM
data.WebAssembly = {};
// Prevent Reflection
data.Reflect = {};
data.Proxy = {};
data.constructor = {};
// Deprecated
data.escape = {};
data.unescape = {};
/**
* Allowlist
*/
// Dates
data.Date = Date;
data.DateTime = DateTime;
data.Interval = Interval;
data.Duration = Duration;
// Objects
data.Object = Object;
// Arrays
data.Array = Array;
data.Int8Array = Int8Array;
data.Uint8Array = Uint8Array;
data.Uint8ClampedArray = Uint8ClampedArray;
data.Int16Array = Int16Array;
data.Uint16Array = Uint16Array;
data.Int32Array = Int32Array;
data.Uint32Array = Uint32Array;
data.Float32Array = Float32Array;
data.Float64Array = Float64Array;
data.BigInt64Array = typeof BigInt64Array !== 'undefined' ? BigInt64Array : {};
data.BigUint64Array = typeof BigUint64Array !== 'undefined' ? BigUint64Array : {};
// Collections
data.Map = typeof Map !== 'undefined' ? Map : {};
data.WeakMap = typeof WeakMap !== 'undefined' ? WeakMap : {};
data.Set = typeof Set !== 'undefined' ? Set : {};
data.WeakSet = typeof WeakSet !== 'undefined' ? WeakSet : {};
// Errors
data.Error = Error;
data.TypeError = TypeError;
data.SyntaxError = SyntaxError;
data.EvalError = EvalError;
data.RangeError = RangeError;
data.ReferenceError = ReferenceError;
data.URIError = URIError;
// Internationalization
data.Intl = typeof Intl !== 'undefined' ? Intl : {};
// Text
// eslint-disable-next-line id-denylist
data.String = String;
data.RegExp = RegExp;
// Math
data.Math = Math;
// eslint-disable-next-line id-denylist
data.Number = Number;
data.BigInt = typeof BigInt !== 'undefined' ? BigInt : {};
data.Infinity = Infinity;
data.NaN = NaN;
data.isFinite = Number.isFinite;
data.isNaN = Number.isNaN;
data.parseFloat = parseFloat;
data.parseInt = parseInt;
// Structured data
data.JSON = JSON;
data.ArrayBuffer = typeof ArrayBuffer !== 'undefined' ? ArrayBuffer : {};
data.SharedArrayBuffer = typeof SharedArrayBuffer !== 'undefined' ? SharedArrayBuffer : {};
data.Atomics = typeof Atomics !== 'undefined' ? Atomics : {};
data.DataView = typeof DataView !== 'undefined' ? DataView : {};
data.encodeURI = encodeURI;
data.encodeURIComponent = encodeURIComponent;
data.decodeURI = decodeURI;
data.decodeURIComponent = decodeURIComponent;
// Other
// eslint-disable-next-line id-denylist
data.Boolean = Boolean;
data.Symbol = Symbol;
// expression extensions
data.extend = extend;
data.extendOptional = extendOptional;
Object.assign(data, extendedFunctions);
const constructorValidation = new RegExp(/\.\s*constructor/gm);
if (parameterValue.match(constructorValidation)) {
throw new ExpressionError('Expression contains invalid constructor function call', {
causeDetailed: 'Constructor override attempt is not allowed due to security concerns',
runIndex,
itemIndex,
});
}
// Execute the expression
const extendedExpression = extendSyntax(parameterValue);
const returnValue = this.renderExpression(extendedExpression, data);
if (typeof returnValue === 'function') {
if (returnValue.name === '$') throw new Error('invalid syntax');
if (returnValue.name === 'DateTime')
throw new Error('this is a DateTime, please access its methods');
throw new Error('this is a function, please add ()');
} else if (typeof returnValue === 'string') {
return returnValue;
} else if (returnValue !== null && typeof returnValue === 'object') {
if (returnObjectAsString) {
return this.convertObjectValueToString(returnValue);
}
}
return returnValue;
}
private renderExpression(
expression: string,
data: IWorkflowDataProxyData,
): tmpl.ReturnValue | undefined {
try {
return tmpl.tmpl(expression, data);
} catch (error) {
if (error instanceof ExpressionError) {
// Ignore all errors except if they are ExpressionErrors and they are supposed
// to fail the execution
if (error.context.failExecution) {
throw error;
}
if (typeof process === 'undefined' && error.clientOnly) {
throw error;
}
}
// Syntax errors resolve to `Error` on the frontend and `null` on the backend.
// This is a temporary divergence in evaluation behavior until we make the
// breaking change to allow syntax errors to fail executions.
if (
typeof process === 'undefined' &&
error instanceof Error &&
error.name === 'SyntaxError'
) {
throw new Error('invalid syntax');
}
if (
typeof process === 'undefined' &&
error instanceof Error &&
error.name === 'TypeError' &&
error.message.endsWith('is not a function')
) {
const match = error.message.match(/(?<msg>[^.]+is not a function)/);
if (!match?.groups?.msg) return null;
throw new Error(match.groups.msg);
}
}
return null;
}
/**
* Resolves value of parameter. But does not work for workflow-data.
*
* @param {(string | undefined)} parameterValue
*/
getSimpleParameterValue(
node: INode,
parameterValue: string | boolean | undefined,
mode: WorkflowExecuteMode,
timezone: string,
additionalKeys: IWorkflowDataProxyAdditionalKeys,
executeData?: IExecuteData,
defaultValue?: boolean | number | string,
): boolean | number | string | undefined {
if (parameterValue === undefined) {
// Value is not set so return the default
return defaultValue;
}
// Get the value of the node (can be an expression)
const runIndex = 0;
const itemIndex = 0;
const connectionInputData: INodeExecutionData[] = [];
const runData: IRunExecutionData = {
resultData: {
runData: {},
},
};
return this.getParameterValue(
parameterValue,
runData,
runIndex,
itemIndex,
node.name,
connectionInputData,
mode,
timezone,
additionalKeys,
executeData,
) as boolean | number | string | undefined;
}
/**
* Resolves value of complex parameter. But does not work for workflow-data.
*
* @param {(NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[])} parameterValue
* @param {(NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | undefined)} [defaultValue]
*/
getComplexParameterValue(
node: INode,
parameterValue: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[],
mode: WorkflowExecuteMode,
timezone: string,
additionalKeys: IWorkflowDataProxyAdditionalKeys,
executeData?: IExecuteData,
defaultValue: NodeParameterValueType | undefined = undefined,
selfData = {},
): NodeParameterValueType | undefined {
if (parameterValue === undefined) {
// Value is not set so return the default
return defaultValue;
}
// Get the value of the node (can be an expression)
const runIndex = 0;
const itemIndex = 0;
const connectionInputData: INodeExecutionData[] = [];
const runData: IRunExecutionData = {
resultData: {
runData: {},
},
};
// Resolve the "outer" main values
const returnData = this.getParameterValue(
parameterValue,
runData,
runIndex,
itemIndex,
node.name,
connectionInputData,
mode,
timezone,
additionalKeys,
executeData,
false,
selfData,
);
// Resolve the "inner" values
return this.getParameterValue(
returnData,
runData,
runIndex,
itemIndex,
node.name,
connectionInputData,
mode,
timezone,
additionalKeys,
executeData,
false,
selfData,
);
}
/**
* Returns the resolved node parameter value. If it is an expression it will execute it and
* return the result. If the value to resolve is an array or object it will do the same
* for all of the items and values.
*
* @param {(NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[])} parameterValue
* @param {(IRunExecutionData | null)} runExecutionData
* @param {boolean} [returnObjectAsString=false]
*/
getParameterValue(
parameterValue: NodeParameterValueType | INodeParameterResourceLocator,
runExecutionData: IRunExecutionData | null,
runIndex: number,
itemIndex: number,
activeNodeName: string,
connectionInputData: INodeExecutionData[],
mode: WorkflowExecuteMode,
timezone: string,
additionalKeys: IWorkflowDataProxyAdditionalKeys,
executeData?: IExecuteData,
returnObjectAsString = false,
selfData = {},
): NodeParameterValueType {
// Helper function which returns true when the parameter is a complex one or array
const isComplexParameter = (value: NodeParameterValueType) => {
return typeof value === 'object';
};
// Helper function which resolves a parameter value depending on if it is simply or not
const resolveParameterValue = (
value: NodeParameterValueType,
siblingParameters: INodeParameters,
) => {
if (isComplexParameter(value)) {
return this.getParameterValue(
value,
runExecutionData,
runIndex,
itemIndex,
activeNodeName,
connectionInputData,
mode,
timezone,
additionalKeys,
executeData,
returnObjectAsString,
selfData,
);
}
return this.resolveSimpleParameterValue(
value as NodeParameterValue,
siblingParameters,
runExecutionData,
runIndex,
itemIndex,
activeNodeName,
connectionInputData,
mode,
timezone,
additionalKeys,
executeData,
returnObjectAsString,
selfData,
);
};
// Check if it value is a simple one that we can get it resolved directly
if (!isComplexParameter(parameterValue)) {
return this.resolveSimpleParameterValue(
parameterValue as NodeParameterValue,
{},
runExecutionData,
runIndex,
itemIndex,
activeNodeName,
connectionInputData,
mode,
timezone,
additionalKeys,
executeData,
returnObjectAsString,
selfData,
);
}
// The parameter value is complex so resolve depending on type
if (Array.isArray(parameterValue)) {
// Data is an array
const returnData = parameterValue.map((item) =>
resolveParameterValue(item as NodeParameterValueType, {}),
);
return returnData as NodeParameterValue[] | INodeParameters[];
}
if (parameterValue === null || parameterValue === undefined) {
return parameterValue;
}
if (typeof parameterValue !== 'object') {
return {};
}
// Data is an object
const returnData: INodeParameters = {};
// eslint-disable-next-line no-restricted-syntax
for (const [key, value] of Object.entries(parameterValue)) {
returnData[key] = resolveParameterValue(
value as NodeParameterValueType,
parameterValue as INodeParameters,
);
}
if (returnObjectAsString && typeof returnData === 'object') {
return this.convertObjectValueToString(returnData);
}
return returnData;
}
}