From 319c7614c03dce795f1e19cfc99f5fe80cc2deaf Mon Sep 17 00:00:00 2001 From: Sheldon Guo Date: Fri, 14 Mar 2025 09:41:18 +0800 Subject: [PATCH] feat: update ctx-func tests and parser to support async context functions --- .../src/__tests__/ctx-func.test.ts | 31 +++++++++-------- .../src/parser/json-template-parser.ts | 34 ++++++++++++------- .../src/server/__tests__/date.test.ts | 21 ++++++++---- 3 files changed, 52 insertions(+), 34 deletions(-) diff --git a/packages/core/json-template-parser/src/__tests__/ctx-func.test.ts b/packages/core/json-template-parser/src/__tests__/ctx-func.test.ts index 12fff3a91e..a71a99a6d4 100644 --- a/packages/core/json-template-parser/src/__tests__/ctx-func.test.ts +++ b/packages/core/json-template-parser/src/__tests__/ctx-func.test.ts @@ -18,10 +18,11 @@ describe('ctx function', () => { const variable = extractTemplateVariable(template); 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 data = { - $user({ fields, context }) { + async $user({ fields, context }) { if (context.state.userId) { return (field) => 1; } else return (field) => 2; @@ -30,11 +31,11 @@ describe('ctx function', () => { userId: 1, }, }; - const result = parser.render(template, data); + const result = await parser.render(template, data); 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 data = { $user({ fields, context }) { @@ -42,11 +43,11 @@ describe('ctx function', () => { }, state: {}, }; - const result = parser.render(template, data); + const result = await parser.render(template, data); 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 data = { $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'); }); - it('should handle multiple context functions', () => { + it('should handle multiple context functions', async () => { const template = '{{$user.name}} works at {{$company.name}}'; const data = { $user({ fields, context }) { @@ -73,22 +74,22 @@ describe('ctx function', () => { return (field) => 'NocoBase'; }, }; - const result = parser.render(template, data); + const result = await parser.render(template, data); 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 data = { $user({ fields, context }) { return (field) => undefined; }, }; - const result = parser.render(template, data); + const result = await parser.render(template, data); 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 data = { $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'); }); - it('should escape array', () => { + it('should escape array', async () => { const template = ' {{$user.id}} - {{$user.name}} '; const data = { @@ -116,7 +117,7 @@ describe('ctx function', () => { userId: 1, }, }; - const result = parser.render(template, data); + const result = await parser.render(template, data); expect(result).toEqual(' 1 - 1 '); }); }); diff --git a/packages/core/json-template-parser/src/parser/json-template-parser.ts b/packages/core/json-template-parser/src/parser/json-template-parser.ts index f86d0eeaed..3874d87e12 100644 --- a/packages/core/json-template-parser/src/parser/json-template-parser.ts +++ b/packages/core/json-template-parser/src/parser/json-template-parser.ts @@ -64,18 +64,31 @@ export class JSONTemplateParser { this._engine.registerFilter(filter.name, filter.handler); } - render(template: string, data: any = {}): any { - const NamespaceMap = new Map; cachedResult: Function }>(); + async render(template: string, data: any = {}): Promise { + const NamespaceMap = new Map; fnWrapper: Function; fn?: any }>(); Object.keys(data).forEach((key) => { if (key.startsWith('$') || typeof data[key] === 'function') { - NamespaceMap.set(escape(key), { fields: new Set(), cachedResult: null }); + NamespaceMap.set(escape(key), { fields: new Set(), fnWrapper: data[key] }); } }); 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); } - parse = (value: any, opts?: { NamespaceMap: Map; cachedResult: Function }> }) => { + parse = ( + value: any, + opts?: { NamespaceMap: Map; fnWrapper: Function; fn?: Function }> }, + ) => { const engine = this.engine; function type(value) { let valueType: string = typeof value; @@ -161,20 +174,15 @@ export class JSONTemplateParser { if (opts?.NamespaceMap && opts.NamespaceMap.has(template.variableSegments[0])) { const scopeKey = template.variableSegments[0]; const NS = opts.NamespaceMap.get(scopeKey); - const cachedFn = NS.cachedResult; + const fn = NS.fn; const field = getFieldName({ variableName: template.variableName, variableSegments: template.variableSegments, }); - - if (cachedFn) { - 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)); + if (!fn) { + throw new Error(`fn not found for ${scopeKey}`); } + return fn(revertEscape(field)); } else if (typeof ctxVal === 'function') { const ctxVal = get(escapedContext, template.variableName); value = ctxVal(); diff --git a/packages/plugins/@nocobase/plugin-variable-filters/src/server/__tests__/date.test.ts b/packages/plugins/@nocobase/plugin-variable-filters/src/server/__tests__/date.test.ts index 7e73f5ab56..5487982a42 100644 --- a/packages/plugins/@nocobase/plugin-variable-filters/src/server/__tests__/date.test.ts +++ b/packages/plugins/@nocobase/plugin-variable-filters/src/server/__tests__/date.test.ts @@ -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 { createMockServer, MockServer } from '@nocobase/test'; @@ -15,7 +24,7 @@ describe('date filters', async () => { db = app.db; repo = db.getRepository('authenticators'); agent = app.agent(); - parse = app.jsonTemplateParser.parse; + parse = app.jsonTemplateParser._parse; }); it('date filters', async () => { const template = { @@ -24,7 +33,7 @@ describe('date filters', async () => { 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 result = parsed({ now, @@ -42,7 +51,7 @@ describe('date filters', async () => { firstOfArray1: '{{array.0}}', firstOfArray2: '{{array[0]}}', }; - const result = app.jsonTemplateParser.parse(template)({ + const result = app.jsonTemplateParser._parse(template)({ user: { name: 'john' }, array: ['first', 'second'], }); @@ -60,7 +69,7 @@ describe('date filters', async () => { form: '{{form}}', $form: '{{$form}}', }; - const result = app.jsonTemplateParser.parse(template)({ + const result = app.jsonTemplateParser._parse(template)({ form, $form: form, }); @@ -75,7 +84,7 @@ describe('date filters', async () => { key1: '{{current.key1}}', key2: '{{current.key2}}', }; - const result = app.jsonTemplateParser.parse(template)({ + const result = app.jsonTemplateParser._parse(template)({ current: { key1: 'value1' }, }); expect(result).toEqual({ @@ -91,7 +100,7 @@ describe('date filters', async () => { $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 result = parsed({ $now,