Files
Automata/packages/editor-ui/src/components/CodeNodeEditor/completions/jsonField.completions.ts
2024-05-28 16:58:44 +02:00

300 lines
9.2 KiB
TypeScript

import { escape } from '../utils';
import type { Completion, CompletionContext, CompletionResult } from '@codemirror/autocomplete';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useNDVStore } from '@/stores/ndv.store';
import { isAllowedInDotNotation } from '@/plugins/codemirror/completions/utils';
import { useI18n } from '@/composables/useI18n';
import type { IPinData, IRunData, IDataObject } from 'n8n-workflow';
function useJsonFieldCompletions() {
const i18n = useI18n();
const ndvStore = useNDVStore();
const workflowsStore = useWorkflowsStore();
/**
* - Complete `x.first().json.` to `.field`.
* - Complete `x.last().json.` to `.field`.
* - Complete `x.all()[index].json.` to `.field`.
* - Complete `x.item.json.` to `.field`.
*
* - Complete `x.first().json[` to `['field']`.
* - Complete `x.last().json[` to `['field']`.
* - Complete `x.all()[index].json[` to `['field']`.
* - Complete `x.item.json[` to `['field']`.
*/
const matcherJsonFieldCompletions = (
context: CompletionContext,
matcher: string,
variablesToValues: Record<string, string>,
): CompletionResult | null => {
const pattern = new RegExp(`(${escape(matcher)})\..*`);
const preCursor = context.matchBefore(pattern);
if (!preCursor || (preCursor.from === preCursor.to && !context.explicit)) return null;
const inputNodeName = getInputNodeName();
if (!inputNodeName) return null;
const [varName] = preCursor.text.split('.');
const originalValue = variablesToValues[varName];
if (!originalValue) return null;
for (const accessor of ['first', 'last', 'item']) {
if (originalValue.includes(accessor) || preCursor.text.includes(accessor)) {
const jsonOutput = getJsonOutput(inputNodeName, { accessor });
if (!jsonOutput) return null;
return toJsonFieldCompletions(preCursor, jsonOutput, matcher);
}
}
if (originalValue.includes('all')) {
const match = originalValue.match(/\$(input|\(.*\))\.all\(\)\[(?<index>.+)\]$/);
if (!match?.groups?.index) return null;
const { index } = match.groups;
const jsonOutput = getJsonOutput(inputNodeName, { index: Number(index) });
if (!jsonOutput) return null;
return toJsonFieldCompletions(preCursor, jsonOutput, matcher);
}
return null;
};
/**
* - Complete `$input.first().json.` to `.field`.
* - Complete `$input.last().json.` to `.field`.
* - Complete `$input.all()[index].json.` to `.field`.
* - Complete `$input.item.json.` to `.field`.
*
* - Complete `$input.first().json[` to `['field']`.
* - Complete `$input.last().json[` to `['field']`.
* - Complete `$input.all()[index].json[` to `['field']`.
* - Complete `$input.item.json[` to `['field']`.
*/
const inputJsonFieldCompletions = (context: CompletionContext): CompletionResult | null => {
const patterns = {
first: /\$input\.first\(\)\.json(\[|\.).*/,
last: /\$input\.last\(\)\.json(\[|\.).*/,
item: /\$input\.item\.json(\[|\.).*/,
all: /\$input\.all\(\)\[(?<index>\w+)\]\.json(\[|\.).*/,
};
for (const [name, regex] of Object.entries(patterns)) {
const preCursor = context.matchBefore(regex);
if (!preCursor || (preCursor.from === preCursor.to && !context.explicit)) continue;
const inputNodeName = getInputNodeName();
if (!inputNodeName) continue;
if (name === 'first' || name === 'last') {
const jsonOutput = getJsonOutput(inputNodeName, { accessor: name });
if (!jsonOutput) continue;
return toJsonFieldCompletions(preCursor, jsonOutput, `$input.${name}().json`);
}
if (name === 'item') {
const jsonOutput = getJsonOutput(inputNodeName, { accessor: 'item' });
if (!jsonOutput) continue;
return toJsonFieldCompletions(preCursor, jsonOutput, '$input.item.json');
}
if (name === 'all') {
const match = preCursor.text.match(regex);
if (!match?.groups?.index) continue;
const { index } = match.groups;
const jsonOutput = getJsonOutput(inputNodeName, { index: Number(index) });
if (!jsonOutput) continue;
return toJsonFieldCompletions(preCursor, jsonOutput, `$input.all()[${index}].json`);
}
}
return null;
};
/**
* Complete `$('nodeName').first().json.` to `.field`.
* Complete `$('nodeName').last().json.` to `.field`.
* Complete `$('nodeName').all()[index].json.` to `.field`.
* Complete `$('nodeName').item.json.` to `.field`.
*
* Complete `$('nodeName').first().json[` to `['field']`.
* Complete `$('nodeName').last().json[` to `['field']`.
* Complete `$('nodeName').all()[index].json[` to `['field']`.
* Complete `$('nodeName').item.json[` to `['field']`.
*/
const selectorJsonFieldCompletions = (context: CompletionContext): CompletionResult | null => {
const patterns = {
first: /\$\((?<quotedNodeName>['"][\w\s]+['"])\)\.first\(\)\.json(\[|\.).*/,
last: /\$\((?<quotedNodeName>['"][\w\s]+['"])\)\.last\(\)\.json(\[|\.).*/,
item: /\$\((?<quotedNodeName>['"][\w\s]+['"])\)\.item\.json(\[|\.).*/,
all: /\$\((?<quotedNodeName>['"][\w\s]+['"])\)\.all\(\)\[(?<index>\w+)\]\.json(\[|\.).*/,
};
for (const [name, regex] of Object.entries(patterns)) {
const preCursor = context.matchBefore(regex);
if (!preCursor || (preCursor.from === preCursor.to && !context.explicit)) continue;
const match = preCursor.text.match(regex);
if (!match?.groups?.quotedNodeName) continue;
const { quotedNodeName } = match.groups;
const selector = `$(${match.groups.quotedNodeName})`;
if (name === 'first' || name === 'last') {
const jsonOutput = getJsonOutput(quotedNodeName, { accessor: name });
if (!jsonOutput) continue;
return toJsonFieldCompletions(preCursor, jsonOutput, `${selector}.${name}().json`);
}
if (name === 'item') {
const jsonOutput = getJsonOutput(quotedNodeName, { accessor: 'item' });
if (!jsonOutput) continue;
return toJsonFieldCompletions(preCursor, jsonOutput, `${selector}.item.json`);
}
if (name === 'all') {
const regexMatch = preCursor.text.match(regex);
if (!regexMatch?.groups?.index) continue;
const { index } = regexMatch.groups;
const jsonOutput = getJsonOutput(quotedNodeName, { index: Number(index) });
if (!jsonOutput) continue;
return toJsonFieldCompletions(preCursor, jsonOutput, `${selector}.all()[${index}].json`);
}
}
return null;
};
const getInputNodeName = (): string | null => {
try {
const activeNode = ndvStore.activeNode;
if (activeNode) {
const workflow = workflowsStore.getCurrentWorkflow();
const input = workflow.connectionsByDestinationNode[activeNode.name];
return input.main[0][0].node;
}
} catch (e) {
console.error(e);
return null;
}
return null;
};
/**
* .json -> .json['field']
* .json -> .json.field
*/
const toJsonFieldCompletions = (
preCursor: NonNullable<ReturnType<CompletionContext['matchBefore']>>,
jsonOutput: IDataObject,
matcher: string,
): CompletionResult | null => {
if (
/\.json\[/.test(preCursor.text) ||
new RegExp(`(${escape(matcher)})\\[`).test(preCursor.text)
) {
const options: Completion[] = Object.keys(jsonOutput)
.map((field) => `${matcher}['${field}']`)
.map((label) => ({
label,
info: i18n.baseText('codeNodeEditor.completer.json'),
}));
return {
from: preCursor.from,
options,
};
}
if (
/\.json\./.test(preCursor.text) ||
new RegExp(`(${escape(matcher)})\.`).test(preCursor.text)
) {
const options: Completion[] = Object.keys(jsonOutput)
.filter(isAllowedInDotNotation)
.map((field) => `${matcher}.${field}`)
.map((label) => ({
label,
info: i18n.baseText('codeNodeEditor.completer.json'),
}));
return {
from: preCursor.from,
options,
};
}
return null;
};
/**
* Get the `json` output of a node from `runData` or `pinData`.
*
* `accessor` is the method or property used to find the item index.
* `index` is only passed for `all()`.
*/
const getJsonOutput = (
quotedNodeName: string,
options?: { accessor?: string; index?: number },
) => {
let nodeName = quotedNodeName;
const isSingleQuoteWrapped = quotedNodeName.startsWith("'") && quotedNodeName.endsWith("'");
const isDoubleQuoteWrapped = quotedNodeName.startsWith('"') && quotedNodeName.endsWith('"');
if (isSingleQuoteWrapped) {
nodeName = quotedNodeName.replace(/^'/, '').replace(/'$/, '');
} else if (isDoubleQuoteWrapped) {
nodeName = quotedNodeName.replace(/^"/, '').replace(/"$/, '');
}
const pinData: IPinData | undefined = useWorkflowsStore().pinnedWorkflowData;
const nodePinData = pinData?.[nodeName];
if (nodePinData) {
try {
let itemIndex = options?.index ?? 0;
if (options?.accessor === 'last') {
itemIndex = nodePinData.length - 1;
}
return nodePinData[itemIndex].json;
} catch {}
}
const runData: IRunData | null = useWorkflowsStore().getWorkflowRunData;
const nodeRunData = runData?.[nodeName];
if (!nodeRunData) return null;
try {
let itemIndex = options?.index ?? 0;
if (options?.accessor === 'last') {
const inputItems = nodeRunData[0].data?.main[0] ?? [];
itemIndex = inputItems.length - 1;
}
return nodeRunData[0].data?.main[0]?.[itemIndex].json;
} catch {
return null;
}
};
return {
matcherJsonFieldCompletions,
inputJsonFieldCompletions,
selectorJsonFieldCompletions,
};
}
export { useJsonFieldCompletions };