feat: 全文检索允许自定义语言配置(用于postgres)

This commit is contained in:
Pan Qiancheng 2025-12-31 12:40:13 +08:00
parent a1e851e186
commit d32c29da15
7 changed files with 1398 additions and 37 deletions

View File

@ -54,6 +54,7 @@ export type Q_State<S> = S | {
export type Q_FullTextValue = {
$search: string;
$language?: 'zh_CN' | 'en_US';
$ts?: string;
};
export type Q_FullTextKey = '$text';
export type FulltextFilter = {

View File

@ -11,6 +11,7 @@ export interface IndexConfig {
unique?: boolean;
type?: 'fulltext' | 'btree' | 'hash' | 'spatial';
parser?: 'ngram';
tsConfig?: string | string[];
}
export interface Index<SH extends EntityShape> {
name: string;

658
lib/types/Trigger.d.ts vendored
View File

@ -5,120 +5,756 @@ import { SyncContext } from "../store/SyncRowStore";
import { EntityDict, OperateOption } from "../types/Entity";
import { EntityShape } from "../types/Entity";
import { EntityDict as BaseEntityDict } from '../base-app-domain';
/**
* Modi
*
* "修改申请"Modi
*
*
* @example
* // 场景员工修改个人信息需要HR审批
* // 1. 员工提交修改 -> 创建 Modi 记录create 阶段)
* // 2. HR 审批通过 -> 应用 Modi 到实际数据apply 阶段)
*
* - 'create': Modi
* - 'apply': Modi
* - 'both':
*
*
* - commit apply
* - commit create
*
* @see TriggerExecutor.judgeModiTurn
*/
export type ModiTurn = 'create' | 'apply' | 'both';
/**
* 199
*
*
*/
export declare const TRIGGER_MIN_PRIORITY = 1;
/**
*
* 使
*/
export declare const TRIGGER_DEFAULT_PRIORITY = 25;
/**
*
*
*/
export declare const TRIGGER_MAX_PRIORITY = 50;
/**
* Checker
* Checker 51-99
*
*/
export declare const CHECKER_MAX_PRIORITY = 99;
/**
* logical可能会更改row和data的值data和row不能修改相关的值
* logicalData去改data中的值
* Checker
*
*
* 1. logicalData (31) - data
* 2. logical (33) -
* 3. row (51) -
* 4. relation (56) -
* 5. data (61) -
*
* @example
* // logicalData 类型可以自动填充默认值
* // logical 类型进行业务逻辑校验
* // row 类型检查当前行状态是否允许操作
*/
export declare const CHECKER_PRIORITY_MAP: Record<CheckerType, number>;
/**
*
*
* @template ED -
* @template T -
*/
interface TriggerBase<ED extends EntityDict & BaseEntityDict, T extends keyof ED> {
/**
* Checker Checker
* 使
*/
checkerType?: CheckerType;
/**
*
* @example 'user', 'order', 'product'
*/
entity: T;
/**
*
*
* @example '订单创建后发送通知', 'user-create-init-profile'
*/
name: string;
/**
* Root
*
* true
*
*
* 使
*
* @example
* // 自动记录操作日志,需要写入用户无权访问的日志表
* {
* name: '记录操作日志',
* asRoot: true,
* fn: async (event, context) => {
* await context.operate('operationLog', { ... });
* }
* }
*/
asRoot?: true;
/**
*
*
* TRIGGER_MIN_PRIORITY (1) CHECKER_MAX_PRIORITY (99)
*
*
*
* - 1-10: 数据预处理
* - 11-30: 普通业务逻辑
* - 31-50: 依赖其他触发器结果的逻辑
* - 51-99: 数据校验 Checker 使
*
* @default TRIGGER_DEFAULT_PRIORITY (25)
*/
priority?: number;
}
/**
* Create
*
* @template ED -
* @template T -
* @template Cxt -
*
* @example
* // 创建用户时自动初始化用户配置
* const trigger: CreateTrigger<ED, 'user', Cxt> = {
* name: '初始化用户配置',
* entity: 'user',
* action: 'create',
* when: 'after',
* fn: async ({ operation }, context) => {
* const userId = operation.data.id;
* await context.operate('userConfig', {
* action: 'create',
* data: { id: generateId(), userId, theme: 'default' }
* });
* return 1;
* }
* };
*/
export interface CreateTriggerBase<ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED> | SyncContext<ED>> extends TriggerBase<ED, T> {
/**
* 'create'
*/
action: 'create';
/**
* Modi
* @see ModiTurn
*/
mt?: ModiTurn;
/**
*
*
* false
*
*
* @param operation -
* @returns true false
*
* @example
* // 只有创建 VIP 用户时才发送欢迎邮件
* check: (operation) => operation.data.isVip === true
*/
check?: (operation: ED[T]['Create']) => boolean;
}
/**
* Create
*
*
*
*
* @example
* // before: 在数据插入前执行,可以修改要插入的数据
* const beforeTrigger: CreateTriggerInTxn<ED, 'order', Cxt> = {
* name: '自动生成订单号',
* entity: 'order',
* action: 'create',
* when: 'before',
* fn: ({ operation }, context) => {
* operation.data.orderNo = generateOrderNo();
* return 0; // 返回修改的行数(此处未实际修改数据库)
* }
* };
*
* // after: 在数据插入后执行,可以执行关联操作
* const afterTrigger: CreateTriggerInTxn<ED, 'order', Cxt> = {
* name: '创建订单日志',
* entity: 'order',
* action: 'create',
* when: 'after',
* fn: async ({ operation }, context) => {
* await context.operate('orderLog', {
* action: 'create',
* data: { orderId: operation.data.id, action: 'created' }
* });
* return 1;
* }
* };
*/
export interface CreateTriggerInTxn<ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED> | SyncContext<ED>> extends CreateTriggerBase<ED, T, Cxt> {
/**
*
* - 'before': operation.data
* - 'after':
*/
when: 'before' | 'after';
/**
*
*
* @param event.operation - action data
* @param context -
* @param option -
* @returns
*/
fn: (event: {
operation: ED[T]['Create'];
}, context: Cxt, option: OperateOption) => Promise<number> | number;
}
/**
*
*
*
* -
* - API
* -
*
* checkpoint
*/
interface TriggerCrossTxn<ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED> | SyncContext<ED>> {
/**
* 'commit'
*
*/
when: 'commit';
/**
*
*
* - 'takeEasy':
*
*
* - 'makeSure':
* $$triggerData$$ $$triggerUuid$$
* checkpoint
*
*
* @default 'takeEasy'
*
* @example
* // 发送欢迎邮件 - 失败了也没关系
* { strict: 'takeEasy' }
*
* // 同步订单到ERP - 必须成功
* { strict: 'makeSure' }
*/
strict?: 'takeEasy' | 'makeSure';
/**
*
*
* true
*
*/
cs?: true;
/**
*
*
* true $$triggerData$$ $$triggerUuid$$
* fn
*
*
*/
cleanTriggerDataBySelf?: true;
/**
*
*
* true
*
*/
singleton?: true;
/**
*
*
* true checkpoint ID fn
*
*
* - false (): triggerUuid fn
* - true:
*
* @example
* // 批量同步数据到外部系统
* {
* grouped: true,
* fn: async ({ ids }, context) => {
* await syncToExternalSystem(ids); // 批量处理更高效
* }
* }
*/
grouped?: true;
/**
*
*
* @param event.ids - ID
* @param context -
* @param option -
* @returns
* - void:
* - 函数: 在清理标记后执行的回调函数
* - 对象: 需要更新到数据行上的额外数据
*
* @example
* // 基本用法
* fn: async ({ ids }, context) => {
* for (const id of ids) {
* await sendNotification(id);
* }
* }
*
* // 返回回调函数,在清理完成后执行
* fn: async ({ ids }, context) => {
* const results = await processIds(ids);
* return async (ctx, opt) => {
* await saveResults(results);
* };
* }
*
* // 返回需要更新的数据
* fn: async ({ ids }, context) => {
* return { syncedAt: Date.now() };
* }
*/
fn: (event: {
ids: string[];
}, context: Cxt, option: OperateOption) => Promise<((context: Cxt, option: OperateOption) => Promise<any>) | void | ED[T]['Update']['data']>;
}
/**
* Create
*
* CreateTriggerBase TriggerCrossTxn
*
*
* @example
* const trigger: CreateTriggerCrossTxn<ED, 'user', Cxt> = {
* name: '发送欢迎邮件',
* entity: 'user',
* action: 'create',
* when: 'commit',
* strict: 'makeSure', // 确保发送成功
* fn: async ({ ids }, context) => {
* for (const userId of ids) {
* await emailService.sendWelcome(userId);
* }
* }
* };
*/
export interface CreateTriggerCrossTxn<ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED> | SyncContext<ED>> extends CreateTriggerBase<ED, T, Cxt>, TriggerCrossTxn<ED, T, Cxt> {
}
/**
* Create
*
*/
export type CreateTrigger<ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED> | SyncContext<ED>> = CreateTriggerInTxn<ED, T, Cxt> | CreateTriggerCrossTxn<ED, T, Cxt>;
/**
* update trigger如果带有filter
* filter条件和trigger所定义的filter是否有交集
* triggerexists而不是all
* Update
*
* Update filter
* Create
*
* @template ED -
* @template T -
* @template Cxt -
*
* @example
* // 订单状态变为"已支付"时触发
* const trigger: UpdateTrigger<ED, 'order', Cxt> = {
* name: '订单支付成功处理',
* entity: 'order',
* action: 'pay', // 支付动作
* when: 'after',
* filter: { status: 'pending' }, // 只对待支付订单触发
* attributes: ['status'], // 只在 status 字段变化时触发
* fn: async ({ operation }, context) => {
* // 处理支付成功逻辑
* }
* };
*/
export interface UpdateTriggerBase<ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED> | SyncContext<ED>> extends TriggerBase<ED, T> {
/**
*
*
*
* 'create''remove''select'
*
* @example
* action: 'update' // 单个动作
* action: ['update', 'pay'] // 多个动作
*/
action: Exclude<ED[T]['Action'], ExcludeUpdateAction> | Array<Exclude<ED[T]['Action'], ExcludeUpdateAction>>;
/**
*
*
*
*
*
* operation.data
*
*
* @example
* // 只在 price 或 quantity 变化时重新计算总价
* attributes: ['price', 'quantity']
*/
attributes?: Array<keyof ED[T]['OpSchema']>;
/**
* Modi
* @see ModiTurn
*/
mt?: ModiTurn;
/**
*
*
* @param operation -
* @returns true false
*/
check?: (operation: ED[T]['Update']) => boolean;
/**
*
*
*
*
*
* filter filter
*
*
* @example
* // 静态过滤:只对状态为 active 的订单触发
* filter: { status: 'active' }
*
* // 动态过滤:根据操作内容决定过滤条件
* filter: (operation, context) => {
* if (operation.data.type === 'vip') {
* return { level: { $gte: 5 } };
* }
* return { level: { $gte: 1 } };
* }
*/
filter?: ED[T]['Filter'] | ((operation: ED[T]['Update'], context: Cxt, option: OperateOption) => ED[T]['Filter'] | Promise<ED[T]['Filter']>);
}
/**
* Update
*
* @example
* // 更新库存时检查是否需要补货提醒
* const trigger: UpdateTriggerInTxn<ED, 'product', Cxt> = {
* name: '库存不足提醒',
* entity: 'product',
* action: 'update',
* when: 'after',
* attributes: ['stock'],
* filter: { stock: { $lt: 10 } }, // 库存小于10时
* fn: async ({ operation }, context) => {
* // 创建补货提醒
* return 1;
* }
* };
*/
export interface UpdateTriggerInTxn<ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED> | SyncContext<ED>> extends UpdateTriggerBase<ED, T, Cxt> {
/**
*
* - 'before': operation.data
* - 'after':
*/
when: 'before' | 'after';
/**
*
*
* @param event.operation -
* @param context -
* @param option -
* @returns
*/
fn: (event: {
operation: ED[T]['Update'];
}, context: Cxt, option: OperateOption) => Promise<number> | number;
}
/**
* Update
*
* @example
* // 订单完成后同步到财务系统
* const trigger: UpdateTriggerCrossTxn<ED, 'order', Cxt> = {
* name: '同步订单到财务系统',
* entity: 'order',
* action: 'complete',
* when: 'commit',
* strict: 'makeSure',
* filter: { status: 'completed' },
* fn: async ({ ids }, context) => {
* await financeSystem.syncOrders(ids);
* }
* };
*/
export interface UpdateTriggerCrossTxn<ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED> | SyncContext<ED>> extends UpdateTriggerBase<ED, T, Cxt>, TriggerCrossTxn<ED, T, Cxt> {
}
/**
* Update
*/
export type UpdateTrigger<ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED> | SyncContext<ED>> = UpdateTriggerInTxn<ED, T, Cxt> | UpdateTriggerCrossTxn<ED, T, Cxt>;
/**
* update trigger一样remove trigger如果带有filter
* filter条件和trigger所定义的filter是否有交集
* triggerexists而不是all
* Remove
*
* Update
*
* @example
* // 删除用户时清理关联数据
* const trigger: RemoveTrigger<ED, 'user', Cxt> = {
* name: '清理用户关联数据',
* entity: 'user',
* action: 'remove',
* when: 'before',
* fn: async ({ operation }, context) => {
* const { filter } = operation;
* // 删除用户的所有订单
* await context.operate('order', {
* id: await generateNewIdAsync(),
* action: 'remove',
* filter: { user: filter }
* });
* return 1;
* }
* };
*/
export interface RemoveTriggerBase<ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED> | SyncContext<ED>> extends TriggerBase<ED, T> {
/**
* 'remove'
*/
action: 'remove';
/**
* Modi
*/
mt?: ModiTurn;
/**
*
*/
check?: (operation: ED[T]['Remove']) => boolean;
/**
*
*
*/
filter?: ED[T]['Remove']['filter'] | ((operation: ED[T]['Remove'], context: Cxt, option: OperateOption) => ED[T]['Remove']['filter'] | Promise<ED[T]['Remove']['filter']>);
}
/**
* Remove
*/
export interface RemoveTriggerInTxn<ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED> | SyncContext<ED>> extends RemoveTriggerBase<ED, T, Cxt> {
when: 'before' | 'after';
fn: (event: {
operation: ED[T]['Remove'];
}, context: Cxt, option: OperateOption) => Promise<number> | number;
}
/**
* Remove
*/
export interface RemoveTriggerCrossTxn<ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED> | SyncContext<ED>> extends RemoveTriggerBase<ED, T, Cxt>, TriggerCrossTxn<ED, T, Cxt> {
}
/**
* Remove
*/
export type RemoveTrigger<ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED> | SyncContext<ED>> = RemoveTriggerInTxn<ED, T, Cxt> | RemoveTriggerCrossTxn<ED, T, Cxt>;
/**
* Select
*
* Select
* -
* -
*/
export interface SelectTriggerBase<ED extends EntityDict & BaseEntityDict, T extends keyof ED> extends TriggerBase<ED, T> {
/**
* 'select'
*/
action: 'select';
}
/**
* selection似乎不需要支持跨事务
* todo by Xc
* Select
*
*
* -
* -
* -
*
* Select
*
* @example
* // 自动添加系统ID过滤
* const trigger: SelectTriggerBefore<ED, 'order', Cxt> = {
* name: '修改filter添加systemId',
* entity: 'order',
* action: 'select',
* when: 'before',
* fn: ({ operation }, context) => {
* const systemId = context.getSystemId();
* // 修改查询条件,添加租户过滤
* operation.filter = {
* ...operation.filter,
* systemId,
* };
* return 0;
* }
* };
*/
export interface SelectTriggerBefore<ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED> | SyncContext<ED>> extends SelectTriggerBase<ED, T> {
/**
* 'before'
*/
when: 'before';
/**
*
*
* @param event.operation -
* @param context -
* @param params -
* @returns 0
*/
fn: (event: {
operation: ED[T]['Selection'];
}, context: Cxt, params?: SelectOption) => Promise<number> | number;
}
/**
* Select
*
*
* -
* -
* -
* -
*
* @example
* // 计算订单的总金额(派生字段)
* const trigger: SelectTriggerAfter<ED, 'order', Cxt> = {
* name: '计算订单总金额',
* entity: 'order',
* action: 'select',
* when: 'after',
* fn: ({ operation, result }, context) => {
* result.forEach(order => {
* if (order.items) {
* order.totalAmount = order.items.reduce(
* (sum, item) => sum + item.price * item.quantity, 0
* );
* }
* });
* return result.length;
* }
* };
*
* // 根据权限隐藏敏感字段
* const sensitiveDataTrigger: SelectTriggerAfter<ED, 'employee', Cxt> = {
* name: '隐藏员工敏感信息',
* entity: 'employee',
* action: 'select',
* when: 'after',
* fn: ({ result }, context) => {
* const isRoot = context.isRoot();
* if (!isRoot) {
* result.forEach(emp => {
* delete emp.salary;
* delete emp.idCard;
* });
* }
* return result.length;
* }
* };
*/
export interface SelectTriggerAfter<ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED> | SyncContext<ED>> extends SelectTriggerBase<ED, T> {
/**
* 'after'
*/
when: 'after';
/**
*
*
* @param event.operation -
* @param event.result -
* @param context -
* @param params -
* @returns
*/
fn: (event: {
operation: ED[T]['Selection'];
result: Partial<ED[T]['Schema']>[];
}, context: Cxt, params?: SelectOption) => Promise<number> | number;
}
/**
* Select
*/
export type SelectTrigger<ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED> | SyncContext<ED>> = SelectTriggerBefore<ED, T, Cxt> | SelectTriggerAfter<ED, T, Cxt>;
/**
*
*
* CRUD
* - CreateTrigger: 创建触发器
* - UpdateTrigger: 更新触发器
* - RemoveTrigger: 删除触发器
* - SelectTrigger: 查询触发器
*
* @example
* // 注册触发器时使用
* function registerTrigger<T extends keyof ED>(trigger: Trigger<ED, T, Cxt>) {
* triggerExecutor.registerTrigger(trigger);
* }
*/
export type Trigger<ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED> | SyncContext<ED>> = CreateTrigger<ED, T, Cxt> | UpdateTrigger<ED, T, Cxt> | RemoveTrigger<ED, T, Cxt> | SelectTrigger<ED, T, Cxt>;
/**
*
*
*
*
*
* @deprecated 使 TriggerDataAttribute TriggerUuidAttribute
*/
export interface TriggerEntityShape extends EntityShape {
/**
*
*
*/
$$triggerData$$?: {
name: string;
operation: object;
};
/**
*
* @deprecated $$triggerUuid$$
*/
$$triggerTimestamp$$?: number;
}
/**
* Volatile
*
* when: 'commit'
* checkpoint
*
*
* -
* -
* -
* -
*
* @example
* // 判断一个触发器是否是跨事务触发器
* function isVolatileTrigger(trigger: Trigger<ED, T, Cxt>): trigger is VolatileTrigger<ED, T, Cxt> {
* return trigger.when === 'commit';
* }
*/
export type VolatileTrigger<ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED> | SyncContext<ED>> = CreateTriggerCrossTxn<ED, T, Cxt> | UpdateTriggerCrossTxn<ED, T, Cxt> | RemoveTriggerCrossTxn<ED, T, Cxt>;
export {};

View File

@ -1,16 +1,47 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CHECKER_PRIORITY_MAP = exports.CHECKER_MAX_PRIORITY = exports.TRIGGER_MAX_PRIORITY = exports.TRIGGER_DEFAULT_PRIORITY = exports.TRIGGER_MIN_PRIORITY = void 0;
/* ============================================================================
* 优先级常量定义
*
* 触发器按优先级从小到大依次执行数值越小越先执行
* 合理设置优先级可以控制触发器的执行顺序确保数据处理的正确性
* ============================================================================ */
/**
* 优先级越小越早执行定义在199之间
* 触发器最小优先级最先执行
* 用于需要最早执行的触发器如数据预处理
*/
exports.TRIGGER_MIN_PRIORITY = 1;
/**
* 触发器默认优先级
* 未指定优先级时使用此值
*/
exports.TRIGGER_DEFAULT_PRIORITY = 25;
/**
* 触发器最大优先级在普通触发器中最后执行
* 用于依赖其他触发器处理结果的场景
*/
exports.TRIGGER_MAX_PRIORITY = 50;
/**
* Checker检查器的最大优先级
* Checker 优先级范围是 51-99在所有普通触发器之后执行
* 这确保了数据校验在数据处理完成后进行
*/
exports.CHECKER_MAX_PRIORITY = 99;
/**
* logical可能会更改row和data的值应当最先执行data和row不能修改相关的值
* 允许logicalData去改data中的值
* 不同类型 Checker 的默认优先级映射
*
* 执行顺序从先到后
* 1. logicalData (31) - 逻辑数据检查可修改 data 中的值
* 2. logical (33) - 纯逻辑检查
* 3. row (51) - 行级检查检查数据库中已存在的行
* 4. relation (56) - 关系检查检查关联数据
* 5. data (61) - 数据完整性检查
*
* @example
* // logicalData 类型可以自动填充默认值
* // logical 类型进行业务逻辑校验
* // row 类型检查当前行状态是否允许操作
*/
exports.CHECKER_PRIORITY_MAP = {
logicalData: 31,

View File

@ -69,7 +69,8 @@ export type Q_State<S> = S | {
export type Q_FullTextValue = {
$search: string;
$language?: 'zh_CN' | 'en_US';
$language?: 'zh_CN' | 'en_US'; // MySQL的查询语言配置
$ts?: string // 可以自定义引擎或者默认在postgres中直接使用索引的tsConfig
};
export type Q_FullTextKey = '$text';

View File

@ -14,7 +14,8 @@ export interface Column<SH extends EntityShape> {
export interface IndexConfig {
unique?: boolean;
type?: 'fulltext'|'btree'|'hash'|'spatial';
parser?: 'ngram';
parser?: 'ngram'; // MySQL使用paser作为分词器配置
tsConfig?: string | string[]; // Postgres中可以自己指定一个或者多个
}
export interface Index<SH extends EntityShape> {

View File

@ -6,21 +6,75 @@ import { EntityDict, OperateOption } from "../types/Entity";
import { EntityShape } from "../types/Entity";
import { EntityDict as BaseEntityDict } from '../base-app-domain';
// 当处于创建modi过程中的行为create代表在创建createModi时就执行apply代表在modi真正被应用动作实际落地时再执行both代表两次都要执行
// 默认行为和trigger是否被标识为commit有关见TriggerExecutor->judgeModiTurn函数逻辑
export type ModiTurn = 'create' | 'apply' | 'both';
/**
* 199
* Modi
*
* "修改申请"Modi
*
*
* @example
* // 场景员工修改个人信息需要HR审批
* // 1. 员工提交修改 -> 创建 Modi 记录create 阶段)
* // 2. HR 审批通过 -> 应用 Modi 到实际数据apply 阶段)
*
* - 'create': Modi
* - 'apply': Modi
* - 'both':
*
*
* - commit apply
* - commit create
*
* @see TriggerExecutor.judgeModiTurn
*/
export type ModiTurn = 'create' | 'apply' | 'both';
/* ============================================================================
*
*
*
*
* ============================================================================ */
/**
*
*
*/
export const TRIGGER_MIN_PRIORITY = 1;
/**
*
* 使
*/
export const TRIGGER_DEFAULT_PRIORITY = 25;
/**
*
*
*/
export const TRIGGER_MAX_PRIORITY = 50;
/**
* Checker
* Checker 51-99
*
*/
export const CHECKER_MAX_PRIORITY = 99;
/**
* logical可能会更改row和data的值data和row不能修改相关的值
* logicalData去改data中的值
* Checker
*
*
* 1. logicalData (31) - data
* 2. logical (33) -
* 3. row (51) -
* 4. relation (56) -
* 5. data (61) -
*
* @example
* // logicalData 类型可以自动填充默认值
* // logical 类型进行业务逻辑校验
* // row 类型检查当前行状态是否允许操作
*/
export const CHECKER_PRIORITY_MAP: Record<CheckerType, number> = {
logicalData: 31,
@ -30,120 +84,756 @@ export const CHECKER_PRIORITY_MAP: Record<CheckerType, number> = {
relation: 56
};
/* ============================================================================
*
* ============================================================================ */
/**
*
*
* @template ED -
* @template T -
*/
interface TriggerBase<ED extends EntityDict & BaseEntityDict, T extends keyof ED> {
/**
* Checker Checker
* 使
*/
checkerType?: CheckerType;
/**
*
* @example 'user', 'order', 'product'
*/
entity: T;
/**
*
*
* @example '订单创建后发送通知', 'user-create-init-profile'
*/
name: string;
/**
* Root
*
* true
*
*
* 使
*
* @example
* // 自动记录操作日志,需要写入用户无权访问的日志表
* {
* name: '记录操作日志',
* asRoot: true,
* fn: async (event, context) => {
* await context.operate('operationLog', { ... });
* }
* }
*/
asRoot?: true;
/**
*
*
* TRIGGER_MIN_PRIORITY (1) CHECKER_MAX_PRIORITY (99)
*
*
*
* - 1-10: 数据预处理
* - 11-30: 普通业务逻辑
* - 31-50: 依赖其他触发器结果的逻辑
* - 51-99: 数据校验 Checker 使
*
* @default TRIGGER_DEFAULT_PRIORITY (25)
*/
priority?: number;
};
/* ============================================================================
* Create -
* ============================================================================ */
/**
* Create
*
* @template ED -
* @template T -
* @template Cxt -
*
* @example
* // 创建用户时自动初始化用户配置
* const trigger: CreateTrigger<ED, 'user', Cxt> = {
* name: '初始化用户配置',
* entity: 'user',
* action: 'create',
* when: 'after',
* fn: async ({ operation }, context) => {
* const userId = operation.data.id;
* await context.operate('userConfig', {
* action: 'create',
* data: { id: generateId(), userId, theme: 'default' }
* });
* return 1;
* }
* };
*/
export interface CreateTriggerBase<ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED> | SyncContext<ED>> extends TriggerBase<ED, T> {
/**
* 'create'
*/
action: 'create';
/**
* Modi
* @see ModiTurn
*/
mt?: ModiTurn;
/**
*
*
* false
*
*
* @param operation -
* @returns true false
*
* @example
* // 只有创建 VIP 用户时才发送欢迎邮件
* check: (operation) => operation.data.isVip === true
*/
check?: (operation: ED[T]['Create']) => boolean;
};
/**
* Create
*
*
*
*
* @example
* // before: 在数据插入前执行,可以修改要插入的数据
* const beforeTrigger: CreateTriggerInTxn<ED, 'order', Cxt> = {
* name: '自动生成订单号',
* entity: 'order',
* action: 'create',
* when: 'before',
* fn: ({ operation }, context) => {
* operation.data.orderNo = generateOrderNo();
* return 0; // 返回修改的行数(此处未实际修改数据库)
* }
* };
*
* // after: 在数据插入后执行,可以执行关联操作
* const afterTrigger: CreateTriggerInTxn<ED, 'order', Cxt> = {
* name: '创建订单日志',
* entity: 'order',
* action: 'create',
* when: 'after',
* fn: async ({ operation }, context) => {
* await context.operate('orderLog', {
* action: 'create',
* data: { orderId: operation.data.id, action: 'created' }
* });
* return 1;
* }
* };
*/
export interface CreateTriggerInTxn<ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED> | SyncContext<ED>> extends CreateTriggerBase<ED, T, Cxt> {
/**
*
* - 'before': operation.data
* - 'after':
*/
when: 'before' | 'after',
/**
*
*
* @param event.operation - action data
* @param context -
* @param option -
* @returns
*/
fn: (event: { operation: ED[T]['Create']; }, context: Cxt, option: OperateOption) => Promise<number> | number;
};
interface TriggerCrossTxn<ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED> | SyncContext<ED>> {
/**
*
*
*
* -
* - API
* -
*
* checkpoint
*/
interface TriggerCrossTxn<ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED> | SyncContext<ED>> {
/**
* 'commit'
*
*/
when: 'commit',
/**
*
*
* - 'takeEasy':
*
*
* - 'makeSure':
* $$triggerData$$ $$triggerUuid$$
* checkpoint
*
*
* @default 'takeEasy'
*
* @example
* // 发送欢迎邮件 - 失败了也没关系
* { strict: 'takeEasy' }
*
* // 同步订单到ERP - 必须成功
* { strict: 'makeSure' }
*/
strict?: 'takeEasy' | 'makeSure';
cs?: true; // cluster sensative集群敏感的需要由对应的集群进程统一处理
cleanTriggerDataBySelf?: true; // 自己在内部清除triggerData不需要框架统一处理
singleton?: true; // 置singleton意味着在集群环境中只有一个进程会去统一执行
grouped?: true; // 置grouped则在检查点时会将所有的不一致行全部传入一次性处理
/**
*
*
* true
*
*/
cs?: true;
/**
*
*
* true $$triggerData$$ $$triggerUuid$$
* fn
*
*
*/
cleanTriggerDataBySelf?: true;
/**
*
*
* true
*
*/
singleton?: true;
/**
*
*
* true checkpoint ID fn
*
*
* - false (): triggerUuid fn
* - true:
*
* @example
* // 批量同步数据到外部系统
* {
* grouped: true,
* fn: async ({ ids }, context) => {
* await syncToExternalSystem(ids); // 批量处理更高效
* }
* }
*/
grouped?: true;
/**
*
*
* @param event.ids - ID
* @param context -
* @param option -
* @returns
* - void:
* - 函数: 在清理标记后执行的回调函数
* - 对象: 需要更新到数据行上的额外数据
*
* @example
* // 基本用法
* fn: async ({ ids }, context) => {
* for (const id of ids) {
* await sendNotification(id);
* }
* }
*
* // 返回回调函数,在清理完成后执行
* fn: async ({ ids }, context) => {
* const results = await processIds(ids);
* return async (ctx, opt) => {
* await saveResults(results);
* };
* }
*
* // 返回需要更新的数据
* fn: async ({ ids }, context) => {
* return { syncedAt: Date.now() };
* }
*/
fn: (event: { ids: string[] }, context: Cxt, option: OperateOption) =>
Promise<((context: Cxt, option: OperateOption) => Promise<any>) | void | ED[T]['Update']['data']>;
// 跨事务的trigger可能紧接着要触发另一个跨事务trigger这里用回调的方式进行。若返回update的数据则一起更新到行上
Promise<((context: Cxt, option: OperateOption) => Promise<any>) | void | ED[T]['Update']['data']>;
}
/**
* Create
*
* CreateTriggerBase TriggerCrossTxn
*
*
* @example
* const trigger: CreateTriggerCrossTxn<ED, 'user', Cxt> = {
* name: '发送欢迎邮件',
* entity: 'user',
* action: 'create',
* when: 'commit',
* strict: 'makeSure', // 确保发送成功
* fn: async ({ ids }, context) => {
* for (const userId of ids) {
* await emailService.sendWelcome(userId);
* }
* }
* };
*/
export interface CreateTriggerCrossTxn<ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED> | SyncContext<ED>>
extends CreateTriggerBase<ED, T, Cxt>, TriggerCrossTxn<ED, T, Cxt> {};
/**
* Create
*
*/
export type CreateTrigger<ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED> | SyncContext<ED>> = CreateTriggerInTxn<ED, T, Cxt> | CreateTriggerCrossTxn<ED, T, Cxt>;
/* ============================================================================
* Update -
* ============================================================================ */
/**
* update trigger如果带有filter
* filter条件和trigger所定义的filter是否有交集
* triggerexists而不是all
* Update
*
* Update filter
* Create
*
* @template ED -
* @template T -
* @template Cxt -
*
* @example
* // 订单状态变为"已支付"时触发
* const trigger: UpdateTrigger<ED, 'order', Cxt> = {
* name: '订单支付成功处理',
* entity: 'order',
* action: 'pay', // 支付动作
* when: 'after',
* filter: { status: 'pending' }, // 只对待支付订单触发
* attributes: ['status'], // 只在 status 字段变化时触发
* fn: async ({ operation }, context) => {
* // 处理支付成功逻辑
* }
* };
*/
export interface UpdateTriggerBase<ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED> | SyncContext<ED>> extends TriggerBase<ED, T> {
/**
*
*
*
* 'create''remove''select'
*
* @example
* action: 'update' // 单个动作
* action: ['update', 'pay'] // 多个动作
*/
action: Exclude<ED[T]['Action'], ExcludeUpdateAction> | Array<Exclude<ED[T]['Action'], ExcludeUpdateAction>>,
/**
*
*
*
*
*
* operation.data
*
*
* @example
* // 只在 price 或 quantity 变化时重新计算总价
* attributes: ['price', 'quantity']
*/
attributes?: Array<keyof ED[T]['OpSchema']>;
/**
* Modi
* @see ModiTurn
*/
mt?: ModiTurn;
/**
*
*
* @param operation -
* @returns true false
*/
check?: (operation: ED[T]['Update']) => boolean;
/**
*
*
*
*
*
* filter filter
*
*
* @example
* // 静态过滤:只对状态为 active 的订单触发
* filter: { status: 'active' }
*
* // 动态过滤:根据操作内容决定过滤条件
* filter: (operation, context) => {
* if (operation.data.type === 'vip') {
* return { level: { $gte: 5 } };
* }
* return { level: { $gte: 1 } };
* }
*/
filter?: ED[T]['Filter'] | ((operation: ED[T]['Update'], context: Cxt, option: OperateOption) => ED[T]['Filter'] | Promise<ED[T]['Filter']>);
};
/**
* Update
*
* @example
* // 更新库存时检查是否需要补货提醒
* const trigger: UpdateTriggerInTxn<ED, 'product', Cxt> = {
* name: '库存不足提醒',
* entity: 'product',
* action: 'update',
* when: 'after',
* attributes: ['stock'],
* filter: { stock: { $lt: 10 } }, // 库存小于10时
* fn: async ({ operation }, context) => {
* // 创建补货提醒
* return 1;
* }
* };
*/
export interface UpdateTriggerInTxn<ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED> | SyncContext<ED>> extends UpdateTriggerBase<ED, T, Cxt> {
/**
*
* - 'before': operation.data
* - 'after':
*/
when: 'before' | 'after',
/**
*
*
* @param event.operation -
* @param context -
* @param option -
* @returns
*/
fn: (event: { operation: ED[T]['Update'] }, context: Cxt, option: OperateOption) => Promise<number> | number;
};
/**
* Update
*
* @example
* // 订单完成后同步到财务系统
* const trigger: UpdateTriggerCrossTxn<ED, 'order', Cxt> = {
* name: '同步订单到财务系统',
* entity: 'order',
* action: 'complete',
* when: 'commit',
* strict: 'makeSure',
* filter: { status: 'completed' },
* fn: async ({ ids }, context) => {
* await financeSystem.syncOrders(ids);
* }
* };
*/
export interface UpdateTriggerCrossTxn<ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED> | SyncContext<ED>>
extends UpdateTriggerBase<ED, T, Cxt>, TriggerCrossTxn<ED, T, Cxt> {};
/**
* Update
*/
export type UpdateTrigger<ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED> | SyncContext<ED>> = UpdateTriggerInTxn<ED, T, Cxt> | UpdateTriggerCrossTxn<ED, T, Cxt>;
/* ============================================================================
* Remove -
* ============================================================================ */
/**
* update trigger一样remove trigger如果带有filter
* filter条件和trigger所定义的filter是否有交集
* triggerexists而不是all
* Remove
*
* Update
*
* @example
* // 删除用户时清理关联数据
* const trigger: RemoveTrigger<ED, 'user', Cxt> = {
* name: '清理用户关联数据',
* entity: 'user',
* action: 'remove',
* when: 'before',
* fn: async ({ operation }, context) => {
* const { filter } = operation;
* // 删除用户的所有订单
* await context.operate('order', {
* id: await generateNewIdAsync(),
* action: 'remove',
* filter: { user: filter }
* });
* return 1;
* }
* };
*/
export interface RemoveTriggerBase<ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED> | SyncContext<ED>> extends TriggerBase<ED, T> {
/**
* 'remove'
*/
action: 'remove',
/**
* Modi
*/
mt?: ModiTurn;
/**
*
*/
check?: (operation: ED[T]['Remove']) => boolean;
/**
*
*
*/
filter?: ED[T]['Remove']['filter'] | ((operation: ED[T]['Remove'], context: Cxt, option: OperateOption) => ED[T]['Remove']['filter'] | Promise<ED[T]['Remove']['filter']>);
};
/**
* Remove
*/
export interface RemoveTriggerInTxn<ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED> | SyncContext<ED>> extends RemoveTriggerBase<ED, T, Cxt> {
when: 'before' | 'after',
fn: (event: { operation: ED[T]['Remove'] }, context: Cxt, option: OperateOption) => Promise<number> | number;
};
/**
* Remove
*/
export interface RemoveTriggerCrossTxn<ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED> | SyncContext<ED>>
extends RemoveTriggerBase<ED, T, Cxt>, TriggerCrossTxn<ED, T, Cxt> {};
/**
* Remove
*/
export type RemoveTrigger<ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED> | SyncContext<ED>> = RemoveTriggerInTxn<ED, T, Cxt> | RemoveTriggerCrossTxn<ED, T, Cxt>;
/* ============================================================================
* Select -
* ============================================================================ */
/**
* Select
*
* Select
* -
* -
*/
export interface SelectTriggerBase<ED extends EntityDict & BaseEntityDict, T extends keyof ED> extends TriggerBase<ED, T> {
/**
* 'select'
*/
action: 'select';
};
/**
* selection似乎不需要支持跨事务
* todo by Xc
* Select
*
*
* -
* -
* -
*
* Select
*
* @example
* // 自动添加系统ID过滤
* const trigger: SelectTriggerBefore<ED, 'order', Cxt> = {
* name: '修改filter添加systemId',
* entity: 'order',
* action: 'select',
* when: 'before',
* fn: ({ operation }, context) => {
* const systemId = context.getSystemId();
* // 修改查询条件,添加租户过滤
* operation.filter = {
* ...operation.filter,
* systemId,
* };
* return 0;
* }
* };
*/
export interface SelectTriggerBefore<ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED> | SyncContext<ED>> extends SelectTriggerBase<ED, T> {
/**
* 'before'
*/
when: 'before';
/**
*
*
* @param event.operation -
* @param context -
* @param params -
* @returns 0
*/
fn: (event: { operation: ED[T]['Selection'] }, context: Cxt, params?: SelectOption) => Promise<number> | number;
};
/**
* Select
*
*
* -
* -
* -
* -
*
* @example
* // 计算订单的总金额(派生字段)
* const trigger: SelectTriggerAfter<ED, 'order', Cxt> = {
* name: '计算订单总金额',
* entity: 'order',
* action: 'select',
* when: 'after',
* fn: ({ operation, result }, context) => {
* result.forEach(order => {
* if (order.items) {
* order.totalAmount = order.items.reduce(
* (sum, item) => sum + item.price * item.quantity, 0
* );
* }
* });
* return result.length;
* }
* };
*
* // 根据权限隐藏敏感字段
* const sensitiveDataTrigger: SelectTriggerAfter<ED, 'employee', Cxt> = {
* name: '隐藏员工敏感信息',
* entity: 'employee',
* action: 'select',
* when: 'after',
* fn: ({ result }, context) => {
* const isRoot = context.isRoot();
* if (!isRoot) {
* result.forEach(emp => {
* delete emp.salary;
* delete emp.idCard;
* });
* }
* return result.length;
* }
* };
*/
export interface SelectTriggerAfter<ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED> | SyncContext<ED>> extends SelectTriggerBase<ED, T> {
/**
* 'after'
*/
when: 'after',
/**
*
*
* @param event.operation -
* @param event.result -
* @param context -
* @param params -
* @returns
*/
fn: (event: {
operation: ED[T]['Selection'];
result: Partial<ED[T]['Schema']>[];
}, context: Cxt, params?: SelectOption) => Promise<number> | number;
};
/**
* Select
*/
export type SelectTrigger<ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED> | SyncContext<ED>> = SelectTriggerBefore<ED, T, Cxt> | SelectTriggerAfter<ED, T, Cxt>;
/* ============================================================================
*
* ============================================================================ */
/**
*
*
* CRUD
* - CreateTrigger: 创建触发器
* - UpdateTrigger: 更新触发器
* - RemoveTrigger: 删除触发器
* - SelectTrigger: 查询触发器
*
* @example
* // 注册触发器时使用
* function registerTrigger<T extends keyof ED>(trigger: Trigger<ED, T, Cxt>) {
* triggerExecutor.registerTrigger(trigger);
* }
*/
export type Trigger<ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED> | SyncContext<ED>> = CreateTrigger<ED, T, Cxt> | UpdateTrigger<ED, T, Cxt>
| RemoveTrigger<ED, T, Cxt> | SelectTrigger<ED, T, Cxt>;
/**
*
*
*
*
*
* @deprecated 使 TriggerDataAttribute TriggerUuidAttribute
*/
export interface TriggerEntityShape extends EntityShape {
/**
*
*
*/
$$triggerData$$?: {
name: string;
operation: object;
};
/**
*
* @deprecated $$triggerUuid$$
*/
$$triggerTimestamp$$?: number;
};
/**
* Volatile
*
* when: 'commit'
* checkpoint
*
*
* -
* -
* -
* -
*
* @example
* // 判断一个触发器是否是跨事务触发器
* function isVolatileTrigger(trigger: Trigger<ED, T, Cxt>): trigger is VolatileTrigger<ED, T, Cxt> {
* return trigger.when === 'commit';
* }
*/
export type VolatileTrigger<ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED> | SyncContext<ED>> = CreateTriggerCrossTxn<ED, T, Cxt> | UpdateTriggerCrossTxn<ED, T, Cxt> | RemoveTriggerCrossTxn<ED, T, Cxt>;