mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-08 15:09:27 +08:00
feat: init render variables
This commit is contained in:
parent
319c7614c0
commit
6218a3c7ce
@ -7,15 +7,14 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { useVariables, useLocalVariables } from '../../../variables';
|
||||
import { getPath } from '../../../variables/utils/getPath';
|
||||
import { isArray } from 'lodash';
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { useLocalVariables, useVariables } from '../../../variables';
|
||||
interface VariableContextValue {
|
||||
value: any;
|
||||
}
|
||||
|
||||
interface VariableProviderProps {
|
||||
interface eProviderProps {
|
||||
variableName: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ describe('ctx function', () => {
|
||||
userId: 1,
|
||||
},
|
||||
};
|
||||
const result = await parser.render(template, data);
|
||||
const result = await parser.render(template, data, data);
|
||||
expect(result).toEqual('1 - 1');
|
||||
});
|
||||
|
||||
@ -113,11 +113,14 @@ describe('ctx function', () => {
|
||||
return (field) => 1;
|
||||
} else return (field) => 2;
|
||||
},
|
||||
};
|
||||
|
||||
const context = {
|
||||
state: {
|
||||
userId: 1,
|
||||
},
|
||||
};
|
||||
const result = await parser.render(template, data);
|
||||
const result = await parser.render(template, data, context);
|
||||
expect(result).toEqual(' 1 - 1 ');
|
||||
});
|
||||
});
|
||||
|
@ -9,7 +9,6 @@
|
||||
|
||||
import { Liquid, TokenKind } from 'liquidjs';
|
||||
import { get } from 'lodash';
|
||||
import { variableFilters, filterGroups } from '../filters';
|
||||
import { escape, revertEscape } from '../escape';
|
||||
type FilterGroup = {
|
||||
name: string;
|
||||
@ -64,16 +63,20 @@ export class JSONTemplateParser {
|
||||
this._engine.registerFilter(filter.name, filter.handler);
|
||||
}
|
||||
|
||||
async render(template: string, data: any = {}): Promise<any> {
|
||||
const NamespaceMap = new Map<string, { fields: Set<String>; fnWrapper: Function; fn?: any }>();
|
||||
async render(template: string, data: any = {}, context?: Record<string, any>): Promise<any> {
|
||||
const NamespaceMap = new Map<string, { fieldSet: Set<String>; fnWrapper: Function; fn?: any }>();
|
||||
Object.keys(data).forEach((key) => {
|
||||
if (key.startsWith('$') || typeof data[key] === 'function') {
|
||||
NamespaceMap.set(escape(key), { fields: new Set<String>(), fnWrapper: data[key] });
|
||||
NamespaceMap.set(escape(key), { fieldSet: new Set<String>(), 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 });
|
||||
const fnPromises = Array.from(NamespaceMap.entries()).map(async ([key, { fieldSet, fnWrapper }]) => {
|
||||
const fields = Array.from(fieldSet);
|
||||
if (fields.length === 0) {
|
||||
return { key, fn: null };
|
||||
}
|
||||
const fn = await fnWrapper({ fields: Array.from(fields), data, context });
|
||||
return { key, fn };
|
||||
});
|
||||
const fns = await Promise.all(fnPromises);
|
||||
@ -87,7 +90,7 @@ export class JSONTemplateParser {
|
||||
|
||||
parse = (
|
||||
value: any,
|
||||
opts?: { NamespaceMap: Map<string, { fields: Set<String>; fnWrapper: Function; fn?: Function }> },
|
||||
opts?: { NamespaceMap: Map<string, { fieldSet: Set<String>; fnWrapper: Function; fn?: Function }> },
|
||||
) => {
|
||||
const engine = this.engine;
|
||||
function type(value) {
|
||||
@ -114,7 +117,7 @@ export class JSONTemplateParser {
|
||||
// template parameter syntax such as {{foo}} or {{foo:someDefault}}.
|
||||
const getFieldName = ({ variableName, variableSegments }) =>
|
||||
revertEscape(variableName.slice(variableSegments[0].length + 1));
|
||||
return (str) => {
|
||||
return (str, preKeys: string[]) => {
|
||||
const escapeStr = escape(str);
|
||||
const rawTemplates = engine.parse(escapeStr);
|
||||
const templates = rawTemplates.map((rawTemplate) => {
|
||||
@ -135,7 +138,7 @@ export class JSONTemplateParser {
|
||||
variableSegments.length > 1 &&
|
||||
opts.NamespaceMap.has(variableSegments[0])
|
||||
) {
|
||||
const fieldSet = opts.NamespaceMap.get(variableSegments[0]).fields;
|
||||
const fieldSet = opts.NamespaceMap.get(variableSegments[0]).fieldSet;
|
||||
const field = getFieldName({ variableName, variableSegments });
|
||||
fieldSet.add(field);
|
||||
}
|
||||
@ -182,7 +185,7 @@ export class JSONTemplateParser {
|
||||
if (!fn) {
|
||||
throw new Error(`fn not found for ${scopeKey}`);
|
||||
}
|
||||
return fn(revertEscape(field));
|
||||
return fn(revertEscape(field), preKeys);
|
||||
} else if (typeof ctxVal === 'function') {
|
||||
const ctxVal = get(escapedContext, template.variableName);
|
||||
value = ctxVal();
|
||||
@ -212,10 +215,10 @@ export class JSONTemplateParser {
|
||||
};
|
||||
})();
|
||||
|
||||
function parseObject(object) {
|
||||
function parseObject(object, preKeys) {
|
||||
const children = Object.keys(object).map((key) => ({
|
||||
keyTemplate: parseString(key),
|
||||
valueTemplate: _parse(object[key]),
|
||||
keyTemplate: parseString(key, preKeys),
|
||||
valueTemplate: _parse(object[key], [...preKeys, key]),
|
||||
}));
|
||||
const templateParameters = children.reduce(
|
||||
(parameters, child) => parameters.concat(child.valueTemplate.parameters, child.keyTemplate.parameters),
|
||||
@ -232,22 +235,22 @@ export class JSONTemplateParser {
|
||||
}
|
||||
|
||||
// Parses non-leaf-nodes in the template object that are arrays.
|
||||
function parseArray(array) {
|
||||
const templates = array.map(_parse);
|
||||
function parseArray(array, preKeys) {
|
||||
const templates = array.map((item, index) => _parse(item, [...preKeys, index]));
|
||||
const templateParameters = templates.reduce((parameters, template) => parameters.concat(template.parameters), []);
|
||||
const templateFn = (context) => templates.map((template) => template(context));
|
||||
|
||||
return Template(templateFn, templateParameters);
|
||||
}
|
||||
|
||||
function _parse(value) {
|
||||
function _parse(value, keys = []) {
|
||||
switch (type(value)) {
|
||||
case 'string':
|
||||
return parseString(value);
|
||||
return parseString(value, keys);
|
||||
case 'object':
|
||||
return parseObject(value);
|
||||
return parseObject(value, keys);
|
||||
case 'array':
|
||||
return parseArray(value);
|
||||
return parseArray(value, keys);
|
||||
default:
|
||||
return Template(function () {
|
||||
return value;
|
||||
|
126
packages/core/server/src/middlewares/render-variables.ts
Normal file
126
packages/core/server/src/middlewares/render-variables.ts
Normal file
@ -0,0 +1,126 @@
|
||||
/**
|
||||
* 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 { createJSONTemplateParser } from '@nocobase/json-template-parser';
|
||||
import { getDateVars } from '@nocobase/utils';
|
||||
import { get } from 'lodash';
|
||||
|
||||
function getUser(ctx) {
|
||||
return async ({ fields }) => {
|
||||
const userFields = fields.filter((f) => f && ctx.db.getFieldByPath('users.' + f));
|
||||
ctx.logger?.info('filter-parse: ', { userFields });
|
||||
if (!ctx.state.currentUser) {
|
||||
return;
|
||||
}
|
||||
if (!userFields.length) {
|
||||
return;
|
||||
}
|
||||
const user = await ctx.db.getRepository('users').findOne({
|
||||
filterByTk: ctx.state.currentUser.id,
|
||||
fields: userFields,
|
||||
});
|
||||
ctx.logger?.info('filter-parse: ', {
|
||||
$user: user?.toJSON(),
|
||||
});
|
||||
return ({ field }) => {
|
||||
get(user, field);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function isNumeric(str: any) {
|
||||
if (typeof str === 'number') return true;
|
||||
if (typeof str != 'string') return false;
|
||||
return !isNaN(str as any) && !isNaN(parseFloat(str));
|
||||
}
|
||||
const isDateOperator = (op) => {
|
||||
return [
|
||||
'$dateOn',
|
||||
'$dateNotOn',
|
||||
'$dateBefore',
|
||||
'$dateAfter',
|
||||
'$dateNotBefore',
|
||||
'$dateNotAfter',
|
||||
'$dateBetween',
|
||||
].includes(op);
|
||||
};
|
||||
|
||||
function isDate(input) {
|
||||
return input instanceof Date || Object.prototype.toString.call(input) === '[object Date]';
|
||||
}
|
||||
|
||||
const dateValueWrapper = (value: any, timezone?: string) => {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length === 2) {
|
||||
value.push('[]', timezone);
|
||||
} else if (value.length === 3) {
|
||||
value.push(timezone);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
if (!timezone || /(\+|-)\d\d:\d\d$/.test(value)) {
|
||||
return value;
|
||||
}
|
||||
return value + timezone;
|
||||
}
|
||||
|
||||
if (isDate(value)) {
|
||||
return value.toISOString();
|
||||
}
|
||||
};
|
||||
|
||||
const $date = ({ fields, data, context }) => {
|
||||
const timezone = context.timezone;
|
||||
const dateVars = getDateVars();
|
||||
return (field, keys) => {
|
||||
const value = get(dateVars, field);
|
||||
const operator = keys[keys.length - 1];
|
||||
if (isDateOperator(operator)) {
|
||||
const field = context?.getField?.(keys);
|
||||
if (field?.constructor.name === 'DateOnlyField' || field?.constructor.name === 'DatetimeNoTzField') {
|
||||
return value;
|
||||
}
|
||||
return dateValueWrapper(value, field?.timezone || timezone);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const parser = createJSONTemplateParser();
|
||||
|
||||
export async function renderVariables(ctx, next) {
|
||||
const filter = ctx.action.params.filter;
|
||||
if (!filter) {
|
||||
return next();
|
||||
}
|
||||
ctx.action.params.filter = await parser.render(
|
||||
filter,
|
||||
{
|
||||
$user: getUser(ctx),
|
||||
$date,
|
||||
$nDate: $date,
|
||||
$nRole: ctx.state.currentRole,
|
||||
},
|
||||
{
|
||||
timezone: ctx.get('x-timezone'),
|
||||
now: new Date().toISOString(),
|
||||
getField: (keys) => {
|
||||
const fieldPath = keys.filter((p) => !p.startsWith('$') && !isNumeric(p)).join('.');
|
||||
const { resourceName } = ctx.action;
|
||||
return ctx.db.getFieldByPath(`${resourceName}.${fieldPath}`);
|
||||
},
|
||||
},
|
||||
);
|
||||
await next();
|
||||
}
|
22
packages/core/utils/src/render-filter.ts
Normal file
22
packages/core/utils/src/render-filter.ts
Normal file
@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 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 { createJSONTemplateParser } from '@nocobase/json-template-parser';
|
||||
|
||||
const parser = createJSONTemplateParser();
|
||||
|
||||
type ParseFilterOptions = {
|
||||
now?: any;
|
||||
timezone?: string;
|
||||
getField?: any;
|
||||
};
|
||||
|
||||
function renderFilters(filters, data, context: ParseFilterOptions = {}) {
|
||||
return parser.render(filters, data, context);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user