@yss123/cage-kernel 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +20 -2
- package/dist/brands.d.ts +32 -0
- package/dist/brands.js +35 -0
- package/dist/brands.js.map +1 -0
- package/dist/db/connect.d.ts +28 -0
- package/dist/db/connect.js +57 -0
- package/dist/db/connect.js.map +1 -0
- package/dist/db/ddl.d.ts +69 -0
- package/dist/db/ddl.js +522 -0
- package/dist/db/ddl.js.map +1 -0
- package/dist/db/index.d.ts +5 -0
- package/dist/db/index.js +7 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/migrate.d.ts +22 -0
- package/dist/db/migrate.js +87 -0
- package/dist/db/migrate.js.map +1 -0
- package/dist/db/rows.d.ts +10 -0
- package/dist/db/rows.js +59 -0
- package/dist/db/rows.js.map +1 -0
- package/dist/db/session.d.ts +4 -0
- package/dist/db/session.js +20 -0
- package/dist/db/session.js.map +1 -0
- package/dist/errors.d.ts +44 -0
- package/dist/errors.js +83 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/kernel.d.ts +99 -0
- package/dist/kernel.js +456 -0
- package/dist/kernel.js.map +1 -0
- package/dist/mask.d.ts +13 -0
- package/dist/mask.js +67 -0
- package/dist/mask.js.map +1 -0
- package/dist/org.d.ts +46 -0
- package/dist/org.js +127 -0
- package/dist/org.js.map +1 -0
- package/dist/pipeline/idempotency.d.ts +10 -0
- package/dist/pipeline/idempotency.js +29 -0
- package/dist/pipeline/idempotency.js.map +1 -0
- package/dist/pipeline/load.d.ts +15 -0
- package/dist/pipeline/load.js +87 -0
- package/dist/pipeline/load.js.map +1 -0
- package/dist/primitives/aggregate.d.ts +75 -0
- package/dist/primitives/aggregate.js +69 -0
- package/dist/primitives/aggregate.js.map +1 -0
- package/dist/primitives/command.d.ts +101 -0
- package/dist/primitives/command.js +49 -0
- package/dist/primitives/command.js.map +1 -0
- package/dist/primitives/constraints.d.ts +36 -0
- package/dist/primitives/constraints.js +41 -0
- package/dist/primitives/constraints.js.map +1 -0
- package/dist/primitives/event.d.ts +31 -0
- package/dist/primitives/event.js +44 -0
- package/dist/primitives/event.js.map +1 -0
- package/dist/primitives/fields.d.ts +127 -0
- package/dist/primitives/fields.js +151 -0
- package/dist/primitives/fields.js.map +1 -0
- package/dist/primitives/flow.d.ts +126 -0
- package/dist/primitives/flow.js +90 -0
- package/dist/primitives/flow.js.map +1 -0
- package/dist/primitives/states.d.ts +19 -0
- package/dist/primitives/states.js +37 -0
- package/dist/primitives/states.js.map +1 -0
- package/dist/read.d.ts +53 -0
- package/dist/read.js +217 -0
- package/dist/read.js.map +1 -0
- package/dist/registry.d.ts +71 -0
- package/dist/registry.js +177 -0
- package/dist/registry.js.map +1 -0
- package/dist/relations.d.ts +14 -0
- package/dist/relations.js +58 -0
- package/dist/relations.js.map +1 -0
- package/dist/scope/index.d.ts +42 -0
- package/dist/scope/index.js +90 -0
- package/dist/scope/index.js.map +1 -0
- package/dist/telemetry.d.ts +11 -0
- package/dist/telemetry.js +35 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/transfer/csv.d.ts +8 -0
- package/dist/transfer/csv.js +114 -0
- package/dist/transfer/csv.js.map +1 -0
- package/dist/transfer/index.d.ts +49 -0
- package/dist/transfer/index.js +245 -0
- package/dist/transfer/index.js.map +1 -0
- package/dist/transfer/schema.d.ts +4 -0
- package/dist/transfer/schema.js +31 -0
- package/dist/transfer/schema.js.map +1 -0
- package/dist/types.d.ts +52 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/util/names.d.ts +9 -0
- package/dist/util/names.js +38 -0
- package/dist/util/names.js.map +1 -0
- package/dist/worker/consumers.d.ts +29 -0
- package/dist/worker/consumers.js +79 -0
- package/dist/worker/consumers.js.map +1 -0
- package/dist/worker/index.d.ts +36 -0
- package/dist/worker/index.js +122 -0
- package/dist/worker/index.js.map +1 -0
- package/dist/worker/outbox.d.ts +38 -0
- package/dist/worker/outbox.js +121 -0
- package/dist/worker/outbox.js.map +1 -0
- package/dist/worker/scheduler.d.ts +7 -0
- package/dist/worker/scheduler.js +77 -0
- package/dist/worker/scheduler.js.map +1 -0
- package/package.json +58 -3
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { CapabilityParams, Ctx, MaybePromise } from '../types.js';
|
|
2
|
+
export type ConstraintResult = {
|
|
3
|
+
ok: true;
|
|
4
|
+
} | {
|
|
5
|
+
ok: false;
|
|
6
|
+
message: string;
|
|
7
|
+
};
|
|
8
|
+
export declare const ok: () => ConstraintResult;
|
|
9
|
+
export declare const violation: (message: string) => ConstraintResult;
|
|
10
|
+
export interface ConstraintMeta {
|
|
11
|
+
/** 正在执行的命令的能力点,例如 PurchaseOrder.approve */
|
|
12
|
+
capability: string;
|
|
13
|
+
/** 取某人的能力(额度类约束用) */
|
|
14
|
+
capabilitiesOf(userId: string): Promise<Map<string, CapabilityParams>>;
|
|
15
|
+
/** 按当前主体的范围读取一条记录(框架内置约束用,例如防提权检查要看被分配的角色) */
|
|
16
|
+
load(aggregate: string, id: string): Promise<Record<string, any> | null>;
|
|
17
|
+
/** 能力目录 */
|
|
18
|
+
catalog(): string[];
|
|
19
|
+
}
|
|
20
|
+
export interface Constraint<_A = any> {
|
|
21
|
+
/** 约束名,出现在本体图和报错里 */
|
|
22
|
+
readonly name: string;
|
|
23
|
+
/** 用到的字段,抽取器据此画 uses 边,影响分析能找到它 */
|
|
24
|
+
readonly fields?: readonly string[];
|
|
25
|
+
readonly check: (current: any, ctx: Ctx, input: any, meta: ConstraintMeta) => MaybePromise<ConstraintResult>;
|
|
26
|
+
}
|
|
27
|
+
/** 职责分离:操作人(或系统身份代表的真人)不能是该字段记录的人,例如提单人不能审批 */
|
|
28
|
+
export declare const sod: {
|
|
29
|
+
notSameAs(field: string): Constraint;
|
|
30
|
+
};
|
|
31
|
+
/** 额度:字段值不得超过操作人在该能力点上的 ceiling 参数;多角色取最大值 */
|
|
32
|
+
export declare function ceiling(field: string, param?: string): Constraint;
|
|
33
|
+
/** 二次认证:最近 maxAgeSec 秒内完成过二次认证 */
|
|
34
|
+
export declare function stepUp(maxAgeSec?: number): Constraint;
|
|
35
|
+
/** 自定义约束(仍然是一个纯函数,必须写清楚用到哪些字段) */
|
|
36
|
+
export declare function constraint(name: string, fields: string[], check: Constraint['check']): Constraint;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export const ok = () => ({ ok: true });
|
|
2
|
+
export const violation = (message) => ({ ok: false, message });
|
|
3
|
+
/** 职责分离:操作人(或系统身份代表的真人)不能是该字段记录的人,例如提单人不能审批 */
|
|
4
|
+
export const sod = {
|
|
5
|
+
notSameAs(field) {
|
|
6
|
+
return Object.freeze({
|
|
7
|
+
name: `sod.notSameAs(${field})`,
|
|
8
|
+
fields: [field],
|
|
9
|
+
check: (cur, ctx) => {
|
|
10
|
+
const who = ctx.onBehalfOf ?? ctx.userId;
|
|
11
|
+
return who && cur[field] === who ? violation(`操作人不能是 ${field} 记录的人(职责分离)`) : ok();
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
/** 额度:字段值不得超过操作人在该能力点上的 ceiling 参数;多角色取最大值 */
|
|
17
|
+
export function ceiling(field, param = 'ceiling') {
|
|
18
|
+
return Object.freeze({
|
|
19
|
+
name: `ceiling(${field})`,
|
|
20
|
+
fields: [field],
|
|
21
|
+
check: async (cur, ctx, _input, meta) => {
|
|
22
|
+
const caps = ctx.onBehalfOf ? await meta.capabilitiesOf(ctx.onBehalfOf) : ctx.capabilities;
|
|
23
|
+
const limit = caps.get(meta.capability)?.[param];
|
|
24
|
+
if (limit === undefined || limit === null)
|
|
25
|
+
return violation(`没有为 ${meta.capability} 设置额度 ${param}`);
|
|
26
|
+
return Number(cur[field]) <= Number(limit) ? ok() : violation(`${field}=${cur[field]} 超过额度 ${limit}`);
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/** 二次认证:最近 maxAgeSec 秒内完成过二次认证 */
|
|
31
|
+
export function stepUp(maxAgeSec = 300) {
|
|
32
|
+
return Object.freeze({
|
|
33
|
+
name: 'stepUp',
|
|
34
|
+
check: (_cur, ctx) => ctx.stepUpAt && Date.now() - ctx.stepUpAt <= maxAgeSec * 1000 ? ok() : violation('需要二次认证'),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/** 自定义约束(仍然是一个纯函数,必须写清楚用到哪些字段) */
|
|
38
|
+
export function constraint(name, fields, check) {
|
|
39
|
+
return Object.freeze({ name, fields, check });
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=constraints.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constraints.js","sourceRoot":"","sources":["../../src/primitives/constraints.ts"],"names":[],"mappings":"AAKA,MAAM,CAAC,MAAM,EAAE,GAAG,GAAqB,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;AACzD,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,OAAe,EAAoB,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC;AAqBzF,+CAA+C;AAC/C,MAAM,CAAC,MAAM,GAAG,GAAG;IACjB,SAAS,CAAC,KAAa;QACrB,OAAO,MAAM,CAAC,MAAM,CAAC;YACnB,IAAI,EAAE,iBAAiB,KAAK,GAAG;YAC/B,MAAM,EAAE,CAAC,KAAK,CAAC;YACf,KAAK,EAAE,CAAC,GAAQ,EAAE,GAAQ,EAAE,EAAE;gBAC5B,MAAM,GAAG,GAAG,GAAG,CAAC,UAAU,IAAI,GAAG,CAAC,MAAM,CAAC;gBACzC,OAAO,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,UAAU,KAAK,aAAa,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;YACpF,CAAC;SACF,CAAC,CAAC;IACL,CAAC;CACF,CAAC;AAEF,8CAA8C;AAC9C,MAAM,UAAU,OAAO,CAAC,KAAa,EAAE,KAAK,GAAG,SAAS;IACtD,OAAO,MAAM,CAAC,MAAM,CAAC;QACnB,IAAI,EAAE,WAAW,KAAK,GAAG;QACzB,MAAM,EAAE,CAAC,KAAK,CAAC;QACf,KAAK,EAAE,KAAK,EAAE,GAAQ,EAAE,GAAQ,EAAE,MAAW,EAAE,IAAoB,EAAE,EAAE;YACrE,MAAM,IAAI,GAAG,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,YAAY,CAAC;YAC3F,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC;YACjD,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI;gBAAE,OAAO,SAAS,CAAC,OAAO,IAAI,CAAC,UAAU,SAAS,KAAK,EAAE,CAAC,CAAC;YACpG,OAAO,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,KAAK,IAAI,GAAG,CAAC,KAAK,CAAC,SAAS,KAAK,EAAE,CAAC,CAAC;QACxG,CAAC;KACF,CAAC,CAAC;AACL,CAAC;AAED,kCAAkC;AAClC,MAAM,UAAU,MAAM,CAAC,SAAS,GAAG,GAAG;IACpC,OAAO,MAAM,CAAC,MAAM,CAAC;QACnB,IAAI,EAAE,QAAQ;QACd,KAAK,EAAE,CAAC,IAAS,EAAE,GAAQ,EAAE,EAAE,CAC7B,GAAG,CAAC,QAAQ,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,GAAG,CAAC,QAAQ,IAAI,SAAS,GAAG,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC;KAC7F,CAAC,CAAC;AACL,CAAC;AAED,kCAAkC;AAClC,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,MAAgB,EAAE,KAA0B;IACnF,OAAO,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;AAChD,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { EVENT } from '../brands.js';
|
|
3
|
+
import { type FieldValues, type Fields } from './fields.js';
|
|
4
|
+
export interface Event<N extends string = string, F extends Fields = Fields> {
|
|
5
|
+
readonly [EVENT]: true;
|
|
6
|
+
readonly name: N;
|
|
7
|
+
readonly fields: F;
|
|
8
|
+
readonly schema: z.ZodObject<any>;
|
|
9
|
+
readonly description?: string;
|
|
10
|
+
}
|
|
11
|
+
export type AnyEvent = Event<string, any>;
|
|
12
|
+
export type PayloadOf<E> = E extends Event<any, infer F> ? FieldValues<F> : never;
|
|
13
|
+
export declare function event<const N extends string, F extends Fields>(name: N, fields: F, opts?: {
|
|
14
|
+
description?: string;
|
|
15
|
+
}): Event<N, F>;
|
|
16
|
+
export declare function isEvent(v: unknown): v is AnyEvent;
|
|
17
|
+
/** 显式指定载荷怎么从快照得到:emits: [emit(OrderApproved, (o) => ({ orderId: o.id, items: o.items }))] */
|
|
18
|
+
export interface EmitSpec<E extends AnyEvent = AnyEvent> {
|
|
19
|
+
readonly event: E;
|
|
20
|
+
/** snapshot:写入后的快照;input:命令输入;previous:写入前的快照(新建时为 null) */
|
|
21
|
+
readonly map?: (snapshot: any, input: any, previous: any) => PayloadOf<E>;
|
|
22
|
+
}
|
|
23
|
+
export declare function emit<E extends AnyEvent>(ev: E, map: (snapshot: any, input: any, previous: any) => PayloadOf<E>): EmitSpec<E>;
|
|
24
|
+
export declare function toEmitSpec(e: AnyEvent | EmitSpec): EmitSpec;
|
|
25
|
+
/**
|
|
26
|
+
* 默认载荷映射规则(没有写 emit(...) 时):
|
|
27
|
+
* 1. 载荷字段名在快照里存在 → 取快照的值
|
|
28
|
+
* 2. 载荷字段名以 Id 结尾、快照里没有同名字段 → 取聚合自己的 id(例如 orderId)
|
|
29
|
+
* 3. 其他 → 取命令输入里的同名值
|
|
30
|
+
*/
|
|
31
|
+
export declare function defaultPayload(ev: AnyEvent, snapshot: Record<string, unknown>, input: Record<string, unknown>): Record<string, unknown>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { EVENT } from '../brands.js';
|
|
2
|
+
import { declError } from '../errors.js';
|
|
3
|
+
import { isRelation, objectSchema } from './fields.js';
|
|
4
|
+
export function event(name, fields, opts) {
|
|
5
|
+
if (!/^[A-Z][A-Za-z0-9]*$/.test(name))
|
|
6
|
+
throw declError('CAGE-NAME-003', `事件名 ${name} 必须是大驼峰`);
|
|
7
|
+
if (Object.values(fields).some(isRelation))
|
|
8
|
+
throw declError('CAGE-REL-001', '事件载荷不声明持久化关系;传递 ID 数组请使用 list(id())');
|
|
9
|
+
return Object.freeze({
|
|
10
|
+
[EVENT]: true,
|
|
11
|
+
name,
|
|
12
|
+
fields: Object.freeze({ ...fields }),
|
|
13
|
+
schema: objectSchema(fields),
|
|
14
|
+
description: opts?.description,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
export function isEvent(v) {
|
|
18
|
+
return !!v && typeof v === 'object' && v[EVENT] === true;
|
|
19
|
+
}
|
|
20
|
+
export function emit(ev, map) {
|
|
21
|
+
return Object.freeze({ event: ev, map });
|
|
22
|
+
}
|
|
23
|
+
export function toEmitSpec(e) {
|
|
24
|
+
return isEvent(e) ? { event: e } : e;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* 默认载荷映射规则(没有写 emit(...) 时):
|
|
28
|
+
* 1. 载荷字段名在快照里存在 → 取快照的值
|
|
29
|
+
* 2. 载荷字段名以 Id 结尾、快照里没有同名字段 → 取聚合自己的 id(例如 orderId)
|
|
30
|
+
* 3. 其他 → 取命令输入里的同名值
|
|
31
|
+
*/
|
|
32
|
+
export function defaultPayload(ev, snapshot, input) {
|
|
33
|
+
const out = {};
|
|
34
|
+
for (const k of Object.keys(ev.fields)) {
|
|
35
|
+
if (k in snapshot)
|
|
36
|
+
out[k] = snapshot[k];
|
|
37
|
+
else if (k.endsWith('Id'))
|
|
38
|
+
out[k] = snapshot.id;
|
|
39
|
+
else if (input && k in input)
|
|
40
|
+
out[k] = input[k];
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=event.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event.js","sourceRoot":"","sources":["../../src/primitives/event.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AACrC,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAiC,UAAU,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAatF,MAAM,UAAU,KAAK,CAA2C,IAAO,EAAE,MAAS,EAAE,IAA+B;IACjH,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,MAAM,SAAS,CAAC,eAAe,EAAE,OAAO,IAAI,SAAS,CAAC,CAAC;IAC9F,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC;QAAE,MAAM,SAAS,CAAC,cAAc,EAAE,qCAAqC,CAAC,CAAC;IACnH,OAAO,MAAM,CAAC,MAAM,CAAC;QACnB,CAAC,KAAK,CAAC,EAAE,IAAa;QACtB,IAAI;QACJ,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,MAAM,EAAE,CAAC;QACpC,MAAM,EAAE,YAAY,CAAC,MAAM,CAAC;QAC5B,WAAW,EAAE,IAAI,EAAE,WAAW;KAC/B,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,CAAU;IAChC,OAAO,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAK,CAAS,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC;AACpE,CAAC;AASD,MAAM,UAAU,IAAI,CAAqB,EAAK,EAAE,GAA+D;IAC7G,OAAO,MAAM,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;AAC3C,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,CAAsB;IAC/C,OAAO,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;AACvC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,EAAY,EAAE,QAAiC,EAAE,KAA8B;IAC5G,MAAM,GAAG,GAA4B,EAAE,CAAC;IACxC,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC;QACvC,IAAI,CAAC,IAAI,QAAQ;YAAE,GAAG,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;aACnC,IAAI,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC;YAAE,GAAG,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC;aAC3C,IAAI,KAAK,IAAI,CAAC,IAAI,KAAK;YAAE,GAAG,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IAClD,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/** 敏感种类是封闭集合,每种在内核里有一个掩码函数(见 db/ddl.ts 的 sys.mask)。 */
|
|
3
|
+
export type SensitiveKind = 'phone' | 'idCard' | 'bankCard' | 'email' | 'name' | 'address';
|
|
4
|
+
export declare const SENSITIVE_KINDS: readonly SensitiveKind[];
|
|
5
|
+
export type FieldKind = 'id' | 'ref' | 'text' | 'int' | 'number' | 'money' | 'date' | 'datetime' | 'bool' | 'enum' | 'list' | 'hasMany' | 'manyToMany';
|
|
6
|
+
export interface FieldMeta {
|
|
7
|
+
readonly kind: FieldKind;
|
|
8
|
+
readonly sql: string;
|
|
9
|
+
readonly isOptional: boolean;
|
|
10
|
+
readonly sensitiveKind?: SensitiveKind;
|
|
11
|
+
/** 引用的聚合名,抽取器据此画引用边,生成外键 */
|
|
12
|
+
readonly ref?: string;
|
|
13
|
+
/** hasMany 的反向外键。关系字段不占主表列,sql 为空字符串。 */
|
|
14
|
+
readonly mappedBy?: string;
|
|
15
|
+
readonly isUnique?: boolean;
|
|
16
|
+
/** 隐藏字段:不进入安全视图、审计、事件、前端契约(例如密码哈希) */
|
|
17
|
+
readonly isHidden?: boolean;
|
|
18
|
+
/** 口径说明:变化会出现在影响分析里 */
|
|
19
|
+
readonly description?: string;
|
|
20
|
+
/** enum 的取值 */
|
|
21
|
+
readonly values?: readonly string[];
|
|
22
|
+
/** list 的元素结构;标量数组时为 { item } */
|
|
23
|
+
readonly shape?: Readonly<Record<string, AnyField>>;
|
|
24
|
+
/** 文本最大长度 */
|
|
25
|
+
readonly max?: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 字段定义。方法都返回新对象(不可变),可以链式调用:
|
|
29
|
+
* text(50).sensitive('phone').optional()
|
|
30
|
+
*/
|
|
31
|
+
export declare class Field<T extends z.ZodType = z.ZodType, Opt extends boolean = boolean, K extends FieldKind = FieldKind> implements FieldMeta {
|
|
32
|
+
readonly kind: K;
|
|
33
|
+
readonly sql: string;
|
|
34
|
+
readonly isOptional: boolean;
|
|
35
|
+
readonly sensitiveKind?: SensitiveKind;
|
|
36
|
+
readonly ref?: string;
|
|
37
|
+
readonly mappedBy?: string;
|
|
38
|
+
readonly isUnique?: boolean;
|
|
39
|
+
readonly isHidden?: boolean;
|
|
40
|
+
readonly description?: string;
|
|
41
|
+
readonly values?: readonly string[];
|
|
42
|
+
readonly shape?: Readonly<Record<string, AnyField>>;
|
|
43
|
+
readonly max?: number;
|
|
44
|
+
/** 基础校验(不含可空) */
|
|
45
|
+
readonly base: T;
|
|
46
|
+
/** 仅用于类型推导 */
|
|
47
|
+
readonly __opt?: Opt;
|
|
48
|
+
constructor(base: T, meta: Omit<FieldMeta, 'kind' | 'isOptional'> & {
|
|
49
|
+
kind: K;
|
|
50
|
+
isOptional?: boolean;
|
|
51
|
+
});
|
|
52
|
+
meta(): FieldMeta & {
|
|
53
|
+
kind: K;
|
|
54
|
+
};
|
|
55
|
+
private with;
|
|
56
|
+
/** 校验规则(前后端共用);可选字段允许 null 与缺省 */
|
|
57
|
+
get zod(): z.ZodType;
|
|
58
|
+
/** 可选字段:列可空,输入可以不填 */
|
|
59
|
+
optional(): Field<T, true, K>;
|
|
60
|
+
/** 敏感字段:读侧按种类掩码,持有 聚合.unmask.种类 能力才能看明文 */
|
|
61
|
+
sensitive(kind: SensitiveKind): Field<T, Opt, K>;
|
|
62
|
+
/** 唯一约束(租户内) */
|
|
63
|
+
unique(): Field<T, Opt, K>;
|
|
64
|
+
/** 隐藏字段:不出现在读侧、审计、事件和前端契约 */
|
|
65
|
+
hidden(): Field<T, Opt, K>;
|
|
66
|
+
/** 口径说明 */
|
|
67
|
+
describe(text: string): Field<T, Opt, K>;
|
|
68
|
+
}
|
|
69
|
+
export type AnyField = Field<z.ZodType, boolean>;
|
|
70
|
+
export type Fields = Record<string, AnyField>;
|
|
71
|
+
/** 字段的 TypeScript 值类型 */
|
|
72
|
+
export type ValueOf<F> = F extends Field<infer T, infer O, any> ? ([O] extends [true] ? z.output<T> | null : z.output<T>) : never;
|
|
73
|
+
export type FieldValues<F extends Fields> = {
|
|
74
|
+
-readonly [K in keyof F as F[K]['kind'] extends 'hasMany' ? never : K]: ValueOf<F[K]>;
|
|
75
|
+
};
|
|
76
|
+
/** 文本,默认最长 500 字 */
|
|
77
|
+
export declare const text: (max?: number) => Field<z.ZodString, false, "text">;
|
|
78
|
+
/** 整数 */
|
|
79
|
+
export declare const int: () => Field<z.ZodNumber, false, "int">;
|
|
80
|
+
/** 数值(双精度)。金额请用 money() */
|
|
81
|
+
export declare const number: () => Field<z.ZodNumber, false, "number">;
|
|
82
|
+
/** 金额:用字符串传输,避免浮点误差 */
|
|
83
|
+
export declare const money: () => Field<z.ZodString, false, "money">;
|
|
84
|
+
/** 主键或外键 */
|
|
85
|
+
export declare const id: () => Field<z.ZodString, false, "id">;
|
|
86
|
+
/** 日期 YYYY-MM-DD */
|
|
87
|
+
export declare const date: () => Field<z.ZodString, false, "date">;
|
|
88
|
+
/** 时间戳(ISO 8601) */
|
|
89
|
+
export declare const datetime: () => Field<z.ZodString, false, "datetime">;
|
|
90
|
+
/** 布尔 */
|
|
91
|
+
export declare const bool: () => Field<z.ZodBoolean, false, "bool">;
|
|
92
|
+
/** 枚举:enumOf('A', 'B') */
|
|
93
|
+
export declare function enumOf<const V extends readonly [string, ...string[]]>(...values: V): Field<z.ZodEnum<{ [k_1 in V[number]]: k_1; } extends infer T ? { [k in keyof T]: T[k]; } : never>, false, "enum">;
|
|
94
|
+
/** 引用另一个聚合:本质是 uuid,额外记下引用目标。可传聚合对象或聚合名(解决循环导入)。 */
|
|
95
|
+
export declare function ref(to: {
|
|
96
|
+
name: string;
|
|
97
|
+
} | string): Field<z.ZodString, false, "ref">;
|
|
98
|
+
/** 一对多反查:实际外键在目标聚合上,不创建另一份关系或可写数组。 */
|
|
99
|
+
export declare function hasMany(to: {
|
|
100
|
+
name: string;
|
|
101
|
+
} | string, options: {
|
|
102
|
+
mappedBy: string;
|
|
103
|
+
description?: string;
|
|
104
|
+
}): Field<z.ZodArray<z.ZodString>, false, "hasMany">;
|
|
105
|
+
/** 多对多:字段与普通属性一处声明,中间表由框架生成;业务命令返回目标 ID 数组。 */
|
|
106
|
+
export declare function manyToMany(to: {
|
|
107
|
+
name: string;
|
|
108
|
+
} | string, options?: {
|
|
109
|
+
description?: string;
|
|
110
|
+
}): Field<z.ZodArray<z.ZodString>, false, "manyToMany">;
|
|
111
|
+
export declare const isRelation: (field: FieldMeta) => boolean;
|
|
112
|
+
type ShapeZod<S extends Fields> = {
|
|
113
|
+
[K in keyof S]: S[K] extends Field<infer T, infer O> ? ([O] extends [true] ? z.ZodOptional<z.ZodNullable<T>> : T) : never;
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* 明细行,存为 jsonb:
|
|
117
|
+
* list({ sku: text(), qty: int() }) 对象数组
|
|
118
|
+
* list(id()) 标量数组
|
|
119
|
+
*/
|
|
120
|
+
export declare function list<T extends z.ZodType>(item: Field<T, false>): Field<z.ZodArray<T>, false>;
|
|
121
|
+
export declare function list<S extends Fields>(shape: S): Field<z.ZodArray<z.ZodObject<ShapeZod<S>>>, false>;
|
|
122
|
+
export declare function isField(v: unknown): v is AnyField;
|
|
123
|
+
/** 把一组字段拼成对象 schema */
|
|
124
|
+
export declare function objectSchema(fields: Fields): z.ZodObject<any>;
|
|
125
|
+
/** 结构不固定的 JSON 对象(例如能力点参数),存为 jsonb。业务字段请优先用 list 或具体类型。 */
|
|
126
|
+
export declare const json: () => Field<z.ZodRecord<z.ZodString, z.ZodUnknown>, false, "list">;
|
|
127
|
+
export {};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// 字段原语:每个字段同时携带校验(Zod)、存储(SQL 列类型)和元数据(敏感种类、引用目标)。
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { declError } from '../errors.js';
|
|
4
|
+
export const SENSITIVE_KINDS = ['phone', 'idCard', 'bankCard', 'email', 'name', 'address'];
|
|
5
|
+
/**
|
|
6
|
+
* 字段定义。方法都返回新对象(不可变),可以链式调用:
|
|
7
|
+
* text(50).sensitive('phone').optional()
|
|
8
|
+
*/
|
|
9
|
+
export class Field {
|
|
10
|
+
kind;
|
|
11
|
+
sql;
|
|
12
|
+
isOptional;
|
|
13
|
+
sensitiveKind;
|
|
14
|
+
ref;
|
|
15
|
+
mappedBy;
|
|
16
|
+
isUnique;
|
|
17
|
+
isHidden;
|
|
18
|
+
description;
|
|
19
|
+
values;
|
|
20
|
+
shape;
|
|
21
|
+
max;
|
|
22
|
+
/** 基础校验(不含可空) */
|
|
23
|
+
base;
|
|
24
|
+
constructor(base, meta) {
|
|
25
|
+
this.base = base;
|
|
26
|
+
this.kind = meta.kind;
|
|
27
|
+
this.sql = meta.sql;
|
|
28
|
+
this.isOptional = meta.isOptional ?? false;
|
|
29
|
+
this.sensitiveKind = meta.sensitiveKind;
|
|
30
|
+
this.ref = meta.ref;
|
|
31
|
+
this.mappedBy = meta.mappedBy;
|
|
32
|
+
this.isUnique = meta.isUnique;
|
|
33
|
+
this.isHidden = meta.isHidden;
|
|
34
|
+
this.description = meta.description;
|
|
35
|
+
this.values = meta.values;
|
|
36
|
+
this.shape = meta.shape;
|
|
37
|
+
this.max = meta.max;
|
|
38
|
+
Object.freeze(this);
|
|
39
|
+
}
|
|
40
|
+
meta() {
|
|
41
|
+
return {
|
|
42
|
+
kind: this.kind, sql: this.sql, isOptional: this.isOptional, sensitiveKind: this.sensitiveKind, ref: this.ref,
|
|
43
|
+
isUnique: this.isUnique, isHidden: this.isHidden, description: this.description, values: this.values,
|
|
44
|
+
shape: this.shape, max: this.max,
|
|
45
|
+
mappedBy: this.mappedBy,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
with(patch) {
|
|
49
|
+
return new Field(this.base, { ...this.meta(), ...patch });
|
|
50
|
+
}
|
|
51
|
+
/** 校验规则(前后端共用);可选字段允许 null 与缺省 */
|
|
52
|
+
get zod() {
|
|
53
|
+
return this.isOptional ? this.base.nullable().optional() : this.base;
|
|
54
|
+
}
|
|
55
|
+
/** 可选字段:列可空,输入可以不填 */
|
|
56
|
+
optional() {
|
|
57
|
+
if (isRelation(this))
|
|
58
|
+
throw declError('CAGE-REL-001', '集合关系使用空集合表示没有关联,不支持 optional');
|
|
59
|
+
return this.with({ isOptional: true });
|
|
60
|
+
}
|
|
61
|
+
/** 敏感字段:读侧按种类掩码,持有 聚合.unmask.种类 能力才能看明文 */
|
|
62
|
+
sensitive(kind) {
|
|
63
|
+
if (isRelation(this))
|
|
64
|
+
throw declError('CAGE-REL-001', '关系字段不支持 sensitive;目标字段按目标聚合权限掩码');
|
|
65
|
+
if (!SENSITIVE_KINDS.includes(kind))
|
|
66
|
+
throw declError('CAGE-FIELD-001', `未知的敏感种类 ${kind}`);
|
|
67
|
+
return this.with({ sensitiveKind: kind });
|
|
68
|
+
}
|
|
69
|
+
/** 唯一约束(租户内) */
|
|
70
|
+
unique() {
|
|
71
|
+
if (isRelation(this))
|
|
72
|
+
throw declError('CAGE-REL-001', '集合关系自动保证关联对唯一,不支持单字段 unique');
|
|
73
|
+
return this.with({ isUnique: true });
|
|
74
|
+
}
|
|
75
|
+
/** 隐藏字段:不出现在读侧、审计、事件和前端契约 */
|
|
76
|
+
hidden() {
|
|
77
|
+
if (isRelation(this))
|
|
78
|
+
throw declError('CAGE-REL-001', '关系使用所属对象和目标对象权限,不支持 hidden 修饰');
|
|
79
|
+
return this.with({ isHidden: true });
|
|
80
|
+
}
|
|
81
|
+
/** 口径说明 */
|
|
82
|
+
describe(text) {
|
|
83
|
+
return this.with({ description: text });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// ---------- 字段构造函数 ----------
|
|
87
|
+
function mk(base, meta) {
|
|
88
|
+
return new Field(base, meta);
|
|
89
|
+
}
|
|
90
|
+
/** 文本,默认最长 500 字 */
|
|
91
|
+
export const text = (max = 500) => mk(z.string().max(max), { kind: 'text', sql: 'text', max });
|
|
92
|
+
/** 整数 */
|
|
93
|
+
export const int = () => mk(z.number().int(), { kind: 'int', sql: 'integer' });
|
|
94
|
+
/** 数值(双精度)。金额请用 money() */
|
|
95
|
+
export const number = () => mk(z.number(), { kind: 'number', sql: 'double precision' });
|
|
96
|
+
/** 金额:用字符串传输,避免浮点误差 */
|
|
97
|
+
export const money = () => mk(z.string().regex(/^-?\d+(\.\d{1,2})?$/, '金额格式应为最多两位小数的数字字符串'), { kind: 'money', sql: 'numeric(18,2)' });
|
|
98
|
+
/** 主键或外键 */
|
|
99
|
+
export const id = () => mk(z.string().uuid(), { kind: 'id', sql: 'uuid' });
|
|
100
|
+
/** 日期 YYYY-MM-DD */
|
|
101
|
+
export const date = () => mk(z.string().regex(/^\d{4}-\d{2}-\d{2}$/, '日期格式应为 YYYY-MM-DD'), { kind: 'date', sql: 'date' });
|
|
102
|
+
/** 时间戳(ISO 8601) */
|
|
103
|
+
export const datetime = () => mk(z.string().datetime({ offset: true }), { kind: 'datetime', sql: 'timestamptz' });
|
|
104
|
+
/** 布尔 */
|
|
105
|
+
export const bool = () => mk(z.boolean(), { kind: 'bool', sql: 'boolean' });
|
|
106
|
+
/** 枚举:enumOf('A', 'B') */
|
|
107
|
+
export function enumOf(...values) {
|
|
108
|
+
return mk(z.enum(values), { kind: 'enum', sql: 'text', values });
|
|
109
|
+
}
|
|
110
|
+
/** 引用另一个聚合:本质是 uuid,额外记下引用目标。可传聚合对象或聚合名(解决循环导入)。 */
|
|
111
|
+
export function ref(to) {
|
|
112
|
+
const target = typeof to === 'string' ? to : to?.name;
|
|
113
|
+
if (!target)
|
|
114
|
+
throw declError('CAGE-FIELD-002', 'ref() 的目标为空,可能是循环导入;可改为 ref("聚合名")');
|
|
115
|
+
return mk(z.string().uuid(), { kind: 'ref', sql: 'uuid', ref: target });
|
|
116
|
+
}
|
|
117
|
+
/** 一对多反查:实际外键在目标聚合上,不创建另一份关系或可写数组。 */
|
|
118
|
+
export function hasMany(to, options) {
|
|
119
|
+
const target = typeof to === 'string' ? to : to?.name;
|
|
120
|
+
if (!target)
|
|
121
|
+
throw declError('CAGE-REL-001', 'hasMany() 需要目标聚合;循环引用请使用聚合名字符串');
|
|
122
|
+
return mk(z.array(z.string().uuid()), { kind: 'hasMany', sql: '', ref: target, ...options });
|
|
123
|
+
}
|
|
124
|
+
/** 多对多:字段与普通属性一处声明,中间表由框架生成;业务命令返回目标 ID 数组。 */
|
|
125
|
+
export function manyToMany(to, options = {}) {
|
|
126
|
+
const target = typeof to === 'string' ? to : to?.name;
|
|
127
|
+
if (!target)
|
|
128
|
+
throw declError('CAGE-REL-001', 'manyToMany() 需要目标聚合;循环引用请使用聚合名字符串');
|
|
129
|
+
return mk(z.array(z.string().uuid()), { kind: 'manyToMany', sql: '', ref: target, ...options });
|
|
130
|
+
}
|
|
131
|
+
export const isRelation = (field) => field.kind === 'hasMany' || field.kind === 'manyToMany';
|
|
132
|
+
export function list(arg) {
|
|
133
|
+
const members = isField(arg) ? [arg] : Object.values(arg);
|
|
134
|
+
if (members.some(isRelation))
|
|
135
|
+
throw declError('CAGE-REL-001', '关系字段只能直接声明在聚合 fields 中,不能嵌入 JSON 明细');
|
|
136
|
+
if (isField(arg)) {
|
|
137
|
+
return mk(z.array(arg.zod), { kind: 'list', sql: 'jsonb', shape: { item: arg } });
|
|
138
|
+
}
|
|
139
|
+
const item = z.object(Object.fromEntries(Object.entries(arg).map(([k, f]) => [k, f.zod])));
|
|
140
|
+
return mk(z.array(item), { kind: 'list', sql: 'jsonb', shape: arg });
|
|
141
|
+
}
|
|
142
|
+
export function isField(v) {
|
|
143
|
+
return v instanceof Field || (!!v && typeof v === 'object' && 'kind' in v && 'base' in v && 'sql' in v);
|
|
144
|
+
}
|
|
145
|
+
/** 把一组字段拼成对象 schema */
|
|
146
|
+
export function objectSchema(fields) {
|
|
147
|
+
return z.object(Object.fromEntries(Object.entries(fields).filter(([, f]) => f.kind !== 'hasMany').map(([k, f]) => [k, f.zod])));
|
|
148
|
+
}
|
|
149
|
+
/** 结构不固定的 JSON 对象(例如能力点参数),存为 jsonb。业务字段请优先用 list 或具体类型。 */
|
|
150
|
+
export const json = () => mk(z.record(z.string(), z.unknown()), { kind: 'list', sql: 'jsonb' });
|
|
151
|
+
//# sourceMappingURL=fields.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fields.js","sourceRoot":"","sources":["../../src/primitives/fields.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { Kysely } from 'kysely';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { APPROVAL, JOB, QUERY, REACTION } from '../brands.js';
|
|
4
|
+
import type { AnyAggregate, FieldsOf, SnapshotOf } from './aggregate.js';
|
|
5
|
+
import type { AnyCommand } from './command.js';
|
|
6
|
+
import type { AnyEvent, PayloadOf } from './event.js';
|
|
7
|
+
export type TxMode = 'outbox' | 'same';
|
|
8
|
+
export interface Reaction<E extends AnyEvent = AnyEvent, C extends AnyCommand = AnyCommand> {
|
|
9
|
+
readonly [REACTION]: true;
|
|
10
|
+
readonly event: E;
|
|
11
|
+
readonly command: C;
|
|
12
|
+
/** 作用的聚合(等于 command.agg,写出来便于阅读) */
|
|
13
|
+
readonly target: AnyAggregate;
|
|
14
|
+
/** 把事件载荷映射成命令输入(纯函数);返回 null 表示这次不处理 */
|
|
15
|
+
readonly map: (e: PayloadOf<E>) => z.input<C['input']> | null;
|
|
16
|
+
/** 事务语义:outbox 异步(默认,见 D3);same 与触发命令同一事务 */
|
|
17
|
+
readonly tx: TxMode;
|
|
18
|
+
/** 获得的资源,供资源配对检查使用 */
|
|
19
|
+
readonly acquires?: string;
|
|
20
|
+
/** 释放的资源 */
|
|
21
|
+
readonly releases?: string;
|
|
22
|
+
readonly name?: string;
|
|
23
|
+
}
|
|
24
|
+
export type AnyReaction = Reaction<AnyEvent, AnyCommand>;
|
|
25
|
+
export declare function reaction<E extends AnyEvent, C extends AnyCommand>(ev: E, def: {
|
|
26
|
+
command: C;
|
|
27
|
+
map: (e: PayloadOf<E>) => z.input<C['input']> | null;
|
|
28
|
+
target?: AnyAggregate;
|
|
29
|
+
tx?: TxMode;
|
|
30
|
+
acquires?: string;
|
|
31
|
+
releases?: string;
|
|
32
|
+
name?: string;
|
|
33
|
+
}): Reaction<E, C>;
|
|
34
|
+
export interface Query<I extends z.ZodType = z.ZodType> {
|
|
35
|
+
readonly [QUERY]: true;
|
|
36
|
+
readonly name: string;
|
|
37
|
+
/** 查询读了哪些聚合:只能引用它们的安全视图;调用者需要每个聚合的 read 能力 */
|
|
38
|
+
readonly reads: readonly AnyAggregate[];
|
|
39
|
+
readonly input: I;
|
|
40
|
+
/** 手写 SQL:只能引用 safe.* 视图,参数用 :name 占位 */
|
|
41
|
+
readonly sql?: string;
|
|
42
|
+
/** 查询构建器写法:db 是只读账号上的 Kysely,只映射安全视图 */
|
|
43
|
+
readonly build?: (db: Kysely<any>, input: z.output<I>) => {
|
|
44
|
+
execute(): Promise<unknown[]>;
|
|
45
|
+
};
|
|
46
|
+
readonly description?: string;
|
|
47
|
+
}
|
|
48
|
+
export type AnyQuery = Query<z.ZodType>;
|
|
49
|
+
export declare function query<I extends z.ZodType = z.ZodObject<{}>>(name: string, def: {
|
|
50
|
+
reads: AnyAggregate[];
|
|
51
|
+
input?: I;
|
|
52
|
+
sql?: string;
|
|
53
|
+
build?: (db: Kysely<any>, input: z.output<I>) => {
|
|
54
|
+
execute(): Promise<unknown[]>;
|
|
55
|
+
};
|
|
56
|
+
description?: string;
|
|
57
|
+
}): Query<I>;
|
|
58
|
+
export interface Approval<A extends AnyAggregate = AnyAggregate> {
|
|
59
|
+
readonly [APPROVAL]: true;
|
|
60
|
+
readonly agg: A;
|
|
61
|
+
/** 流程名,例如 po-approval */
|
|
62
|
+
readonly process: string;
|
|
63
|
+
/** 哪个事件启动流程 */
|
|
64
|
+
readonly start: AnyEvent;
|
|
65
|
+
/** 哪些事件取消流程(例如撤回、取消) */
|
|
66
|
+
readonly cancelOn: readonly AnyEvent[];
|
|
67
|
+
/** 可路由字段:只有它们会作为流程变量传给引擎 */
|
|
68
|
+
readonly routable: readonly (keyof FieldsOf<A> & string)[];
|
|
69
|
+
/** 通过与驳回分别落到哪个命令(必须 invokedBy: process) */
|
|
70
|
+
readonly onApproved: AnyCommand;
|
|
71
|
+
readonly onRejected: AnyCommand;
|
|
72
|
+
}
|
|
73
|
+
export declare function approval<A extends AnyAggregate>(agg: A, def: {
|
|
74
|
+
process: string;
|
|
75
|
+
start: AnyEvent;
|
|
76
|
+
cancelOn?: AnyEvent[];
|
|
77
|
+
routable: (keyof FieldsOf<A> & string)[];
|
|
78
|
+
onApproved: AnyCommand;
|
|
79
|
+
onRejected: AnyCommand;
|
|
80
|
+
}): Approval<A>;
|
|
81
|
+
export interface JobQueryApi {
|
|
82
|
+
/** 只读查询:等值条件,另支持 createdBefore / createdAfter / updatedBefore */
|
|
83
|
+
list<A extends AnyAggregate>(agg: A, where?: Partial<SnapshotOf<A>> & {
|
|
84
|
+
createdBefore?: Date | string;
|
|
85
|
+
createdAfter?: Date | string;
|
|
86
|
+
updatedBefore?: Date | string;
|
|
87
|
+
}, opts?: {
|
|
88
|
+
limit?: number;
|
|
89
|
+
}): Promise<SnapshotOf<A>[]>;
|
|
90
|
+
}
|
|
91
|
+
export interface JobApi {
|
|
92
|
+
query: JobQueryApi;
|
|
93
|
+
/** 只能调用 calls 里声明过的命令;每次调用都走完整流水线,审计 source = job */
|
|
94
|
+
call<C extends AnyCommand>(cmd: C, input: z.input<C['input']>): Promise<SnapshotOf<C['agg']>>;
|
|
95
|
+
tenantId: string;
|
|
96
|
+
now: Date;
|
|
97
|
+
log: (msg: string, extra?: Record<string, unknown>) => void;
|
|
98
|
+
}
|
|
99
|
+
export interface Job {
|
|
100
|
+
readonly [JOB]: true;
|
|
101
|
+
readonly name: string;
|
|
102
|
+
/** cron 表达式,例如 0 2 * * * */
|
|
103
|
+
readonly cron: string;
|
|
104
|
+
readonly reads: readonly AnyAggregate[];
|
|
105
|
+
readonly calls: readonly AnyCommand[];
|
|
106
|
+
readonly run: (api: JobApi) => Promise<void>;
|
|
107
|
+
readonly description?: string;
|
|
108
|
+
}
|
|
109
|
+
export declare function job(name: string, def: {
|
|
110
|
+
cron: string;
|
|
111
|
+
reads?: AnyAggregate[];
|
|
112
|
+
calls?: AnyCommand[];
|
|
113
|
+
run: (api: JobApi) => Promise<void>;
|
|
114
|
+
description?: string;
|
|
115
|
+
}): Job;
|
|
116
|
+
/** 前 N 天 */
|
|
117
|
+
export declare function daysAgo(n: number, from?: Date): Date;
|
|
118
|
+
export interface UnsafeTag {
|
|
119
|
+
readonly reason: string;
|
|
120
|
+
readonly owner: string;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* 逃生口:声明表达不了时使用,必须写明原因和负责人。lint 检查参数,计数进 PR 摘要。
|
|
124
|
+
* const v = unsafe({ reason: '…', owner: 'zhangsan' }, () => …)
|
|
125
|
+
*/
|
|
126
|
+
export declare function unsafe<T>(tag: UnsafeTag, fn: () => T): T;
|