Initial commit: n8n Strike API node
- Add Strike API credentials configuration - Implement Strike node with 9 resources (Account, Balance, Currency Exchange, Deposit, Invoice, Payment, Payment Method, Payout, Rates) - Add comprehensive operation descriptions for all resources - Include CLAUDE.MD documentation - Set up build configuration with TypeScript, ESLint, and Prettier 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
138
node_modules/@n8n/tournament/src/Analysis.ts
generated
vendored
Normal file
138
node_modules/@n8n/tournament/src/Analysis.ts
generated
vendored
Normal file
@ -0,0 +1,138 @@
|
||||
import * as tmpl from '@n8n_io/riot-tmpl';
|
||||
import { print, visit } from 'recast';
|
||||
import { isDifferent } from './Differ';
|
||||
|
||||
import type { ExpressionAnalysis } from './ExpressionBuilder';
|
||||
import { getExpressionCode, getParsedExpression } from './ExpressionBuilder';
|
||||
import { joinExpression } from './ExpressionSplitter';
|
||||
|
||||
interface TmplOrTournament {
|
||||
tmpl: boolean;
|
||||
tournament: boolean;
|
||||
}
|
||||
interface TmplSame {
|
||||
same: true;
|
||||
expression?: SanitizedString;
|
||||
}
|
||||
interface TmplDiff {
|
||||
same: false;
|
||||
expression: SanitizedString | 'UNPARSEABLE';
|
||||
has?: ExpressionAnalysis['has'];
|
||||
parserError?: TmplOrTournament;
|
||||
}
|
||||
export type TmplDifference = TmplSame | TmplDiff;
|
||||
|
||||
export const getTmplDifference = (expr: string, dataNodeName: string): TmplDifference => {
|
||||
if (!expr) {
|
||||
return { same: true };
|
||||
}
|
||||
if (tmpl.brackets.settings.brackets !== '{{ }}') {
|
||||
tmpl.brackets.set('{{ }}');
|
||||
}
|
||||
let tournParsed: string | null;
|
||||
let tmplParsed: string | null;
|
||||
let analysis: ExpressionAnalysis | null;
|
||||
try {
|
||||
[tournParsed, analysis] = getExpressionCode(expr, dataNodeName, { before: [], after: [] });
|
||||
} catch (e) {
|
||||
tournParsed = null;
|
||||
analysis = null;
|
||||
}
|
||||
try {
|
||||
tmplParsed = tmpl.tmpl.getStr(expr);
|
||||
} catch (e) {
|
||||
tmplParsed = null;
|
||||
}
|
||||
if (analysis?.has.function || analysis?.has.templateString) {
|
||||
return {
|
||||
same: false,
|
||||
expression: stripIdentifyingInformation(expr),
|
||||
has: analysis.has,
|
||||
};
|
||||
}
|
||||
if (tournParsed === null && tmplParsed === null) {
|
||||
// Bad expression
|
||||
return { same: true };
|
||||
} else if (tournParsed === null) {
|
||||
// Unparsable expression for tournament
|
||||
return {
|
||||
same: false,
|
||||
expression: 'UNPARSEABLE',
|
||||
parserError: {
|
||||
tmpl: false,
|
||||
tournament: true,
|
||||
},
|
||||
};
|
||||
} else if (tmplParsed === null) {
|
||||
// Unparsable expression for tmpl
|
||||
return {
|
||||
same: false,
|
||||
expression: stripIdentifyingInformation(expr),
|
||||
parserError: {
|
||||
tmpl: true,
|
||||
tournament: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
const different = isDifferent(tmplParsed, tournParsed);
|
||||
if (different) {
|
||||
// Different output
|
||||
return {
|
||||
same: false,
|
||||
expression: stripIdentifyingInformation(expr),
|
||||
};
|
||||
}
|
||||
// Same, nothing to report
|
||||
return { same: true, expression: stripIdentifyingInformation(expr) };
|
||||
};
|
||||
|
||||
const CHAR_REPLACE = /\S/gu;
|
||||
|
||||
// Replace all non-whitespace characters with the letter v.
|
||||
// This should work with all characters since we're just
|
||||
// matching on all non-whitespace characters, including
|
||||
// anything outside the ASCII code points.
|
||||
const replaceValue = (value: string): string => {
|
||||
return value.replace(CHAR_REPLACE, 'v');
|
||||
};
|
||||
|
||||
export interface SanitizedString {
|
||||
value: string;
|
||||
sanitized: 'ACTUALLY_SANITIZED_DO_NOT_MANUALLY_MAKE_THIS_OBJECT';
|
||||
}
|
||||
|
||||
export const stripIdentifyingInformation = (expr: string): SanitizedString => {
|
||||
const chunks = getParsedExpression(expr);
|
||||
|
||||
for (const chunk of chunks) {
|
||||
if (chunk.type === 'text') {
|
||||
chunk.text = replaceValue(chunk.text);
|
||||
} else {
|
||||
visit(chunk.parsed, {
|
||||
visitLiteral(path) {
|
||||
this.traverse(path);
|
||||
if (typeof path.node.value === 'string') {
|
||||
path.node.value = replaceValue(path.node.value);
|
||||
}
|
||||
},
|
||||
visitStringLiteral(path) {
|
||||
this.traverse(path);
|
||||
path.node.value = replaceValue(path.node.value);
|
||||
},
|
||||
visitTemplateElement(path) {
|
||||
this.traverse(path);
|
||||
if (path.node.value.cooked !== null) {
|
||||
path.node.value.cooked = replaceValue(path.node.value.cooked);
|
||||
}
|
||||
path.node.value.raw = replaceValue(path.node.value.raw);
|
||||
},
|
||||
});
|
||||
chunk.text = print(chunk.parsed).code;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
value: joinExpression(chunks),
|
||||
sanitized: 'ACTUALLY_SANITIZED_DO_NOT_MANUALLY_MAKE_THIS_OBJECT',
|
||||
};
|
||||
};
|
||||
26
node_modules/@n8n/tournament/src/Constants.ts
generated
vendored
Normal file
26
node_modules/@n8n/tournament/src/Constants.ts
generated
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
import type {
|
||||
CatchClauseKind,
|
||||
ExpressionKind,
|
||||
PatternKind,
|
||||
PropertyKind,
|
||||
StatementKind,
|
||||
VariableDeclaratorKind,
|
||||
} from 'ast-types/lib/gen/kinds';
|
||||
|
||||
export const EXEMPT_IDENTIFIER_LIST = [
|
||||
'isFinite',
|
||||
'isNaN',
|
||||
'NaN',
|
||||
'Date',
|
||||
'RegExp',
|
||||
'Math',
|
||||
'undefined',
|
||||
];
|
||||
|
||||
export type ParentKind =
|
||||
| ExpressionKind
|
||||
| StatementKind
|
||||
| PropertyKind
|
||||
| PatternKind
|
||||
| VariableDeclaratorKind
|
||||
| CatchClauseKind;
|
||||
94
node_modules/@n8n/tournament/src/Differ.ts
generated
vendored
Normal file
94
node_modules/@n8n/tournament/src/Differ.ts
generated
vendored
Normal file
@ -0,0 +1,94 @@
|
||||
import type { StatementKind } from 'ast-types/lib/gen/kinds';
|
||||
import { types, parse } from 'recast';
|
||||
import { parseWithEsprimaNext } from './Parser';
|
||||
|
||||
const isWrapped = (node: types.namedTypes.File): boolean => {
|
||||
return node.program.body[1]?.type === 'TryStatement';
|
||||
};
|
||||
const getWrapped = (node: types.namedTypes.File) => {
|
||||
return (node.program.body[1] as types.namedTypes.TryStatement)?.block.body[0];
|
||||
};
|
||||
|
||||
const isMultiPartExpression = (node: types.namedTypes.File): boolean => {
|
||||
return (
|
||||
node.program.body[1]?.type === 'ReturnStatement' &&
|
||||
node.program.body[1].argument?.type === 'CallExpression' &&
|
||||
node.program.body[1].argument?.arguments[0]?.type === 'Literal' &&
|
||||
node.program.body[1].argument?.arguments[0]?.value === '' &&
|
||||
node.program.body[1].argument?.callee.type === 'MemberExpression' &&
|
||||
node.program.body[1].argument?.callee.object.type === 'ArrayExpression' &&
|
||||
node.program.body[1].argument?.callee.property.type === 'Identifier' &&
|
||||
node.program.body[1].argument?.callee.property.name === 'join'
|
||||
);
|
||||
};
|
||||
|
||||
const getMultiPartExpression = (node: types.namedTypes.File): StatementKind[] => {
|
||||
if (
|
||||
!(
|
||||
node.program.body[1]?.type === 'ReturnStatement' &&
|
||||
node.program.body[1].argument?.type === 'CallExpression' &&
|
||||
node.program.body[1].argument?.arguments[0]?.type === 'Literal' &&
|
||||
node.program.body[1].argument?.arguments[0]?.value === '' &&
|
||||
node.program.body[1].argument?.callee.type === 'MemberExpression' &&
|
||||
node.program.body[1].argument?.callee.object.type === 'ArrayExpression'
|
||||
)
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
return node.program.body[1].argument.callee.object.elements
|
||||
.map((e) => {
|
||||
if (
|
||||
e?.type !== 'CallExpression' ||
|
||||
e.callee.type !== 'MemberExpression' ||
|
||||
e.callee.object.type !== 'FunctionExpression'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const maybeExpr = e.callee.object.body.body[0];
|
||||
if (maybeExpr.type !== 'TryStatement') {
|
||||
return maybeExpr;
|
||||
}
|
||||
return maybeExpr.block.body[0];
|
||||
})
|
||||
.filter((e) => e !== null) as StatementKind[];
|
||||
};
|
||||
|
||||
export const isDifferent = (tmpl: string, tourn: string): boolean => {
|
||||
const tmplParsed = parse(tmpl, {
|
||||
parser: { parse: parseWithEsprimaNext },
|
||||
}) as types.namedTypes.File;
|
||||
const tournParsed = parse(tourn, {
|
||||
parser: { parse: parseWithEsprimaNext },
|
||||
}) as types.namedTypes.File;
|
||||
|
||||
const problemPaths: any[] = [];
|
||||
let same = types.astNodesAreEquivalent(tmplParsed, tournParsed, problemPaths);
|
||||
|
||||
// So we sometimes wrap single value expressions when tmpl doesn't.
|
||||
// This causes our code to obviously be different to tmpl's. It's
|
||||
// actually more of a bug on tmpl's side and it's incredibly hard
|
||||
// to match exactly on our end. I think this is an acceptable difference
|
||||
// in behaviours and shouldn't cause any issues since an exception will
|
||||
// be raised either way.
|
||||
if (!same) {
|
||||
if (isWrapped(tournParsed) && !isWrapped(tmplParsed)) {
|
||||
// This is a single part expression that might be wrapped
|
||||
const tournWrapped = getWrapped(tournParsed);
|
||||
same = types.astNodesAreEquivalent(tmplParsed.program.body[1], tournWrapped);
|
||||
} else if (isMultiPartExpression(tournParsed) && isMultiPartExpression(tmplParsed)) {
|
||||
// This is a multi part expression that may be wrapped internally
|
||||
const tournExprs = getMultiPartExpression(tournParsed);
|
||||
const tmplExprs = getMultiPartExpression(tmplParsed);
|
||||
if (tournExprs.length === tmplExprs.length) {
|
||||
for (let i = 0; i < tournExprs.length; i++) {
|
||||
same = types.astNodesAreEquivalent(tmplExprs[i], tournExprs[i], problemPaths);
|
||||
if (!same) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return !same;
|
||||
};
|
||||
9
node_modules/@n8n/tournament/src/Evaluator.ts
generated
vendored
Normal file
9
node_modules/@n8n/tournament/src/Evaluator.ts
generated
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
import type { ReturnValue, Tournament } from '.';
|
||||
|
||||
export interface ExpressionEvaluator {
|
||||
evaluate(code: string, data: unknown): ReturnValue;
|
||||
}
|
||||
|
||||
export interface ExpressionEvaluatorClass {
|
||||
new (instance: Tournament): ExpressionEvaluator;
|
||||
}
|
||||
297
node_modules/@n8n/tournament/src/ExpressionBuilder.ts
generated
vendored
Normal file
297
node_modules/@n8n/tournament/src/ExpressionBuilder.ts
generated
vendored
Normal file
@ -0,0 +1,297 @@
|
||||
import type { namedTypes } from 'ast-types';
|
||||
import type { types } from 'recast';
|
||||
import { parse, visit, print } from 'recast';
|
||||
|
||||
import { builders as b } from 'ast-types';
|
||||
|
||||
import type { ExpressionKind, StatementKind } from 'ast-types/lib/gen/kinds';
|
||||
import { globalIdentifier, jsVariablePolyfill } from './VariablePolyfill';
|
||||
import type { DataNode } from './VariablePolyfill';
|
||||
import { splitExpression } from './ExpressionSplitter';
|
||||
import type { ExpressionCode, ExpressionText } from './ExpressionSplitter';
|
||||
import { parseWithEsprimaNext } from './Parser';
|
||||
import type { TournamentHooks } from './ast';
|
||||
|
||||
export interface ExpressionAnalysis {
|
||||
has: {
|
||||
function: boolean;
|
||||
templateString: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
const v = b.identifier('v');
|
||||
|
||||
const shouldAlwaysWrapList = ['window', 'global', 'this'];
|
||||
|
||||
const shouldWrapInTry = (node: namedTypes.ASTNode) => {
|
||||
let shouldWrap = false;
|
||||
|
||||
visit(node, {
|
||||
visitMemberExpression() {
|
||||
shouldWrap = true;
|
||||
return false;
|
||||
},
|
||||
visitCallExpression() {
|
||||
shouldWrap = true;
|
||||
return false;
|
||||
},
|
||||
visitIdentifier(path) {
|
||||
if (shouldAlwaysWrapList.includes(path.node.name)) {
|
||||
shouldWrap = true;
|
||||
return false;
|
||||
}
|
||||
this.traverse(path);
|
||||
return;
|
||||
},
|
||||
});
|
||||
|
||||
return shouldWrap;
|
||||
};
|
||||
|
||||
const hasFunction = (node: types.namedTypes.ASTNode) => {
|
||||
let hasFn = false;
|
||||
|
||||
visit(node, {
|
||||
visitFunctionExpression() {
|
||||
hasFn = true;
|
||||
return false;
|
||||
},
|
||||
visitFunctionDeclaration() {
|
||||
hasFn = true;
|
||||
return false;
|
||||
},
|
||||
visitArrowFunctionExpression() {
|
||||
hasFn = true;
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
return hasFn;
|
||||
};
|
||||
|
||||
const hasTemplateString = (node: types.namedTypes.ASTNode) => {
|
||||
let hasTemp = false;
|
||||
|
||||
visit(node, {
|
||||
visitTemplateLiteral(path) {
|
||||
if (path.node.expressions.length) {
|
||||
hasTemp = true;
|
||||
return false;
|
||||
}
|
||||
this.traverse(path);
|
||||
return;
|
||||
},
|
||||
});
|
||||
|
||||
return hasTemp;
|
||||
};
|
||||
|
||||
const wrapInErrorHandler = (node: StatementKind) => {
|
||||
return b.tryStatement(
|
||||
b.blockStatement([node]),
|
||||
b.catchClause(
|
||||
b.identifier('e'),
|
||||
null,
|
||||
b.blockStatement([
|
||||
b.expressionStatement(
|
||||
b.callExpression(b.identifier('E'), [b.identifier('e'), b.thisExpression()]),
|
||||
),
|
||||
]),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const maybeWrapExpr = (expr: string): string => {
|
||||
if (expr.trimStart()[0] === '{') {
|
||||
return '(' + expr + ')';
|
||||
}
|
||||
return expr;
|
||||
};
|
||||
|
||||
const buildFunctionBody = (expr: ExpressionKind) => {
|
||||
return b.blockStatement([
|
||||
// v = (<actual expression>)
|
||||
b.expressionStatement(b.assignmentExpression('=', v, expr)),
|
||||
// Return value or empty string on some falsy values
|
||||
// return v || v === 0 || v === false ? v : 0
|
||||
// The ordering is important for AST nodes to match
|
||||
// tmpl's output
|
||||
b.returnStatement(
|
||||
b.conditionalExpression(
|
||||
b.logicalExpression(
|
||||
'||',
|
||||
b.logicalExpression('||', v, b.binaryExpression('===', v, b.literal(0))),
|
||||
b.binaryExpression('===', v, b.literal(false)),
|
||||
),
|
||||
v,
|
||||
b.literal(''),
|
||||
),
|
||||
),
|
||||
]);
|
||||
};
|
||||
|
||||
type ParsedCode = ExpressionCode & { parsed: types.namedTypes.File };
|
||||
|
||||
// This replaces any actual new lines with \n's. This only happens in
|
||||
// template strings.
|
||||
const fixStringNewLines = (node: types.namedTypes.File): types.namedTypes.File => {
|
||||
const replace = (str: string): string => {
|
||||
return str.replace(/\n/g, '\\n');
|
||||
};
|
||||
visit(node, {
|
||||
visitTemplateElement(path) {
|
||||
this.traverse(path);
|
||||
const el = b.templateElement(
|
||||
{
|
||||
cooked: path.node.value.cooked === null ? null : replace(path.node.value.cooked),
|
||||
raw: replace(path.node.value.raw),
|
||||
},
|
||||
path.node.tail,
|
||||
);
|
||||
path.replace(el);
|
||||
},
|
||||
});
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
export const getParsedExpression = (expr: string): Array<ExpressionText | ParsedCode> => {
|
||||
return splitExpression(expr).map<ExpressionText | ParsedCode>((chunk) => {
|
||||
if (chunk.type === 'code') {
|
||||
const code = maybeWrapExpr(chunk.text);
|
||||
const node = parse(code, {
|
||||
parser: { parse: parseWithEsprimaNext },
|
||||
}) as types.namedTypes.File;
|
||||
|
||||
return { ...chunk, parsed: node };
|
||||
}
|
||||
return chunk;
|
||||
});
|
||||
};
|
||||
|
||||
export const getExpressionCode = (
|
||||
expr: string,
|
||||
dataNodeName: string,
|
||||
hooks: TournamentHooks,
|
||||
): [string, ExpressionAnalysis] => {
|
||||
const chunks = getParsedExpression(expr);
|
||||
|
||||
const newProg = b.program([
|
||||
b.variableDeclaration('var', [b.variableDeclarator(globalIdentifier, b.objectExpression([]))]),
|
||||
]);
|
||||
|
||||
// This is what's used to access that's passed in. For compatibility we us
|
||||
// `this` unless the expression contains a function. If it contains an
|
||||
// expression we instead assign a different variable to hold onto the contents
|
||||
// of `this` (default: `___n8n_data`) since functions aren't compatibility
|
||||
// anyway.
|
||||
let dataNode: DataNode = b.thisExpression();
|
||||
const hasFn = (chunks.filter((c) => c.type === 'code') as ParsedCode[]).some((c) =>
|
||||
hasFunction(c.parsed),
|
||||
);
|
||||
if (hasFn) {
|
||||
dataNode = b.identifier(dataNodeName);
|
||||
newProg.body.push(
|
||||
b.variableDeclaration('var', [b.variableDeclarator(dataNode, b.thisExpression())]),
|
||||
);
|
||||
}
|
||||
|
||||
const hasTempString = (chunks.filter((c) => c.type === 'code') as ParsedCode[]).some((c) =>
|
||||
hasTemplateString(c.parsed),
|
||||
);
|
||||
|
||||
// So for compatibility we parse expressions the same way that `tmpl` does.
|
||||
// This means we always have an initial text chunk but if there's only a blank
|
||||
// text chunk and a code chunk then we want to return the actual value of the
|
||||
// expression, not turn it into a string.
|
||||
if (
|
||||
chunks.length > 2 ||
|
||||
chunks[0].text !== '' ||
|
||||
// This is a blank expression. It should just return an empty string
|
||||
(chunks[0].text === '' && chunks.length === 1)
|
||||
) {
|
||||
let parts: ExpressionKind[] = [];
|
||||
for (const chunk of chunks) {
|
||||
// This is just a text chunks, push it up as a literal.
|
||||
if (chunk.type === 'text') {
|
||||
parts.push(b.literal(chunk.text));
|
||||
// This is a code chunk so do some magic
|
||||
} else {
|
||||
const fixed = fixStringNewLines(chunk.parsed);
|
||||
for (const hook of hooks.before) {
|
||||
hook(fixed, dataNode);
|
||||
}
|
||||
const parsed = jsVariablePolyfill(fixed, dataNode)?.[0];
|
||||
if (!parsed || parsed.type !== 'ExpressionStatement') {
|
||||
throw new SyntaxError('Not a expression statement');
|
||||
}
|
||||
|
||||
for (const hook of hooks.after) {
|
||||
hook(parsed, dataNode);
|
||||
}
|
||||
|
||||
const functionBody = buildFunctionBody(parsed.expression);
|
||||
|
||||
if (shouldWrapInTry(parsed)) {
|
||||
// Wraps the body of our expression function in a try/catch
|
||||
// to match tmpl
|
||||
functionBody.body = [
|
||||
wrapInErrorHandler(functionBody.body[0]),
|
||||
// This is for tmpl compat. It puts a ; after the try/catch
|
||||
// creating an empty statement. emptyStatement is just printed
|
||||
// to nothing so we use an expression statement with a blank
|
||||
// identifier.
|
||||
b.expressionStatement(b.identifier('')),
|
||||
functionBody.body[1],
|
||||
];
|
||||
}
|
||||
|
||||
// Turn our expression into a function call with bound this. The function
|
||||
// it create has a parameter called `v` that we don't actually set. I think
|
||||
// this is a hack around only being able to use `var` and not `let`/`const`.
|
||||
parts.push(
|
||||
b.callExpression(
|
||||
b.memberExpression(b.functionExpression(null, [v], functionBody), b.identifier('call')),
|
||||
[b.thisExpression()],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Just return the raw string if it's just a single string
|
||||
if (chunks.length < 2) {
|
||||
newProg.body.push(b.returnStatement(parts[0]));
|
||||
} else {
|
||||
// Filter out empty string literals for compat
|
||||
parts = parts.filter((i) => !(i.type === 'Literal' && i.value === ''));
|
||||
newProg.body.push(
|
||||
b.returnStatement(
|
||||
b.callExpression(b.memberExpression(b.arrayExpression(parts), b.identifier('join')), [
|
||||
b.literal(''),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const fixed = fixStringNewLines((chunks[1] as ParsedCode).parsed);
|
||||
for (const hook of hooks.before) {
|
||||
hook(fixed, dataNode);
|
||||
}
|
||||
const parsed = jsVariablePolyfill(fixed, dataNode)?.[0];
|
||||
if (!parsed || parsed.type !== 'ExpressionStatement') {
|
||||
throw new SyntaxError('Not a expression statement');
|
||||
}
|
||||
|
||||
for (const hook of hooks.after) {
|
||||
hook(parsed, dataNode);
|
||||
}
|
||||
|
||||
let retData: StatementKind = b.returnStatement(parsed.expression);
|
||||
if (shouldWrapInTry(parsed)) {
|
||||
retData = wrapInErrorHandler(retData);
|
||||
}
|
||||
newProg.body.push(retData);
|
||||
}
|
||||
|
||||
return [print(newProg).code, { has: { function: hasFn, templateString: hasTempString } }];
|
||||
};
|
||||
108
node_modules/@n8n/tournament/src/ExpressionSplitter.ts
generated
vendored
Normal file
108
node_modules/@n8n/tournament/src/ExpressionSplitter.ts
generated
vendored
Normal file
@ -0,0 +1,108 @@
|
||||
export interface ExpressionText {
|
||||
type: 'text';
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface ExpressionCode {
|
||||
type: 'code';
|
||||
text: string;
|
||||
// tmpl has different behaviours if the last expression
|
||||
// doesn't close itself.
|
||||
hasClosingBrackets: boolean;
|
||||
}
|
||||
|
||||
export type ExpressionChunk = ExpressionCode | ExpressionText;
|
||||
|
||||
const OPEN_BRACKET = /(?<brackets>\{\{)/;
|
||||
const CLOSE_BRACKET = /(?<brackets>\}\})/;
|
||||
|
||||
export const escapeCode = (text: string): string => {
|
||||
return text.replace('\\}}', '}}');
|
||||
};
|
||||
|
||||
const normalizeBackslashes = (text: string): string => {
|
||||
return text.replace(/\\\\/g, '\\');
|
||||
};
|
||||
|
||||
export const splitExpression = (expression: string): ExpressionChunk[] => {
|
||||
const chunks: ExpressionChunk[] = [];
|
||||
let searchingFor: 'open' | 'close' = 'open';
|
||||
let activeRegex = OPEN_BRACKET;
|
||||
|
||||
let buffer = '';
|
||||
|
||||
let index = 0;
|
||||
|
||||
while (index < expression.length) {
|
||||
const expr = expression.slice(index);
|
||||
const res = activeRegex.exec(expr);
|
||||
// No more brackets. If it's a closing bracket
|
||||
// this is sort of valid so we accept it but mark
|
||||
// that it has no closing bracket.
|
||||
if (!res?.groups) {
|
||||
buffer += expr;
|
||||
if (searchingFor === 'open') {
|
||||
chunks.push({
|
||||
type: 'text',
|
||||
text: buffer,
|
||||
});
|
||||
} else {
|
||||
chunks.push({
|
||||
type: 'code',
|
||||
text: escapeCode(buffer),
|
||||
hasClosingBrackets: false,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
const beforeMatch = expr.slice(0, res.index);
|
||||
const backslashCount = beforeMatch.match(/\\*$/)?.[0]?.length ?? 0;
|
||||
const isEscaped = backslashCount % 2 === 1;
|
||||
|
||||
if (isEscaped) {
|
||||
buffer += expr.slice(0, res.index + '{{'.length);
|
||||
index += res.index + '{{'.length;
|
||||
} else {
|
||||
buffer += expr.slice(0, res.index);
|
||||
|
||||
if (searchingFor === 'open') {
|
||||
chunks.push({
|
||||
type: 'text',
|
||||
text: normalizeBackslashes(buffer),
|
||||
});
|
||||
searchingFor = 'close';
|
||||
activeRegex = CLOSE_BRACKET;
|
||||
} else {
|
||||
chunks.push({
|
||||
type: 'code',
|
||||
text: escapeCode(buffer),
|
||||
hasClosingBrackets: true,
|
||||
});
|
||||
searchingFor = 'open';
|
||||
activeRegex = OPEN_BRACKET;
|
||||
}
|
||||
|
||||
index += res.index + 2;
|
||||
buffer = '';
|
||||
}
|
||||
}
|
||||
|
||||
return chunks;
|
||||
};
|
||||
|
||||
// Expressions only have closing brackets escaped
|
||||
const escapeTmplExpression = (part: string) => {
|
||||
return part.replace('}}', '\\}}');
|
||||
};
|
||||
|
||||
export const joinExpression = (parts: ExpressionChunk[]): string => {
|
||||
return parts
|
||||
.map((chunk) => {
|
||||
if (chunk.type === 'code') {
|
||||
return `{{${escapeTmplExpression(chunk.text)}${chunk.hasClosingBrackets ? '}}' : ''}`;
|
||||
}
|
||||
return chunk.text;
|
||||
})
|
||||
.join('');
|
||||
};
|
||||
22
node_modules/@n8n/tournament/src/FunctionEvaluator.ts
generated
vendored
Normal file
22
node_modules/@n8n/tournament/src/FunctionEvaluator.ts
generated
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
import type { ExpressionEvaluator, ReturnValue, Tournament } from '.';
|
||||
|
||||
export class FunctionEvaluator implements ExpressionEvaluator {
|
||||
private _codeCache: Record<string, Function> = {};
|
||||
|
||||
constructor(private instance: Tournament) {}
|
||||
|
||||
private getFunction(expr: string): Function {
|
||||
if (expr in this._codeCache) {
|
||||
return this._codeCache[expr];
|
||||
}
|
||||
const [code] = this.instance.getExpressionCode(expr);
|
||||
const func = new Function('E', code + ';');
|
||||
this._codeCache[expr] = func;
|
||||
return func;
|
||||
}
|
||||
|
||||
evaluate(expr: string, data: unknown): ReturnValue {
|
||||
const fn = this.getFunction(expr);
|
||||
return fn.call(data, this.instance.errorHandler);
|
||||
}
|
||||
}
|
||||
23
node_modules/@n8n/tournament/src/Parser.ts
generated
vendored
Normal file
23
node_modules/@n8n/tournament/src/Parser.ts
generated
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
import { parse as esprimaParse } from 'esprima-next';
|
||||
import type { Config as EsprimaConfig } from 'esprima-next';
|
||||
import { getOption } from 'recast/lib/util';
|
||||
|
||||
export function parseWithEsprimaNext(source: string, options?: any): any {
|
||||
try {
|
||||
const ast = esprimaParse(source, {
|
||||
loc: true,
|
||||
locations: true,
|
||||
comment: true,
|
||||
range: getOption(options, 'range', false) as boolean,
|
||||
tolerant: getOption(options, 'tolerant', true) as boolean,
|
||||
tokens: true,
|
||||
jsx: getOption(options, 'jsx', false) as boolean,
|
||||
sourceType: getOption(options, 'sourceType', 'module') as string,
|
||||
} as EsprimaConfig);
|
||||
|
||||
return ast;
|
||||
} catch (error) {
|
||||
if (error instanceof Error) throw new SyntaxError(error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
313
node_modules/@n8n/tournament/src/VariablePolyfill.ts
generated
vendored
Normal file
313
node_modules/@n8n/tournament/src/VariablePolyfill.ts
generated
vendored
Normal file
@ -0,0 +1,313 @@
|
||||
import type { types } from 'recast';
|
||||
import { visit } from 'recast';
|
||||
import type { StatementKind, VariableDeclaratorKind } from 'ast-types/lib/gen/kinds';
|
||||
import type { NodePath } from 'ast-types/lib/node-path';
|
||||
import type { namedTypes } from 'ast-types';
|
||||
import { builders as b } from 'ast-types';
|
||||
import type { ParentKind } from './Constants';
|
||||
import { EXEMPT_IDENTIFIER_LIST } from './Constants';
|
||||
|
||||
function assertNever(value: never): value is never {
|
||||
return true;
|
||||
}
|
||||
|
||||
export const globalIdentifier = b.identifier(
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
typeof window !== 'object' ? 'global' : 'window',
|
||||
);
|
||||
|
||||
const buildGlobalSwitch = (node: types.namedTypes.Identifier, dataNode: DataNode) => {
|
||||
return b.memberExpression(
|
||||
b.conditionalExpression(
|
||||
b.binaryExpression('in', b.literal(node.name), dataNode),
|
||||
dataNode,
|
||||
globalIdentifier,
|
||||
),
|
||||
b.identifier(node.name),
|
||||
);
|
||||
};
|
||||
|
||||
const isInScope = (path: NodePath<types.namedTypes.Identifier>) => {
|
||||
let scope = path.scope;
|
||||
while (scope !== null) {
|
||||
if (scope.declares(path.node.name)) {
|
||||
return true;
|
||||
}
|
||||
scope = scope.parent;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const polyfillExceptions = ['this', 'window', 'global'];
|
||||
|
||||
const polyfillVar = (
|
||||
path: NodePath<types.namedTypes.Identifier>,
|
||||
dataNode: DataNode,
|
||||
force: boolean = false,
|
||||
) => {
|
||||
if (!force) {
|
||||
if (isInScope(path)) {
|
||||
// console.log('In scope', path.node.name);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// For tmpl compat we ignore these identifiers
|
||||
if (polyfillExceptions.includes(path.node.name)) {
|
||||
return;
|
||||
}
|
||||
path.replace(buildGlobalSwitch(path.node, dataNode));
|
||||
};
|
||||
|
||||
export type DataNode = namedTypes.ThisExpression | namedTypes.Identifier;
|
||||
|
||||
type CustomPatcher = (
|
||||
path: NodePath<types.namedTypes.Identifier>,
|
||||
parent: any,
|
||||
dataNode: DataNode,
|
||||
) => void;
|
||||
|
||||
const customPatches: Partial<Record<ParentKind['type'], CustomPatcher>> = {
|
||||
MemberExpression(path, parent: namedTypes.MemberExpression, dataNode) {
|
||||
if (parent.object === path.node || parent.computed) {
|
||||
polyfillVar(path, dataNode);
|
||||
}
|
||||
},
|
||||
OptionalMemberExpression(path, parent: namedTypes.OptionalMemberExpression, dataNode) {
|
||||
if (parent.object === path.node) {
|
||||
polyfillVar(path, dataNode);
|
||||
}
|
||||
},
|
||||
Property(path, parent: namedTypes.Property, dataNode) {
|
||||
if (path.node !== parent.value) {
|
||||
return;
|
||||
}
|
||||
const objPattern: namedTypes.ObjectPattern = path.parent?.parent?.node;
|
||||
if (!objPattern) {
|
||||
return;
|
||||
}
|
||||
const objParent: VariableDeclaratorKind = path.parent.parent.parent?.node;
|
||||
if (!objParent) {
|
||||
return;
|
||||
}
|
||||
if (objParent.type === 'VariableDeclarator' && objParent.id === objPattern) {
|
||||
return;
|
||||
}
|
||||
|
||||
parent.shorthand = false;
|
||||
polyfillVar(path, dataNode);
|
||||
},
|
||||
AssignmentPattern(path, parent: namedTypes.AssignmentPattern, dataNode) {
|
||||
if (parent.right === path.node) {
|
||||
polyfillVar(path, dataNode);
|
||||
}
|
||||
},
|
||||
VariableDeclarator(path, parent: namedTypes.VariableDeclarator, dataNode) {
|
||||
if (parent.init === path.node) {
|
||||
polyfillVar(path, dataNode);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const jsVariablePolyfill = (
|
||||
ast: types.namedTypes.File,
|
||||
dataNode: DataNode,
|
||||
): StatementKind[] | undefined => {
|
||||
visit(ast, {
|
||||
visitImportExpression(_path) {
|
||||
throw new Error('Imports are not supported');
|
||||
},
|
||||
visitIdentifier(path) {
|
||||
this.traverse(path);
|
||||
const parent: ParentKind = path.parent.node;
|
||||
|
||||
// This is for tmpl compat
|
||||
if (EXEMPT_IDENTIFIER_LIST.includes(path.node.name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (parent.type) {
|
||||
case 'AssignmentPattern':
|
||||
case 'Property':
|
||||
case 'MemberExpression':
|
||||
case 'OptionalMemberExpression':
|
||||
case 'VariableDeclarator':
|
||||
if (!customPatches[parent.type]) {
|
||||
throw new Error(`Couldn\'t find custom patcher for parent type: ${parent.type}`);
|
||||
}
|
||||
customPatches[parent.type]!(path, parent, dataNode);
|
||||
break;
|
||||
case 'BinaryExpression':
|
||||
case 'UnaryExpression':
|
||||
case 'ArrayExpression':
|
||||
case 'AssignmentExpression':
|
||||
case 'SequenceExpression':
|
||||
case 'YieldExpression':
|
||||
case 'UpdateExpression':
|
||||
case 'LogicalExpression':
|
||||
case 'ConditionalExpression':
|
||||
case 'NewExpression':
|
||||
case 'CallExpression':
|
||||
case 'OptionalCallExpression':
|
||||
case 'TaggedTemplateExpression':
|
||||
case 'TemplateLiteral':
|
||||
case 'AwaitExpression':
|
||||
case 'ImportExpression':
|
||||
case 'ForStatement':
|
||||
case 'IfStatement':
|
||||
case 'WhileStatement':
|
||||
case 'ForInStatement':
|
||||
case 'ForOfStatement':
|
||||
case 'SwitchStatement':
|
||||
case 'ReturnStatement':
|
||||
case 'DoWhileStatement':
|
||||
case 'ExpressionStatement':
|
||||
case 'ForAwaitStatement':
|
||||
case 'ThrowStatement':
|
||||
case 'WithStatement':
|
||||
case 'TupleExpression':
|
||||
polyfillVar(path, dataNode);
|
||||
break;
|
||||
|
||||
// Do nothing
|
||||
case 'Super':
|
||||
case 'Identifier':
|
||||
case 'ArrowFunctionExpression':
|
||||
case 'FunctionDeclaration':
|
||||
case 'FunctionExpression':
|
||||
case 'ThisExpression':
|
||||
case 'ObjectExpression':
|
||||
case 'MetaProperty':
|
||||
case 'ChainExpression':
|
||||
case 'PrivateName':
|
||||
case 'ParenthesizedExpression':
|
||||
case 'Import':
|
||||
case 'VariableDeclaration':
|
||||
case 'CatchClause':
|
||||
case 'BlockStatement':
|
||||
case 'TryStatement':
|
||||
case 'EmptyStatement':
|
||||
case 'LabeledStatement':
|
||||
case 'BreakStatement':
|
||||
case 'ContinueStatement':
|
||||
case 'DebuggerStatement':
|
||||
case 'ImportDeclaration':
|
||||
case 'ExportDeclaration':
|
||||
case 'ExportAllDeclaration':
|
||||
case 'ExportDefaultDeclaration':
|
||||
case 'Noop':
|
||||
case 'ClassMethod':
|
||||
case 'ClassPrivateMethod':
|
||||
case 'RestElement':
|
||||
case 'ArrayPattern':
|
||||
case 'ObjectPattern':
|
||||
case 'ClassExpression':
|
||||
case 'RecordExpression':
|
||||
case 'V8IntrinsicIdentifier':
|
||||
case 'TopicReference':
|
||||
case 'MethodDefinition':
|
||||
case 'ClassDeclaration':
|
||||
case 'ClassProperty':
|
||||
case 'StaticBlock':
|
||||
case 'ClassBody':
|
||||
case 'ExportNamedDeclaration':
|
||||
case 'ClassPrivateProperty':
|
||||
case 'ClassAccessorProperty':
|
||||
case 'PropertyPattern':
|
||||
break;
|
||||
|
||||
// I can't seem to figure out what causes these
|
||||
case 'SpreadElementPattern':
|
||||
case 'SpreadPropertyPattern':
|
||||
case 'ClassPropertyDefinition':
|
||||
break;
|
||||
|
||||
// Flow types
|
||||
case 'DeclareClass':
|
||||
case 'DeclareModule':
|
||||
case 'DeclareVariable':
|
||||
case 'DeclareFunction':
|
||||
case 'DeclareInterface':
|
||||
case 'DeclareTypeAlias':
|
||||
case 'DeclareOpaqueType':
|
||||
case 'DeclareModuleExports':
|
||||
case 'DeclareExportDeclaration':
|
||||
case 'DeclareExportAllDeclaration':
|
||||
case 'InterfaceDeclaration':
|
||||
case 'TypeAlias':
|
||||
case 'OpaqueType':
|
||||
case 'EnumDeclaration':
|
||||
case 'TypeCastExpression':
|
||||
break;
|
||||
|
||||
// Typescript types
|
||||
case 'TSAsExpression':
|
||||
case 'TSTypeParameter':
|
||||
case 'TSTypeAssertion':
|
||||
case 'TSDeclareMethod':
|
||||
case 'TSIndexSignature':
|
||||
case 'TSDeclareFunction':
|
||||
case 'TSMethodSignature':
|
||||
case 'TSEnumDeclaration':
|
||||
case 'TSExportAssignment':
|
||||
case 'TSNonNullExpression':
|
||||
case 'TSPropertySignature':
|
||||
case 'TSModuleDeclaration':
|
||||
case 'TSParameterProperty':
|
||||
case 'TSTypeCastExpression':
|
||||
case 'TSSatisfiesExpression':
|
||||
case 'TSTypeAliasDeclaration':
|
||||
case 'TSInterfaceDeclaration':
|
||||
case 'TSImportEqualsDeclaration':
|
||||
case 'TSExternalModuleReference':
|
||||
case 'TSInstantiationExpression':
|
||||
case 'TSTypeParameterDeclaration':
|
||||
case 'TSCallSignatureDeclaration':
|
||||
case 'TSNamespaceExportDeclaration':
|
||||
case 'TSConstructSignatureDeclaration':
|
||||
break;
|
||||
|
||||
// Literals that can't contain an identifier
|
||||
case 'DirectiveLiteral':
|
||||
case 'StringLiteral':
|
||||
case 'NumericLiteral':
|
||||
case 'BigIntLiteral':
|
||||
case 'NullLiteral':
|
||||
case 'Literal':
|
||||
case 'RegExpLiteral':
|
||||
case 'BooleanLiteral':
|
||||
case 'DecimalLiteral':
|
||||
break;
|
||||
|
||||
// Proposals that are stage 0 or 1
|
||||
case 'DoExpression':
|
||||
case 'BindExpression':
|
||||
break;
|
||||
|
||||
// JSX stuff. We don't support this so just do nothing.
|
||||
case 'JSXIdentifier':
|
||||
case 'JSXText':
|
||||
case 'JSXElement':
|
||||
case 'JSXFragment':
|
||||
case 'JSXMemberExpression':
|
||||
case 'JSXExpressionContainer':
|
||||
break;
|
||||
|
||||
// I _think_ these are obsolete features proposed as part of ECMAScript 7.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Deprecated_and_obsolete_features#legacy_generator_and_iterator
|
||||
case 'ComprehensionExpression':
|
||||
case 'GeneratorExpression':
|
||||
polyfillVar(path, dataNode);
|
||||
break;
|
||||
|
||||
default:
|
||||
// This is a simple type guard that guarantees we haven't missed
|
||||
// a case. It'll result in a type error at compile time.
|
||||
assertNever(parent);
|
||||
break;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return ast.program.body as StatementKind[];
|
||||
};
|
||||
21
node_modules/@n8n/tournament/src/ast.ts
generated
vendored
Normal file
21
node_modules/@n8n/tournament/src/ast.ts
generated
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
export type { types as astTypes } from 'recast';
|
||||
export { visit as astVisit } from 'recast';
|
||||
export { builders as astBuilders, type namedTypes as astNamedTypes } from 'ast-types';
|
||||
|
||||
import type { types } from 'recast';
|
||||
import type { namedTypes } from 'ast-types';
|
||||
|
||||
export interface TournamentHooks {
|
||||
before: ASTBeforeHook[];
|
||||
after: ASTAfterHook[];
|
||||
}
|
||||
|
||||
export type ASTAfterHook = (
|
||||
ast: namedTypes.ExpressionStatement,
|
||||
dataNode: namedTypes.ThisExpression | namedTypes.Identifier,
|
||||
) => void;
|
||||
|
||||
export type ASTBeforeHook = (
|
||||
ast: types.namedTypes.File,
|
||||
dataNode: namedTypes.ThisExpression | namedTypes.Identifier,
|
||||
) => void;
|
||||
47
node_modules/@n8n/tournament/src/index.ts
generated
vendored
Normal file
47
node_modules/@n8n/tournament/src/index.ts
generated
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
import { getExpressionCode } from './ExpressionBuilder';
|
||||
import type { ExpressionAnalysis } from './ExpressionBuilder';
|
||||
import { getTmplDifference } from './Analysis';
|
||||
import type { ExpressionEvaluator, ExpressionEvaluatorClass } from './Evaluator';
|
||||
import { FunctionEvaluator } from './FunctionEvaluator';
|
||||
import type { TournamentHooks } from './ast';
|
||||
|
||||
export type { TmplDifference } from './Analysis';
|
||||
export type { ExpressionEvaluator, ExpressionEvaluatorClass } from './Evaluator';
|
||||
export * from './ast';
|
||||
|
||||
const DATA_NODE_NAME = '___n8n_data';
|
||||
export type ReturnValue = string | null | (() => unknown);
|
||||
|
||||
export class Tournament {
|
||||
private evaluator!: ExpressionEvaluator;
|
||||
|
||||
constructor(
|
||||
public errorHandler: (error: Error) => void = () => {},
|
||||
private _dataNodeName: string = DATA_NODE_NAME,
|
||||
Evaluator: ExpressionEvaluatorClass = FunctionEvaluator,
|
||||
private readonly astHooks: TournamentHooks = { before: [], after: [] },
|
||||
) {
|
||||
this.setEvaluator(Evaluator);
|
||||
}
|
||||
|
||||
setEvaluator(Evaluator: ExpressionEvaluatorClass) {
|
||||
this.evaluator = new Evaluator(this);
|
||||
}
|
||||
|
||||
getExpressionCode(expr: string): [string, ExpressionAnalysis] {
|
||||
return getExpressionCode(expr, this._dataNodeName, this.astHooks);
|
||||
}
|
||||
|
||||
tmplDiff(expr: string) {
|
||||
return getTmplDifference(expr, this._dataNodeName);
|
||||
}
|
||||
|
||||
execute(expr: string, data: unknown): ReturnValue {
|
||||
// This is to match tmpl. This will only really happen if
|
||||
// an empty expression is passed in.
|
||||
if (!expr) {
|
||||
return expr;
|
||||
}
|
||||
return this.evaluator.evaluate(expr, data);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user