feat: update ctx-func tests and parser to support async context functions

This commit is contained in:
Sheldon Guo 2025-03-14 09:41:18 +08:00
parent 4c92a8a788
commit 319c7614c0
3 changed files with 52 additions and 34 deletions

View File

@ -18,10 +18,11 @@ describe('ctx function', () => {
const variable = extractTemplateVariable(template); const variable = extractTemplateVariable(template);
expect(variable).toBe(null); expect(variable).toBe(null);
}); });
it('should handle basic context function with state', () => {
it('should handle basic context function with state', async () => {
const template = '{{$user.id}} - {{$user.name}}'; const template = '{{$user.id}} - {{$user.name}}';
const data = { const data = {
$user({ fields, context }) { async $user({ fields, context }) {
if (context.state.userId) { if (context.state.userId) {
return (field) => 1; return (field) => 1;
} else return (field) => 2; } else return (field) => 2;
@ -30,11 +31,11 @@ describe('ctx function', () => {
userId: 1, userId: 1,
}, },
}; };
const result = parser.render(template, data); const result = await parser.render(template, data);
expect(result).toEqual('1 - 1'); expect(result).toEqual('1 - 1');
}); });
it('should handle context function without state', () => { it('should handle context function without state', async () => {
const template = '{{$user.id}} - {{$user.name}}'; const template = '{{$user.id}} - {{$user.name}}';
const data = { const data = {
$user({ fields, context }) { $user({ fields, context }) {
@ -42,11 +43,11 @@ describe('ctx function', () => {
}, },
state: {}, state: {},
}; };
const result = parser.render(template, data); const result = await parser.render(template, data);
expect(result).toEqual('2 - 2'); expect(result).toEqual('2 - 2');
}); });
it('should handle nested context values', () => { it('should handle nested context values', async () => {
const template = '{{$user.profile.email}} - {{$user.profile.address.city}}'; const template = '{{$user.profile.email}} - {{$user.profile.address.city}}';
const data = { const data = {
$user({ fields, context }) { $user({ fields, context }) {
@ -59,11 +60,11 @@ describe('ctx function', () => {
}; };
}, },
}; };
const result = parser.render(template, data); const result = await parser.render(template, data);
expect(result).toEqual('test@example.com - New York'); expect(result).toEqual('test@example.com - New York');
}); });
it('should handle multiple context functions', () => { it('should handle multiple context functions', async () => {
const template = '{{$user.name}} works at {{$company.name}}'; const template = '{{$user.name}} works at {{$company.name}}';
const data = { const data = {
$user({ fields, context }) { $user({ fields, context }) {
@ -73,22 +74,22 @@ describe('ctx function', () => {
return (field) => 'NocoBase'; return (field) => 'NocoBase';
}, },
}; };
const result = parser.render(template, data); const result = await parser.render(template, data);
expect(result).toEqual('John works at NocoBase'); expect(result).toEqual('John works at NocoBase');
}); });
it('should handle undefined context values', () => { it('should handle undefined context values', async () => {
const template = '{{$user.nonexistent}}'; const template = '{{$user.nonexistent}}';
const data = { const data = {
$user({ fields, context }) { $user({ fields, context }) {
return (field) => undefined; return (field) => undefined;
}, },
}; };
const result = parser.render(template, data); const result = await parser.render(template, data);
expect(result).toBeUndefined(); expect(result).toBeUndefined();
}); });
it('should handle context function with array values', () => { it('should handle context function with array values', async () => {
const template = '{{$user.roles[0]}} and {{$user.roles.1}}'; const template = '{{$user.roles[0]}} and {{$user.roles.1}}';
const data = { const data = {
$user({ fields, context }) { $user({ fields, context }) {
@ -99,11 +100,11 @@ describe('ctx function', () => {
}; };
}, },
}; };
const result = parser.render(template, data); const result = await parser.render(template, data);
expect(result).toEqual('admin and user'); expect(result).toEqual('admin and user');
}); });
it('should escape array', () => { it('should escape array', async () => {
const template = ' {{$user.id}} - {{$user.name}} '; const template = ' {{$user.id}} - {{$user.name}} ';
const data = { const data = {
@ -116,7 +117,7 @@ describe('ctx function', () => {
userId: 1, userId: 1,
}, },
}; };
const result = parser.render(template, data); const result = await parser.render(template, data);
expect(result).toEqual(' 1 - 1 '); expect(result).toEqual(' 1 - 1 ');
}); });
}); });

View File

@ -64,18 +64,31 @@ export class JSONTemplateParser {
this._engine.registerFilter(filter.name, filter.handler); this._engine.registerFilter(filter.name, filter.handler);
} }
render(template: string, data: any = {}): any { async render(template: string, data: any = {}): Promise<any> {
const NamespaceMap = new Map<string, { fields: Set<String>; cachedResult: Function }>(); const NamespaceMap = new Map<string, { fields: Set<String>; fnWrapper: Function; fn?: any }>();
Object.keys(data).forEach((key) => { Object.keys(data).forEach((key) => {
if (key.startsWith('$') || typeof data[key] === 'function') { if (key.startsWith('$') || typeof data[key] === 'function') {
NamespaceMap.set(escape(key), { fields: new Set<String>(), cachedResult: null }); NamespaceMap.set(escape(key), { fields: new Set<String>(), fnWrapper: data[key] });
} }
}); });
const parsed = this.parse(template, { NamespaceMap }); const parsed = this.parse(template, { NamespaceMap });
const fnPromises = Array.from(NamespaceMap.entries()).map(async ([key, { fields, fnWrapper }]) => {
const fn = await fnWrapper({ fields: Array.from(fields), context: data });
return { key, fn };
});
const fns = await Promise.all(fnPromises);
fns.forEach(({ key, fn }) => {
const NS = NamespaceMap.get(key);
NS.fn = fn;
});
return parsed(data); return parsed(data);
} }
parse = (value: any, opts?: { NamespaceMap: Map<string, { fields: Set<String>; cachedResult: Function }> }) => { parse = (
value: any,
opts?: { NamespaceMap: Map<string, { fields: Set<String>; fnWrapper: Function; fn?: Function }> },
) => {
const engine = this.engine; const engine = this.engine;
function type(value) { function type(value) {
let valueType: string = typeof value; let valueType: string = typeof value;
@ -161,20 +174,15 @@ export class JSONTemplateParser {
if (opts?.NamespaceMap && opts.NamespaceMap.has(template.variableSegments[0])) { if (opts?.NamespaceMap && opts.NamespaceMap.has(template.variableSegments[0])) {
const scopeKey = template.variableSegments[0]; const scopeKey = template.variableSegments[0];
const NS = opts.NamespaceMap.get(scopeKey); const NS = opts.NamespaceMap.get(scopeKey);
const cachedFn = NS.cachedResult; const fn = NS.fn;
const field = getFieldName({ const field = getFieldName({
variableName: template.variableName, variableName: template.variableName,
variableSegments: template.variableSegments, variableSegments: template.variableSegments,
}); });
if (!fn) {
if (cachedFn) { throw new Error(`fn not found for ${scopeKey}`);
return cachedFn(revertEscape(field));
} else {
const fnWrapper = get(escapedContext, scopeKey);
const fn = fnWrapper({ fields: Array.from(opts.NamespaceMap.get(scopeKey).fields), context });
NS.cachedResult = fn;
return fn(revertEscape(field));
} }
return fn(revertEscape(field));
} else if (typeof ctxVal === 'function') { } else if (typeof ctxVal === 'function') {
const ctxVal = get(escapedContext, template.variableName); const ctxVal = get(escapedContext, template.variableName);
value = ctxVal(); value = ctxVal();

View File

@ -1,3 +1,12 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import Database, { Repository } from '@nocobase/database'; import Database, { Repository } from '@nocobase/database';
import { createMockServer, MockServer } from '@nocobase/test'; import { createMockServer, MockServer } from '@nocobase/test';
@ -15,7 +24,7 @@ describe('date filters', async () => {
db = app.db; db = app.db;
repo = db.getRepository('authenticators'); repo = db.getRepository('authenticators');
agent = app.agent(); agent = app.agent();
parse = app.jsonTemplateParser.parse; parse = app.jsonTemplateParser._parse;
}); });
it('date filters', async () => { it('date filters', async () => {
const template = { const template = {
@ -24,7 +33,7 @@ describe('date filters', async () => {
yesterday: '{{now | date_subtract: 1, "day" | date_format: "YYYY-MM-DD"}}', yesterday: '{{now | date_subtract: 1, "day" | date_format: "YYYY-MM-DD"}}',
}; };
const parsed = app.jsonTemplateParser.parse(template); const parsed = app.jsonTemplateParser._parse(template);
const now = new Date('2025-01-01 12:00:00'); const now = new Date('2025-01-01 12:00:00');
const result = parsed({ const result = parsed({
now, now,
@ -42,7 +51,7 @@ describe('date filters', async () => {
firstOfArray1: '{{array.0}}', firstOfArray1: '{{array.0}}',
firstOfArray2: '{{array[0]}}', firstOfArray2: '{{array[0]}}',
}; };
const result = app.jsonTemplateParser.parse(template)({ const result = app.jsonTemplateParser._parse(template)({
user: { name: 'john' }, user: { name: 'john' },
array: ['first', 'second'], array: ['first', 'second'],
}); });
@ -60,7 +69,7 @@ describe('date filters', async () => {
form: '{{form}}', form: '{{form}}',
$form: '{{$form}}', $form: '{{$form}}',
}; };
const result = app.jsonTemplateParser.parse(template)({ const result = app.jsonTemplateParser._parse(template)({
form, form,
$form: form, $form: form,
}); });
@ -75,7 +84,7 @@ describe('date filters', async () => {
key1: '{{current.key1}}', key1: '{{current.key1}}',
key2: '{{current.key2}}', key2: '{{current.key2}}',
}; };
const result = app.jsonTemplateParser.parse(template)({ const result = app.jsonTemplateParser._parse(template)({
current: { key1: 'value1' }, current: { key1: 'value1' },
}); });
expect(result).toEqual({ expect(result).toEqual({
@ -91,7 +100,7 @@ describe('date filters', async () => {
$yesterday: '{{ $now | date_subtract: 1, "day" | date_format: "YYYY-MM-DD" }}', $yesterday: '{{ $now | date_subtract: 1, "day" | date_format: "YYYY-MM-DD" }}',
}; };
const parsed = app.jsonTemplateParser.parse(template); const parsed = app.jsonTemplateParser._parse(template);
const $now = new Date('2025-01-01: 12:00:00'); const $now = new Date('2025-01-01: 12:00:00');
const result = parsed({ const result = parsed({
$now, $now,