fix(core): refactor evaluate to support dash in key path (#3517)

* fix(core): refactor evaluate to support dash in key path

* fix(core): fix evaluate expression from date variable

* fix(plugin-workflow): fix test case

* fix(client): fix pre-replace logic
This commit is contained in:
Junyi 2024-02-19 17:07:19 +08:00 committed by GitHub
parent 0d8604ee2c
commit c02e759830
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 69 additions and 63 deletions

View File

@ -162,7 +162,7 @@ async function replaceVariables(
return { return {
exp: value.replace(REGEX_OF_VARIABLE, (match) => { exp: value.replace(REGEX_OF_VARIABLE, (match) => {
return store[match] || match; return `{{${store[match] || match}}}`;
}), }),
scope, scope,
}; };

View File

@ -18,6 +18,15 @@ describe('evaluate', () => {
expect(result).toBe("I'm done."); expect(result).toBe("I'm done.");
}); });
it('function result null', () => {
const result = formulaEval('{{a}}', {
a() {
return null;
},
});
expect(result).toBe(null);
});
it('function result number', () => { it('function result number', () => {
const result = formulaEval('{{a}}', { const result = formulaEval('{{a}}', {
a() { a() {
@ -28,16 +37,29 @@ describe('evaluate', () => {
}); });
it('function result Date', () => { it('function result Date', () => {
const now = new Date();
const result = formulaEval('{{a}}', { const result = formulaEval('{{a}}', {
a() { a() {
return new Date(); return now;
}, },
}); });
expect(result).toMatch(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,3})?Z/); expect(result).toBeInstanceOf(Date);
expect(result).toBe(now);
}); });
it('number path to array item 0 (math.js)', () => { it('deep array', () => {
expect(() => mathEval('{{a.0}}', { a: [1, 2, 3] })).toThrow(); const result = formulaEval('{{a.b}}', { a: [{ b: 1 }, { b: 2 }] });
expect(result).toEqual([1, 2]);
});
it('key with dash', () => {
const result = formulaEval('{{a-b}}', { 'a-b': 1 });
expect(result).toBe(1);
});
it('deep key with dash', () => {
const result = formulaEval('{{a.b-c}}', { a: { 'b-c': 1 } });
expect(result).toBe(1);
}); });
it('number path to array item 1 (math.js)', () => { it('number path to array item 1 (math.js)', () => {
@ -45,7 +67,12 @@ describe('evaluate', () => {
expect(result).toBe(1); expect(result).toBe(1);
}); });
it('number path to array item 1 (math.js)', () => { // NOTE: This case is skipped because `a.<number>` is not able to be configured from UI.
it.skip('number path to array item 0 (math.js)', () => {
expect(() => mathEval('{{a.0}}', { a: [1, 2, 3] })).toThrow();
});
it.skip('number path to array item 1 (math.js)', () => {
const result = mathEval('{{a.1}}', { a: [1, 2, 3] }); const result = mathEval('{{a.1}}', { a: [1, 2, 3] });
expect(result).toBe(1); expect(result).toBe(1);
}); });

View File

@ -17,49 +17,25 @@ export function appendArrayColumn(scope, key) {
} }
} }
function replaceNumberIndex(path: string, scope: Scope): string {
const segments = path.split('.');
const paths: string[] = [];
for (let i = 0; i < segments.length; i++) {
const p = segments[i];
if (p[0] && '0123456789'.indexOf(p[0]) > -1) {
paths.push(Array.isArray(get(scope, segments.slice(0, i))) ? `[${p}]` : `["${p}"]`);
} else {
if (i) {
paths.push('.', p);
} else {
paths.push(p);
}
}
}
return paths.join('');
}
export function evaluate(this: Evaluator, expression: string, scope: Scope = {}) { export function evaluate(this: Evaluator, expression: string, scope: Scope = {}) {
const context = cloneDeep(scope); const context = cloneDeep(scope);
const newContext = {};
const exp = expression.trim().replace(/{{\s*([^{}]+)\s*}}/g, (_, v) => { const exp = expression.trim().replace(/{{\s*([^{}]+)\s*}}/g, (_, v) => {
appendArrayColumn(context, v); appendArrayColumn(context, v);
const item = get(context, v); let item = get(context, v);
let result; if (typeof item === 'function') {
item = item();
}
const randomKey = `$$${Math.random().toString(36).slice(2, 10).padEnd(8, '0')}`;
if (item == null) { if (item == null) {
result = 'null'; return 'null';
} else if (typeof item === 'function') {
result = item();
result = typeof result === 'string' ? `'${result.replace(/'/g, "\\'")}'` : result;
} else {
result = replaceNumberIndex(v, context);
} }
newContext[randomKey] = item;
if (result instanceof Date) { return ` ${randomKey} `;
result = `'${result.toISOString()}'`;
}
return ` ${result} `;
}); });
return this(exp, context);
return this(exp, newContext);
} }

View File

@ -69,7 +69,10 @@ export class FormulaField extends Field {
}; };
updateFieldData = async (instance, { transaction }) => { updateFieldData = async (instance, { transaction }) => {
if (this.collection.name === instance.collectionName && instance.name === this.options.name) { if (this.collection.name !== instance.collectionName || instance.name !== this.options.name) {
return;
}
this.options = Object.assign(this.options, instance.options); this.options = Object.assign(this.options, instance.options);
const { name } = this.options; const { name } = this.options;
@ -92,7 +95,6 @@ export class FormulaField extends Field {
}, },
); );
} }
}
}; };
bind() { bind() {

View File

@ -166,7 +166,8 @@ describe('workflow > instructions > calculation', () => {
const [execution] = await workflow.getExecutions(); const [execution] = await workflow.getExecutions();
const [job] = await execution.getJobs(); const [job] = await execution.getJobs();
expect(job.result).toBe('a $context.data.title '); expect((job.result as string).startsWith('a $$')).toBe(true);
expect((job.result as string).endsWith(' ')).toBe(true);
}); });
it('text', async () => { it('text', async () => {