From 5251c606c00e7772ed3dcd4e5dcc11681c8a7f17 Mon Sep 17 00:00:00 2001 From: Val <68596159+valya@users.noreply.github.com> Date: Tue, 14 Mar 2023 15:22:52 +0000 Subject: [PATCH] test: Add expression transform tests (#5497) (no-changelog) test: add expression transform tests --- packages/workflow/src/Expression.ts | 49 +------ .../src/Extensions/ExpressionExtension.ts | 136 ++++++++++++++++-- packages/workflow/test/Expression.test.ts | 34 +++-- .../workflow/test/ExpressionFixtures/base.ts | 128 ++++++++++++++++- 4 files changed, 278 insertions(+), 69 deletions(-) diff --git a/packages/workflow/src/Expression.ts b/packages/workflow/src/Expression.ts index 5f996b408b59e..485cce8b4ccbd 100644 --- a/packages/workflow/src/Expression.ts +++ b/packages/workflow/src/Expression.ts @@ -14,16 +14,14 @@ import type { NodeParameterValueType, WorkflowExecuteMode, } from './Interfaces'; -import { ExpressionError, ExpressionExtensionError } from './ExpressionError'; +import { ExpressionError } from './ExpressionError'; import { WorkflowDataProxy } from './WorkflowDataProxy'; import type { Workflow } from './Workflow'; // eslint-disable-next-line import/no-cycle -import { extend, extendOptional, hasExpressionExtension, hasNativeMethod } from './Extensions'; -import type { ExpressionChunk, ExpressionCode } from './Extensions/ExpressionParser'; -import { joinExpression, splitExpression } from './Extensions/ExpressionParser'; -import { extendTransform } from './Extensions/ExpressionExtension'; +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('{{ }}'); @@ -292,7 +290,7 @@ export class Expression { } // Execute the expression - const extendedExpression = this.extendSyntax(parameterValue); + const extendedExpression = extendSyntax(parameterValue); const returnValue = this.renderExpression(extendedExpression, data); if (typeof returnValue === 'function') { if (returnValue.name === '$') throw new Error('invalid syntax'); @@ -358,45 +356,6 @@ export class Expression { return null; } - extendSyntax(bracketedExpression: string): string { - const chunks = splitExpression(bracketedExpression); - - const codeChunks = chunks - .filter((c) => c.type === 'code') - .map((c) => c.text.replace(/("|').*?("|')/, '').trim()); - - if (!codeChunks.some(hasExpressionExtension) || hasNativeMethod(bracketedExpression)) - return bracketedExpression; - - const extendedChunks = chunks.map((chunk): ExpressionChunk => { - if (chunk.type === 'code') { - const output = extendTransform(chunk.text); - - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (!output?.code) { - throw new ExpressionExtensionError('invalid syntax'); - } - - let text = output.code; - - // We need to cut off any trailing semicolons. These cause issues - // with certain types of expression and cause the whole expression - // to fail. - if (text.trim().endsWith(';')) { - text = text.trim().slice(0, -1); - } - - return { - ...chunk, - text, - } as ExpressionCode; - } - return chunk; - }); - - return joinExpression(extendedChunks); - } - /** * Resolves value of parameter. But does not work for workflow-data. * diff --git a/packages/workflow/src/Extensions/ExpressionExtension.ts b/packages/workflow/src/Extensions/ExpressionExtension.ts index 112e5ce36cf41..6600f8e8ead68 100644 --- a/packages/workflow/src/Extensions/ExpressionExtension.ts +++ b/packages/workflow/src/Extensions/ExpressionExtension.ts @@ -3,6 +3,7 @@ import { DateTime } from 'luxon'; import { ExpressionExtensionError } from '../ExpressionError'; import { parse, visit, types, print } from 'recast'; import { getOption } from 'recast/lib/util'; +import type { Config as EsprimaConfig } from 'esprima-next'; import { parse as esprimaParse } from 'esprima-next'; import { arrayExtensions } from './ArrayExtensions'; @@ -12,6 +13,9 @@ import { stringExtensions } from './StringExtensions'; import { objectExtensions } from './ObjectExtensions'; import type { ExpressionKind } from 'ast-types/gen/kinds'; +import type { ExpressionChunk, ExpressionCode } from './ExpressionParser'; +import { joinExpression, splitExpression } from './ExpressionParser'; + const EXPRESSION_EXTENDER = 'extend'; const EXPRESSION_EXTENDER_OPTIONAL = 'extendOptional'; @@ -89,17 +93,12 @@ function parseWithEsprimaNext(source: string, options?: any): any { loc: true, locations: true, comment: true, - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - range: getOption(options, 'range', false), - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - tolerant: getOption(options, 'tolerant', true), + range: getOption(options, 'range', false) as boolean, + tolerant: getOption(options, 'tolerant', true) as boolean, tokens: true, - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - jsx: getOption(options, 'jsx', false), - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - sourceType: getOption(options, 'sourceType', 'module'), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any); + jsx: getOption(options, 'jsx', false) as boolean, + sourceType: getOption(options, 'sourceType', 'module') as string, + } as EsprimaConfig); return ast; } @@ -124,9 +123,8 @@ export const extendTransform = (expression: string): { code: string } | undefine let currentChain = 1; - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + // Polyfill optional chaining visit(ast, { - // Polyfill optional chaining visitChainExpression(path) { this.traverse(path); const chainNumber = currentChain; @@ -138,6 +136,8 @@ export const extendTransform = (expression: string): { code: string } | undefine // @ts-ignore typeof window !== 'object' ? 'global' : 'window', ); + // We want to define all of our commonly used identifiers and member + // expressions now so we don't have to create multiple instances const undefinedIdentifier = types.builders.identifier('undefined'); const cancelIdentifier = types.builders.identifier(`chainCancelToken${chainNumber}`); const valueIdentifier = types.builders.identifier(`chainValue${chainNumber}`); @@ -152,6 +152,8 @@ export const extendTransform = (expression: string): { code: string } | undefine const patchedStack: ExpressionKind[] = []; + // This builds the cancel check. This lets us slide to the end of the expression + // if it's undefined/null at any of the optional points of the chain. const buildCancelCheckWrapper = (node: ExpressionKind): ExpressionKind => { return types.builders.conditionalExpression( types.builders.binaryExpression( @@ -164,10 +166,17 @@ export const extendTransform = (expression: string): { code: string } | undefine ); }; + // This is just a quick small wrapper to create the assignment expression + // for the running value. const buildValueAssignWrapper = (node: ExpressionKind): ExpressionKind => { return types.builders.assignmentExpression('=', valueMemberExpression, node); }; + // This builds what actually does the comparison. It wraps the current + // chunk of the expression with a nullish coalescing operator that returns + // undefined if it's null or undefined. We do this because optional chains + // always return undefined if they fail part way, even if the value they + // fail on is null. const buildOptionalWrapper = (node: ExpressionKind): ExpressionKind => { return types.builders.binaryExpression( '===', @@ -180,6 +189,7 @@ export const extendTransform = (expression: string): { code: string } | undefine ); }; + // Another small wrapper, but for assigning to the cancel token this time. const buildCancelAssignWrapper = (node: ExpressionKind): ExpressionKind => { return types.builders.assignmentExpression('=', cancelMemberExpression, node); }; @@ -189,6 +199,9 @@ export const extendTransform = (expression: string): { code: string } | undefine let patchTop: ExpressionKind | null = null; let wrapNextTopInOptionalExtend = false; + // This patches the previous node to use our current one as it's left hand value. + // It takes `window.chainValue1.test1` and `window.chainValue1.test2` and turns it + // into `window.chainValue1.test2.test1`. const updatePatch = (toPatch: ExpressionKind, node: ExpressionKind) => { if (toPatch.type === 'MemberExpression' || toPatch.type === 'OptionalMemberExpression') { toPatch.object = node; @@ -200,7 +213,15 @@ export const extendTransform = (expression: string): { code: string } | undefine } }; + // This loop walks down an optional chain from the top. This will walk + // from right to left through an optional chain. We keep track of our current + // top of the chain (furthest right) and create a chain below it. This chain + // contains all of the (member and call) expressions that we need. These are + // patched versions that reference our current chain value. We then push this + // chain onto a stack when we hit an optional point in our chain. while (true) { + // This should only ever be these types but you can optional chain on + // JSX nodes, which we don't support. if ( currentNode.type === 'MemberExpression' || currentNode.type === 'OptionalMemberExpression' || @@ -208,6 +229,10 @@ export const extendTransform = (expression: string): { code: string } | undefine currentNode.type === 'OptionalCallExpression' ) { let patchNode: ExpressionKind; + // Here we take the current node and extract the parts we actually care + // about. + // In the case of a member expression we take the property it's trying to + // access and make the object it's accessing be our chain value. if ( currentNode.type === 'MemberExpression' || currentNode.type === 'OptionalMemberExpression' @@ -216,6 +241,8 @@ export const extendTransform = (expression: string): { code: string } | undefine valueMemberExpression, currentNode.property, ); + // In the case of a call expression we take the arguments and make the + // callee our chain value. } else { patchNode = types.builders.callExpression( valueMemberExpression, @@ -223,16 +250,22 @@ export const extendTransform = (expression: string): { code: string } | undefine ); } + // If we have a previous node we patch it here. if (currentPatch) { updatePatch(currentPatch, patchNode); } + // If we have no top patch (first run, or just pushed onto the stack) we + // note it here. if (!patchTop) { patchTop = patchNode; } currentPatch = patchNode; + // This is an optional in our chain. In here we'll push the node onto the + // stack. We also do a polyfill if the top of the stack is function call + // that might be a extended function. if (currentNode.optional) { // Implement polyfill described below if (wrapNextTopInOptionalExtend) { @@ -268,6 +301,7 @@ export const extendTransform = (expression: string): { code: string } | undefine } } + // Finally we get the next point AST to walk down. if ( currentNode.type === 'MemberExpression' || currentNode.type === 'OptionalMemberExpression' @@ -277,6 +311,8 @@ export const extendTransform = (expression: string): { code: string } | undefine currentNode = currentNode.callee; } } else { + // We update the final patch to point to the first part of the optional chain + // which is probably an identifier for an object. if (currentPatch) { updatePatch(currentPatch, currentNode); if (!patchTop) { @@ -298,6 +334,7 @@ export const extendTransform = (expression: string): { code: string } | undefine } } + // Push the first part of our chain to stack. if (patchTop) { patchedStack.push(patchTop); } else { @@ -307,28 +344,42 @@ export const extendTransform = (expression: string): { code: string } | undefine } } + // Since we're working from right to left we need to flip the stack + // for the correct order of operations patchedStack.reverse(); + // Walk the node stack and wrap all our expressions in cancel/assignment + // wrappers. for (let i = 0; i < patchedStack.length; i++) { let node = patchedStack[i]; + // We don't wrap the last expression in an assignment wrapper because + // it's going to be returned anyway. We just wrap it in a cancel check + // wrapper. if (i !== patchedStack.length - 1) { node = buildCancelAssignWrapper(buildOptionalWrapper(node)); } + // Don't wrap the first part in a cancel wrapper because the cancel + // token will always be undefined. if (i !== 0) { node = buildCancelCheckWrapper(node); } + + // Replace the node in the stack with our wrapped one patchedStack[i] = node; } + // Put all our expressions in a sequence expression (also called a + // group operator). These will all be executed in order and the value + // of the final expression will be returned. const sequenceNode = types.builders.sequenceExpression(patchedStack); path.replace(sequenceNode); }, }); - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + // Extended functions visit(ast, { visitCallExpression(path) { this.traverse(path); @@ -377,7 +428,6 @@ export const extendTransform = (expression: string): { code: string } | undefine }, }); - // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-argument return print(ast); } catch (e) { return; @@ -515,3 +565,61 @@ export function extendOptional( return foundFunction.function(input, args); }; } + +const EXTENDED_SYNTAX_CACHE: Record = {}; + +export function extendSyntax(bracketedExpression: string, forceExtend = false): string { + const chunks = splitExpression(bracketedExpression); + + const codeChunks = chunks + .filter((c) => c.type === 'code') + .map((c) => c.text.replace(/("|').*?("|')/, '').trim()); + + if ( + (!codeChunks.some(hasExpressionExtension) || hasNativeMethod(bracketedExpression)) && + !forceExtend + ) { + return bracketedExpression; + } + + // If we've seen this expression before grab it from the cache + if (bracketedExpression in EXTENDED_SYNTAX_CACHE) { + return EXTENDED_SYNTAX_CACHE[bracketedExpression]; + } + + const extendedChunks = chunks.map((chunk): ExpressionChunk => { + if (chunk.type === 'code') { + let output = extendTransform(chunk.text); + + // esprima fails to parse bare objects (e.g. `{ data: something }`), we can + // work around this by wrapping it in an parentheses + if (!output?.code && chunk.text.trim()[0] === '{') { + output = extendTransform(`(${chunk.text})`); + } + + if (!output?.code) { + throw new ExpressionExtensionError('invalid syntax'); + } + + let text = output.code; + + // We need to cut off any trailing semicolons. These cause issues + // with certain types of expression and cause the whole expression + // to fail. + if (text.trim().endsWith(';')) { + text = text.trim().slice(0, -1); + } + + return { + ...chunk, + text, + } as ExpressionCode; + } + return chunk; + }); + + const expression = joinExpression(extendedChunks); + // Cache the expression so we don't have to do this transform again + EXTENDED_SYNTAX_CACHE[bracketedExpression] = expression; + return expression; +} diff --git a/packages/workflow/test/Expression.test.ts b/packages/workflow/test/Expression.test.ts index 470929d8dba08..73bc289311be1 100644 --- a/packages/workflow/test/Expression.test.ts +++ b/packages/workflow/test/Expression.test.ts @@ -6,15 +6,13 @@ import { DateTime, Duration, Interval } from 'luxon'; import { Expression } from '@/Expression'; import { Workflow } from '@/Workflow'; import * as Helpers from './Helpers'; -import { baseFixtures } from './ExpressionFixtures/base'; import { - IConnections, - IExecuteData, - INode, - INodeExecutionData, - IRunExecutionData, - ITaskData, -} from '@/Interfaces'; + baseFixtures, + ExpressionTestEvaluation, + ExpressionTestTransform, +} from './ExpressionFixtures/base'; +import { INodeExecutionData } from '@/Interfaces'; +import { extendSyntax } from '@/Extensions/ExpressionExtension'; describe('Expression', () => { describe('getParameterValue()', () => { @@ -186,7 +184,9 @@ describe('Expression', () => { continue; } test(t.expression, () => { - for (const test of t.tests.filter((test) => test.type === 'evaluation')) { + for (const test of t.tests.filter( + (test) => test.type === 'evaluation', + ) as ExpressionTestEvaluation[]) { expect(evaluate(t.expression, test.input.map((d) => ({ json: d })) as any)).toStrictEqual( test.output, ); @@ -194,4 +194,20 @@ describe('Expression', () => { }); } }); + + describe('Test all expression transform fixtures', () => { + for (const t of baseFixtures) { + if (!t.tests.some((test) => test.type === 'transform')) { + continue; + } + test(t.expression, () => { + for (const test of t.tests.filter( + (test) => test.type === 'transform', + ) as ExpressionTestTransform[]) { + const expr = t.expression; + expect(extendSyntax(expr, test.forceTransform)).toEqual(test.result ?? expr); + } + }); + } + }); }); diff --git a/packages/workflow/test/ExpressionFixtures/base.ts b/packages/workflow/test/ExpressionFixtures/base.ts index 90b8fdc9c89ae..c902ccea0b364 100644 --- a/packages/workflow/test/ExpressionFixtures/base.ts +++ b/packages/workflow/test/ExpressionFixtures/base.ts @@ -10,7 +10,14 @@ export interface ExpressionTestEvaluation extends ExpressionTestBase { output: IDataObject | GenericValue; } -export type ExpressionTests = ExpressionTestEvaluation; +export interface ExpressionTestTransform extends ExpressionTestBase { + type: 'transform'; + // If we don't specify a result we expect it to be the same as the input + result?: string; + forceTransform?: boolean; +} + +export type ExpressionTests = ExpressionTestEvaluation | ExpressionTestTransform; export interface ExpressionTestFixture { expression: string; @@ -31,6 +38,8 @@ export const baseFixtures: ExpressionTestFixture[] = [ input: [{ contact: null }], output: undefined, }, + { type: 'transform' }, + { type: 'transform', forceTransform: true }, ], }, { @@ -56,6 +65,8 @@ export const baseFixtures: ExpressionTestFixture[] = [ input: [{}], output: undefined, }, + { type: 'transform' }, + { type: 'transform', forceTransform: true }, ], }, { @@ -66,6 +77,8 @@ export const baseFixtures: ExpressionTestFixture[] = [ input: [{ test: { json: { message: { message_id: 'value' } } } }], output: 'value', }, + { type: 'transform' }, + { type: 'transform', forceTransform: true }, ], }, { @@ -76,6 +89,8 @@ export const baseFixtures: ExpressionTestFixture[] = [ input: [{ Set2: { json: { apiKey: 'testKey' } }, testKey: 'testValue' }], output: 'testValue', }, + { type: 'transform' }, + { type: 'transform', forceTransform: true }, ], }, { @@ -86,6 +101,8 @@ export const baseFixtures: ExpressionTestFixture[] = [ input: [{ get: { json: { recipes: [{ image: 'test' }] } } }], output: 'test', }, + { type: 'transform' }, + { type: 'transform', forceTransform: true }, ], }, { @@ -97,6 +114,8 @@ export const baseFixtures: ExpressionTestFixture[] = [ input: [{ Clockify1: { parameter: { workspaceId: 'test1' }, json: { id: 'test2' } } }], output: 'https://example.com/api/v1/workspaces/test1/projects/test2', }, + { type: 'transform' }, + { type: 'transform', forceTransform: true }, ], }, { @@ -107,6 +126,8 @@ export const baseFixtures: ExpressionTestFixture[] = [ input: [{ 'dig check CF': { data: { stdout: 'testout' } } }], output: ' testout', }, + { type: 'transform' }, + { type: 'transform', forceTransform: true }, ], }, { @@ -117,6 +138,8 @@ export const baseFixtures: ExpressionTestFixture[] = [ input: [{ 'Set URL': { json: { base_domain: 'left' } }, link: 'right' }], output: 'leftright', }, + { type: 'transform' }, + { type: 'transform', forceTransform: true }, ], }, { @@ -127,6 +150,8 @@ export const baseFixtures: ExpressionTestFixture[] = [ input: [], output: 0, }, + { type: 'transform' }, + { type: 'transform', forceTransform: true }, ], }, { @@ -137,6 +162,8 @@ export const baseFixtures: ExpressionTestFixture[] = [ input: [], output: '', }, + { type: 'transform' }, + { type: 'transform', forceTransform: true }, ], }, { @@ -157,6 +184,8 @@ export const baseFixtures: ExpressionTestFixture[] = [ ], output: 10, }, + { type: 'transform' }, + { type: 'transform', forceTransform: true }, ], }, { @@ -167,6 +196,8 @@ export const baseFixtures: ExpressionTestFixture[] = [ input: [{ GetTicket: { json: { tickets: [1, 2, 3, 4] } } }], output: 4, }, + { type: 'transform' }, + { type: 'transform', forceTransform: true }, ], }, { @@ -177,6 +208,8 @@ export const baseFixtures: ExpressionTestFixture[] = [ input: [{ test: 1 }], output: '[object Object]', }, + { type: 'transform' }, + { type: 'transform', forceTransform: true }, ], }, { @@ -187,6 +220,10 @@ export const baseFixtures: ExpressionTestFixture[] = [ input: [], output: 100, }, + { + type: 'transform', + result: '={{extend(Math, "floor", [extend(Math, "min", [1, 2]) * 100])}}', + }, ], }, { @@ -203,6 +240,8 @@ export const baseFixtures: ExpressionTestFixture[] = [ ], output: 'test', }, + { type: 'transform' }, + { type: 'transform', forceTransform: true }, ], }, { @@ -228,6 +267,8 @@ export const baseFixtures: ExpressionTestFixture[] = [ input: [], output: undefined, }, + { type: 'transform' }, + { type: 'transform', forceTransform: true }, ], }, { @@ -239,6 +280,8 @@ export const baseFixtures: ExpressionTestFixture[] = [ input: [{ Webhook1: { json: { headers: { 'x-api-key': 'left' } } } }], output: 'left-test', }, + { type: 'transform' }, + { type: 'transform', forceTransform: true }, ], }, { @@ -250,6 +293,8 @@ export const baseFixtures: ExpressionTestFixture[] = [ input: [{ Webhook1: { json: { headers: { 'x-api-key': 'left' } } }, test: 3 }], output: 'left-3', }, + { type: 'transform' }, + { type: 'transform', forceTransform: true }, ], }, { @@ -260,6 +305,8 @@ export const baseFixtures: ExpressionTestFixture[] = [ input: [{ 'Create or update': { json: { vid: [1, 2, 3] } } }], output: [1, 2, 3], }, + { type: 'transform' }, + { type: 'transform', forceTransform: true }, ], }, { @@ -270,6 +317,8 @@ export const baseFixtures: ExpressionTestFixture[] = [ input: [{ Crypto: { json: { data: 'testtest' } } }], output: 'https://example.com/test?id=testte', }, + { type: 'transform' }, + { type: 'transform', forceTransform: true }, ], }, { @@ -280,6 +329,8 @@ export const baseFixtures: ExpressionTestFixture[] = [ input: [{ body: { project: { name: 'test[1234]' } } }], output: '1234', }, + { type: 'transform' }, + { type: 'transform', forceTransform: true }, ], }, { @@ -296,6 +347,8 @@ export const baseFixtures: ExpressionTestFixture[] = [ ], output: 4, }, + { type: 'transform' }, + { type: 'transform', forceTransform: true }, ], }, { @@ -312,6 +365,8 @@ export const baseFixtures: ExpressionTestFixture[] = [ input: [{ projectName: 'Project Test', projectsCount: 3 }], output: 'Project Test', }, + { type: 'transform' }, + { type: 'transform', forceTransform: true }, ], }, { @@ -322,6 +377,8 @@ export const baseFixtures: ExpressionTestFixture[] = [ input: [{ created_at: '2023-02-09T13:22:54.187Z' }], output: '2023-02-09T13:22:54.187Z', }, + { type: 'transform' }, + { type: 'transform', forceTransform: true }, ], }, { @@ -332,6 +389,8 @@ export const baseFixtures: ExpressionTestFixture[] = [ input: [{ 'Find by ID1': { json: { fields: { clicks: 8 } } } }], output: 9, }, + { type: 'transform' }, + { type: 'transform', forceTransform: true }, ], }, { @@ -343,6 +402,8 @@ export const baseFixtures: ExpressionTestFixture[] = [ input: [{ Bid: '3,80', Baserow: { json: { Count: '10' } } }], output: '38.00', }, + { type: 'transform' }, + { type: 'transform', forceTransform: true }, ], }, { @@ -363,6 +424,8 @@ export const baseFixtures: ExpressionTestFixture[] = [ } }`, }, + { type: 'transform' }, + { type: 'transform', forceTransform: true }, ], }, { @@ -389,6 +452,8 @@ export const baseFixtures: ExpressionTestFixture[] = [ input: [{ 'Find by ID': { json: {} } }], output: false, }, + { type: 'transform' }, + { type: 'transform', forceTransform: true }, ], }, { @@ -404,6 +469,8 @@ export const baseFixtures: ExpressionTestFixture[] = [ input: [{ 'HTTP Request': { json: {} } }], output: false, }, + { type: 'transform' }, + { type: 'transform', forceTransform: true }, ], }, { @@ -414,6 +481,19 @@ export const baseFixtures: ExpressionTestFixture[] = [ input: [], output: 1, }, + { type: 'transform', result: '={{extend(Math, "min", [1, 2])}}' }, + ], + }, + { + expression: '={{new String().toString();}}', + tests: [ + { + type: 'evaluation', + input: [], + output: '', + }, + { type: 'transform' }, + { type: 'transform', forceTransform: true, result: '={{new String().toString()}}' }, ], }, { @@ -439,6 +519,8 @@ export const baseFixtures: ExpressionTestFixture[] = [ input: [{ different: { phone: 'test', name: 'test2' } }], output: true, }, + { type: 'transform' }, + { type: 'transform', forceTransform: true }, ], }, { @@ -449,6 +531,8 @@ export const baseFixtures: ExpressionTestFixture[] = [ input: [], output: 200, }, + { type: 'transform' }, + { type: 'transform', forceTransform: true }, ], }, { @@ -459,6 +543,8 @@ export const baseFixtures: ExpressionTestFixture[] = [ input: [{ assetValue: 50, value: 50 }], output: 25, }, + { type: 'transform' }, + { type: 'transform', forceTransform: true }, ], }, { @@ -474,6 +560,8 @@ export const baseFixtures: ExpressionTestFixture[] = [ input: [{ search_term: 'asdf' }], output: false, }, + { type: 'transform' }, + { type: 'transform', forceTransform: true }, ], }, { @@ -487,6 +575,8 @@ value multi line`, }, + { type: 'transform' }, + { type: 'transform', forceTransform: true }, ], }, { @@ -497,6 +587,12 @@ line`, input: [{ body: { choices: 'testValue' } }], output: { data: 'testValue' }, }, + { type: 'transform' }, + { + type: 'transform', + forceTransform: true, + result: '={{( { "data": $json.body.choices } )}}', + }, ], }, { @@ -517,6 +613,8 @@ line`, input: [{ data: {} }], output: undefined, }, + { type: 'transform' }, + { type: 'transform', forceTransform: true }, ], }, { @@ -532,6 +630,8 @@ line`, input: [{ asdas: 1 }], output: undefined, }, + { type: 'transform' }, + { type: 'transform', forceTransform: true }, ], }, { @@ -547,6 +647,8 @@ line`, input: [{ data: {} }], output: false, }, + { type: 'transform' }, + { type: 'transform', forceTransform: true }, ], }, { @@ -557,6 +659,8 @@ line`, input: [], output: 'TRUE', }, + { type: 'transform' }, + { type: 'transform', forceTransform: true }, ], }, { @@ -597,6 +701,13 @@ line`, input: [{}], output: true, }, + { type: 'transform' }, + { + type: 'transform', + forceTransform: true, + result: + '={{ !(window.chainCancelToken1 = ((window.chainValue1 = $json) ?? undefined) === undefined, window.chainCancelToken1 === true ? undefined : window.chainCancelToken1 = ((window.chainValue1 = window.chainValue1.data) ?? undefined) === undefined, window.chainCancelToken1 === true ? undefined : window.chainCancelToken1 = ((window.chainValue1 = window.chainValue1.data) ?? undefined) === undefined, window.chainCancelToken1 === true ? undefined : window.chainCancelToken1 = ((window.chainValue1 = window.chainValue1.issues) ?? undefined) === undefined, window.chainCancelToken1 === true ? undefined : window.chainCancelToken1 = ((window.chainValue1 = window.chainValue1.pageInfo) ?? undefined) === undefined, window.chainCancelToken1 === true ? undefined : window.chainValue1.hasNextPage) }}', + }, ], }, { @@ -607,6 +718,8 @@ line`, input: [], output: [{ name: 'something', batch_size: 1000, ignore_cols: ['x'] }], }, + { type: 'transform' }, + { type: 'transform', forceTransform: true }, ], }, { @@ -622,6 +735,8 @@ line`, input: [{ person: { json: {} } }], output: false, }, + { type: 'transform' }, + { type: 'transform', forceTransform: true }, ], }, { @@ -637,6 +752,13 @@ line`, input: [{}], output: '', }, + { type: 'transform' }, + { + type: 'transform', + forceTransform: true, + result: + "={{ (window.chainCancelToken1 = ((window.chainValue1 = $json) ?? undefined) === undefined, window.chainCancelToken1 === true ? undefined : window.chainValue1.data) == undefined ? '' : $json.data }}", + }, ], }, { @@ -657,6 +779,8 @@ line`, input: [{}], output: false, }, + { type: 'transform' }, + { type: 'transform', forceTransform: true }, ], }, { @@ -667,6 +791,8 @@ line`, input: [], output: 7, }, + { type: 'transform' }, + { type: 'transform', forceTransform: true }, ], }, ];