@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
package/dist/org.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { COMBINATORS } from './types.js';
|
|
2
|
+
/** 多角色能力取并集,数值参数(额度)取最大值 */
|
|
3
|
+
export function mergeCaps(roles) {
|
|
4
|
+
const out = new Map();
|
|
5
|
+
for (const r of roles) {
|
|
6
|
+
for (const c of r.capabilities ?? []) {
|
|
7
|
+
const prev = out.get(c.capability);
|
|
8
|
+
const next = { ...(prev ?? {}) };
|
|
9
|
+
for (const [k, v] of Object.entries(c.params ?? {})) {
|
|
10
|
+
const pv = next[k];
|
|
11
|
+
next[k] = typeof v === 'number' && typeof pv === 'number' ? Math.max(v, pv) : pv ?? v;
|
|
12
|
+
}
|
|
13
|
+
out.set(c.capability, next);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
/** 多角色范围取并集:同一聚合的多个组合子用 OR 连接;deptSet 的部门合并 */
|
|
19
|
+
export function mergeScopes(roles) {
|
|
20
|
+
const out = {};
|
|
21
|
+
for (const r of roles) {
|
|
22
|
+
for (const s of r.scopes ?? []) {
|
|
23
|
+
if (!COMBINATORS.includes(s.combinator))
|
|
24
|
+
continue; // 授权数据异常时宁可少给
|
|
25
|
+
const list = (out[s.aggregate] ??= []);
|
|
26
|
+
const same = list.find((g) => g.combinator === s.combinator);
|
|
27
|
+
if (same && s.combinator === 'deptSet')
|
|
28
|
+
same.deptIds = [...new Set([...(same.deptIds ?? []), ...(s.deptIds ?? [])])];
|
|
29
|
+
else if (!same)
|
|
30
|
+
list.push(s.combinator === 'deptSet' ? { combinator: 'deptSet', deptIds: [...(s.deptIds ?? [])] } : { combinator: s.combinator });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
/** 能力点 X.unmask.kind → 明文权限 X.kind */
|
|
36
|
+
export function unmaskFrom(caps) {
|
|
37
|
+
const out = [];
|
|
38
|
+
for (const cap of caps.keys()) {
|
|
39
|
+
const m = /^([A-Z*][A-Za-z0-9]*)\.unmask\.([a-zA-Z]+)$/.exec(cap);
|
|
40
|
+
if (m)
|
|
41
|
+
out.push(`${m[1]}.${m[2]}`);
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
/** 从系统表读取一个用户的授权(内核账号) */
|
|
46
|
+
export async function loadUserGrants(db, userId, tenantId) {
|
|
47
|
+
const user = await db
|
|
48
|
+
.selectFrom('sys.user as u')
|
|
49
|
+
.leftJoin('sys.dept as d', 'd.id', 'u.dept_id')
|
|
50
|
+
.select(['u.id', 'u.status', 'u.dept_id', 'u.role_ids', 'd.ancestors'])
|
|
51
|
+
.where('u.id', '=', userId)
|
|
52
|
+
.where('u.tenant_id', '=', tenantId)
|
|
53
|
+
.executeTakeFirst();
|
|
54
|
+
if (!user)
|
|
55
|
+
return null;
|
|
56
|
+
const roleIds = Array.isArray(user.role_ids) ? user.role_ids : [];
|
|
57
|
+
const roles = roleIds.length
|
|
58
|
+
? await db
|
|
59
|
+
.selectFrom('sys.role')
|
|
60
|
+
.select(['capabilities', 'scopes'])
|
|
61
|
+
.where('id', 'in', roleIds)
|
|
62
|
+
.where('tenant_id', '=', tenantId)
|
|
63
|
+
.where('status', '=', 'active')
|
|
64
|
+
.execute()
|
|
65
|
+
: [];
|
|
66
|
+
const capabilities = mergeCaps(roles);
|
|
67
|
+
return {
|
|
68
|
+
userId,
|
|
69
|
+
tenantId,
|
|
70
|
+
deptId: user.dept_id ?? null,
|
|
71
|
+
deptPath: user.ancestors ?? null,
|
|
72
|
+
capabilities,
|
|
73
|
+
scopes: mergeScopes(roles),
|
|
74
|
+
unmask: unmaskFrom(capabilities),
|
|
75
|
+
active: user.status === 'active',
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** 用户上下文:禁用的用户没有任何能力 */
|
|
79
|
+
export function userCtx(g, extra = {}) {
|
|
80
|
+
return {
|
|
81
|
+
actor: { kind: 'user', id: g.userId },
|
|
82
|
+
userId: g.userId,
|
|
83
|
+
tenantId: g.tenantId,
|
|
84
|
+
deptId: g.deptId,
|
|
85
|
+
deptPath: g.deptPath,
|
|
86
|
+
capabilities: g.active ? g.capabilities : new Map(),
|
|
87
|
+
scopes: g.active ? g.scopes : {},
|
|
88
|
+
unmask: g.active ? g.unmask : [],
|
|
89
|
+
source: 'api',
|
|
90
|
+
...extra,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
export function anonymousCtx(tenantId, extra = {}) {
|
|
94
|
+
return {
|
|
95
|
+
actor: { kind: 'anonymous', id: 'anonymous' },
|
|
96
|
+
userId: null,
|
|
97
|
+
tenantId,
|
|
98
|
+
deptId: null,
|
|
99
|
+
deptPath: null,
|
|
100
|
+
capabilities: new Map(),
|
|
101
|
+
scopes: {},
|
|
102
|
+
unmask: [],
|
|
103
|
+
source: 'api',
|
|
104
|
+
...extra,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* 系统身份上下文:能力只包含它声明过的命令;范围为全部(仍受租户约束)。
|
|
109
|
+
* 审批流程、定时任务、入站回调、反应各自用它。
|
|
110
|
+
*/
|
|
111
|
+
export function systemCtx(opts) {
|
|
112
|
+
const source = opts.kind === 'system' ? 'system' : opts.kind;
|
|
113
|
+
return {
|
|
114
|
+
actor: { kind: opts.kind, id: `${opts.kind}:${opts.name}` },
|
|
115
|
+
userId: null,
|
|
116
|
+
onBehalfOf: opts.onBehalfOf ?? null,
|
|
117
|
+
tenantId: opts.tenantId,
|
|
118
|
+
deptId: null,
|
|
119
|
+
deptPath: null,
|
|
120
|
+
capabilities: new Map(opts.capabilities.map((c) => [c, {}])),
|
|
121
|
+
scopes: { '*': [{ combinator: 'all' }] },
|
|
122
|
+
unmask: [],
|
|
123
|
+
source: source,
|
|
124
|
+
requestId: opts.requestId,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=org.js.map
|
package/dist/org.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"org.js","sourceRoot":"","sources":["../src/org.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAOzC,4BAA4B;AAC5B,MAAM,UAAU,SAAS,CAAC,KAAqB;IAC7C,MAAM,GAAG,GAAG,IAAI,GAAG,EAA4B,CAAC;IAChD,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,YAAY,IAAI,EAAE,EAAE,CAAC;YACrC,MAAM,IAAI,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;YACnC,MAAM,IAAI,GAAqB,EAAE,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,EAAE,CAAC;YACnD,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,IAAI,EAAE,CAAC,EAAE,CAAC;gBACpD,MAAM,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;gBACnB,IAAI,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,KAAK,QAAQ,IAAI,OAAO,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;YACxF,CAAC;YACD,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;QAC9B,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,+CAA+C;AAC/C,MAAM,UAAU,WAAW,CAAC,KAAqB;IAC/C,MAAM,GAAG,GAAiC,EAAE,CAAC;IAC7C,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,IAAI,EAAE,EAAE,CAAC;YAC/B,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC;gBAAE,SAAS,CAAC,cAAc;YACjE,MAAM,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC,CAAC;YACvC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,CAAC,CAAC,UAAU,CAAC,CAAC;YAC7D,IAAI,IAAI,IAAI,CAAC,CAAC,UAAU,KAAK,SAAS;gBAAE,IAAI,CAAC,OAAO,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,IAAI,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;iBAChH,IAAI,CAAC,IAAI;gBAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC;QACpJ,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,sCAAsC;AACtC,MAAM,UAAU,UAAU,CAAC,IAAmC;IAC5D,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;QAC9B,MAAM,CAAC,GAAG,6CAA6C,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAClE,IAAI,CAAC;YAAE,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACrC,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAaD,0BAA0B;AAC1B,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,EAAe,EAAE,MAAc,EAAE,QAAgB;IACpF,MAAM,IAAI,GAAG,MAAM,EAAE;SAClB,UAAU,CAAC,eAAe,CAAC;SAC3B,QAAQ,CAAC,eAAe,EAAE,MAAM,EAAE,WAAW,CAAC;SAC9C,MAAM,CAAC,CAAC,MAAM,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,aAAa,CAAC,CAAC;SACtE,KAAK,CAAC,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC;SAC1B,KAAK,CAAC,aAAa,EAAE,GAAG,EAAE,QAAQ,CAAC;SACnC,gBAAgB,EAAE,CAAC;IACtB,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,MAAM,OAAO,GAAa,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;IAC5E,MAAM,KAAK,GAAmB,OAAO,CAAC,MAAM;QAC1C,CAAC,CAAC,MAAM,EAAE;aACL,UAAU,CAAC,UAAU,CAAC;aACtB,MAAM,CAAC,CAAC,cAAc,EAAE,QAAQ,CAAC,CAAC;aAClC,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC;aAC1B,KAAK,CAAC,WAAW,EAAE,GAAG,EAAE,QAAQ,CAAC;aACjC,KAAK,CAAC,QAAQ,EAAE,GAAG,EAAE,QAAQ,CAAC;aAC9B,OAAO,EAAE;QACd,CAAC,CAAC,EAAE,CAAC;IACP,MAAM,YAAY,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC;IACtC,OAAO;QACL,MAAM;QACN,QAAQ;QACR,MAAM,EAAE,IAAI,CAAC,OAAO,IAAI,IAAI;QAC5B,QAAQ,EAAE,IAAI,CAAC,SAAS,IAAI,IAAI;QAChC,YAAY;QACZ,MAAM,EAAE,WAAW,CAAC,KAAK,CAAC;QAC1B,MAAM,EAAE,UAAU,CAAC,YAAY,CAAC;QAChC,MAAM,EAAE,IAAI,CAAC,MAAM,KAAK,QAAQ;KACjC,CAAC;AACJ,CAAC;AAED,wBAAwB;AACxB,MAAM,UAAU,OAAO,CAAC,CAAa,EAAE,QAAsB,EAAE;IAC7D,OAAO;QACL,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE;QACrC,MAAM,EAAE,CAAC,CAAC,MAAM;QAChB,QAAQ,EAAE,CAAC,CAAC,QAAQ;QACpB,MAAM,EAAE,CAAC,CAAC,MAAM;QAChB,QAAQ,EAAE,CAAC,CAAC,QAAQ;QACpB,YAAY,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,GAAG,EAAE;QACnD,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE;QAChC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE;QAChC,MAAM,EAAE,KAAK;QACb,GAAG,KAAK;KACT,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,QAAgB,EAAE,QAAsB,EAAE;IACrE,OAAO;QACL,KAAK,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,EAAE,WAAW,EAAE;QAC7C,MAAM,EAAE,IAAI;QACZ,QAAQ;QACR,MAAM,EAAE,IAAI;QACZ,QAAQ,EAAE,IAAI;QACd,YAAY,EAAE,IAAI,GAAG,EAAE;QACvB,MAAM,EAAE,EAAE;QACV,MAAM,EAAE,EAAE;QACV,MAAM,EAAE,KAAK;QACb,GAAG,KAAK;KACT,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,SAAS,CAAC,IAOzB;IACC,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;IAC7D,OAAO;QACL,KAAK,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,EAAE,EAAE,GAAG,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,EAAE,EAAE;QAC3D,MAAM,EAAE,IAAI;QACZ,UAAU,EAAE,IAAI,CAAC,UAAU,IAAI,IAAI;QACnC,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,MAAM,EAAE,IAAI;QACZ,QAAQ,EAAE,IAAI;QACd,YAAY,EAAE,IAAI,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;QAC5D,MAAM,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,EAAE;QACxC,MAAM,EAAE,EAAE;QACV,MAAM,EAAE,MAAuB;QAC/B,SAAS,EAAE,IAAI,CAAC,SAAS;KAC1B,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type Transaction } from 'kysely';
|
|
2
|
+
export declare const idempotency: {
|
|
3
|
+
claim(tx: Transaction<any>, key?: string): Promise<{
|
|
4
|
+
fresh: true;
|
|
5
|
+
} | {
|
|
6
|
+
fresh: false;
|
|
7
|
+
result: unknown;
|
|
8
|
+
}>;
|
|
9
|
+
complete(tx: Transaction<any>, key: string | undefined, result: unknown): Promise<void>;
|
|
10
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// 幂等:请求头带 Idempotency-Key,内核在事务开头先「抢占」这个键。
|
|
2
|
+
// 唯一约束让并发的第二个请求等第一个提交后再判断,最终只执行一次;第一个回滚时第二个会正常执行。
|
|
3
|
+
import { sql } from 'kysely';
|
|
4
|
+
export const idempotency = {
|
|
5
|
+
async claim(tx, key) {
|
|
6
|
+
if (!key)
|
|
7
|
+
return { fresh: true };
|
|
8
|
+
const row = await tx
|
|
9
|
+
.insertInto('sys.idempotency')
|
|
10
|
+
.values({ key, status: 'running', expires_at: sql `now() + interval '24 hours'` })
|
|
11
|
+
.onConflict((oc) => oc.column('key').doNothing())
|
|
12
|
+
.returning('key')
|
|
13
|
+
.executeTakeFirst();
|
|
14
|
+
if (row)
|
|
15
|
+
return { fresh: true };
|
|
16
|
+
const prev = await tx.selectFrom('sys.idempotency').select(['result', 'status']).where('key', '=', key).executeTakeFirstOrThrow();
|
|
17
|
+
return { fresh: false, result: prev.result };
|
|
18
|
+
},
|
|
19
|
+
async complete(tx, key, result) {
|
|
20
|
+
if (!key)
|
|
21
|
+
return;
|
|
22
|
+
await tx
|
|
23
|
+
.updateTable('sys.idempotency')
|
|
24
|
+
.set({ status: 'done', result: JSON.stringify(result) })
|
|
25
|
+
.where('key', '=', key)
|
|
26
|
+
.execute();
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=idempotency.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"idempotency.js","sourceRoot":"","sources":["../../src/pipeline/idempotency.ts"],"names":[],"mappings":"AAAA,2CAA2C;AAC3C,kDAAkD;AAClD,OAAO,EAAE,GAAG,EAAoB,MAAM,QAAQ,CAAC;AAE/C,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,KAAK,CAAC,KAAK,CAAC,EAAoB,EAAE,GAAY;QAC5C,IAAI,CAAC,GAAG;YAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;QACjC,MAAM,GAAG,GAAG,MAAM,EAAE;aACjB,UAAU,CAAC,iBAAiB,CAAC;aAC7B,MAAM,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,GAAG,CAAA,6BAA6B,EAAE,CAAC;aAChF,UAAU,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,EAAE,CAAC;aAChD,SAAS,CAAC,KAAK,CAAC;aAChB,gBAAgB,EAAE,CAAC;QACtB,IAAI,GAAG;YAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;QAChC,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,UAAU,CAAC,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,uBAAuB,EAAE,CAAC;QAClI,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC;IAC/C,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,EAAoB,EAAE,GAAuB,EAAE,MAAe;QAC3E,IAAI,CAAC,GAAG;YAAE,OAAO;QACjB,MAAM,EAAE;aACL,WAAW,CAAC,iBAAiB,CAAC;aAC9B,GAAG,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC;aACvD,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,CAAC;aACtB,OAAO,EAAE,CAAC;IACf,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type Kysely, type Transaction } from 'kysely';
|
|
2
|
+
import type { AnyAggregate } from '../primitives/aggregate.js';
|
|
3
|
+
import type { FindOptions, ReadRepo, Reads } from '../primitives/command.js';
|
|
4
|
+
import type { Ctx } from '../types.js';
|
|
5
|
+
type Db = Transaction<any> | Kysely<any>;
|
|
6
|
+
export declare const tableRef: (agg: AnyAggregate, alias?: string) => string;
|
|
7
|
+
export declare function loadScoped(tx: Db, agg: AnyAggregate, id: string, ctx: Ctx, forUpdate: boolean): Promise<Record<string, any> | null>;
|
|
8
|
+
/** 把等值条件翻译成列条件;只允许声明过的字段 */
|
|
9
|
+
export declare function whereColumns(agg: AnyAggregate, where: Record<string, unknown> | undefined): [string, unknown][];
|
|
10
|
+
export declare function scopedFind(tx: Db, agg: AnyAggregate, ctx: Ctx, where?: Record<string, unknown>, opts?: FindOptions): import("kysely").SelectQueryBuilder<any, string, {
|
|
11
|
+
[x: string]: any;
|
|
12
|
+
}>;
|
|
13
|
+
/** 第 7 关:只把 reads 里的聚合做成只读仓储,同样受范围约束 */
|
|
14
|
+
export declare function bindReads<R extends Reads>(tx: Db, reads: R, ctx: Ctx): Record<string, ReadRepo<any>>;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// 按范围加载:把「能不能看到这条数据」直接写进 SQL 条件,范围外的数据根本查不出来。
|
|
2
|
+
import { sql } from 'kysely';
|
|
3
|
+
import { cageError } from '../errors.js';
|
|
4
|
+
import { rowToSnapshot } from '../db/rows.js';
|
|
5
|
+
import { compileScope } from '../scope/index.js';
|
|
6
|
+
import { snake } from '../util/names.js';
|
|
7
|
+
import { loadRelationIds } from '../relations.js';
|
|
8
|
+
import { isRelation } from '../primitives/fields.js';
|
|
9
|
+
export const tableRef = (agg, alias = 't') => `${agg.table.schema}.${agg.table.name} as ${alias}`;
|
|
10
|
+
export async function loadScoped(tx, agg, id, ctx, forUpdate) {
|
|
11
|
+
let q = tx
|
|
12
|
+
.selectFrom(tableRef(agg))
|
|
13
|
+
.selectAll()
|
|
14
|
+
.where('t.id', '=', id)
|
|
15
|
+
.where(compileScope(agg.scope, agg.name, ctx, 't'));
|
|
16
|
+
if (forUpdate)
|
|
17
|
+
q = q.forUpdate();
|
|
18
|
+
const row = await q.executeTakeFirst();
|
|
19
|
+
return row ? (await loadRelationIds(tx, agg, [rowToSnapshot(agg, row)], ctx))[0] : null;
|
|
20
|
+
}
|
|
21
|
+
const SYSTEM_KEYS = {
|
|
22
|
+
id: 'id', status: 'status', version: 'version', createdAt: 'created_at', updatedAt: 'updated_at',
|
|
23
|
+
};
|
|
24
|
+
/** 把等值条件翻译成列条件;只允许声明过的字段 */
|
|
25
|
+
export function whereColumns(agg, where) {
|
|
26
|
+
const out = [];
|
|
27
|
+
for (const [k, v] of Object.entries(where ?? {})) {
|
|
28
|
+
if (v === undefined)
|
|
29
|
+
continue;
|
|
30
|
+
const col = SYSTEM_KEYS[k] ?? (k in agg.fields ? snake(k) : undefined);
|
|
31
|
+
if (!col)
|
|
32
|
+
throw cageError('INVALID_INPUT', { field: k, message: `${agg.name} 没有字段 ${k}` });
|
|
33
|
+
const f = agg.fields[k];
|
|
34
|
+
if (f && (f.sql === 'jsonb' || isRelation(f)))
|
|
35
|
+
throw cageError('INVALID_INPUT', { field: k, message: '明细或集合关系不能作为主表查询条件' });
|
|
36
|
+
out.push([col, v]);
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
export function scopedFind(tx, agg, ctx, where, opts = {}) {
|
|
41
|
+
let q = tx.selectFrom(tableRef(agg)).selectAll().where(compileScope(agg.scope, agg.name, ctx, 't'));
|
|
42
|
+
for (const [col, v] of whereColumns(agg, where)) {
|
|
43
|
+
q = v === null ? q.where(sql.ref(`t.${col}`), 'is', null) : q.where(sql.ref(`t.${col}`), '=', v);
|
|
44
|
+
}
|
|
45
|
+
if (opts.orderBy) {
|
|
46
|
+
const col = SYSTEM_KEYS[opts.orderBy] ?? (opts.orderBy in agg.fields ? snake(opts.orderBy) : undefined);
|
|
47
|
+
const field = agg.fields[opts.orderBy];
|
|
48
|
+
if (!col || (field && isRelation(field)))
|
|
49
|
+
throw cageError('INVALID_INPUT', { field: opts.orderBy });
|
|
50
|
+
q = q.orderBy(sql.ref(`t.${col}`), opts.desc ? 'desc' : 'asc');
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
q = q.orderBy('t.created_at', 'asc');
|
|
54
|
+
}
|
|
55
|
+
return q.limit(Math.min(opts.limit ?? 1000, 5000));
|
|
56
|
+
}
|
|
57
|
+
/** 第 7 关:只把 reads 里的聚合做成只读仓储,同样受范围约束 */
|
|
58
|
+
export function bindReads(tx, reads, ctx) {
|
|
59
|
+
const deps = {};
|
|
60
|
+
for (const [alias, agg] of Object.entries(reads)) {
|
|
61
|
+
deps[alias] = Object.freeze({
|
|
62
|
+
get: async (id) => (typeof id === 'string' && /^[0-9a-f-]{36}$/i.test(id) ? loadScoped(tx, agg, id, ctx, false) : null),
|
|
63
|
+
find: async (where, opts) => loadRelationIds(tx, agg, (await scopedFind(tx, agg, ctx, where, opts).execute()).map((r) => rowToSnapshot(agg, r)), ctx),
|
|
64
|
+
findOne: async (where) => {
|
|
65
|
+
const rows = await scopedFind(tx, agg, ctx, where, { limit: 1 }).execute();
|
|
66
|
+
return rows[0] ? (await loadRelationIds(tx, agg, [rowToSnapshot(agg, rows[0])], ctx))[0] : null;
|
|
67
|
+
},
|
|
68
|
+
count: async (where) => {
|
|
69
|
+
let q = tx.selectFrom(tableRef(agg)).select(sql `count(*)::int`.as('n')).where(compileScope(agg.scope, agg.name, ctx, 't'));
|
|
70
|
+
for (const [col, v] of whereColumns(agg, where))
|
|
71
|
+
q = q.where(sql.ref(`t.${col}`), '=', v);
|
|
72
|
+
const r = await q.executeTakeFirst();
|
|
73
|
+
return Number(r?.n ?? 0);
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
// 访问未声明的读依赖:类型上已经报错;运行时也给出明确的错误码
|
|
78
|
+
return new Proxy(deps, {
|
|
79
|
+
get(target, prop) {
|
|
80
|
+
if (typeof prop === 'string' && !(prop in target) && prop !== 'then' && prop !== 'toJSON') {
|
|
81
|
+
throw new Error(`[CAGE-READS-001] handler 使用了未声明的读依赖 ${prop}(详见 docs/errors/CAGE-READS-001.md)`);
|
|
82
|
+
}
|
|
83
|
+
return target[prop];
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=load.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load.js","sourceRoot":"","sources":["../../src/pipeline/load.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { AGGREGATE } from '../brands.js';
|
|
3
|
+
import type { ScopeSpec } from '../scope/index.js';
|
|
4
|
+
import { type AnyField, type FieldValues, type Fields } from './fields.js';
|
|
5
|
+
import { type StatesDef } from './states.js';
|
|
6
|
+
/** 每张表都有的系统列 */
|
|
7
|
+
export interface SystemCols<S extends string> {
|
|
8
|
+
id: string;
|
|
9
|
+
version: number;
|
|
10
|
+
status: S;
|
|
11
|
+
createdAt: string;
|
|
12
|
+
updatedAt: string;
|
|
13
|
+
}
|
|
14
|
+
export type Snapshot<F extends Fields = Fields, S extends string = string> = FieldValues<F> & SystemCols<S>;
|
|
15
|
+
export declare const RESERVED_FIELDS: readonly ["id", "version", "status", "createdAt", "updatedAt", "tenantId"];
|
|
16
|
+
export interface Aggregate<N extends string = string, F extends Fields = Fields, S extends string = string> {
|
|
17
|
+
readonly [AGGREGATE]: true;
|
|
18
|
+
/** 聚合名,同时是表名和能力点前缀 */
|
|
19
|
+
readonly name: N;
|
|
20
|
+
readonly fields: F;
|
|
21
|
+
readonly states: StatesDef<S>;
|
|
22
|
+
readonly scope: ScopeSpec;
|
|
23
|
+
readonly invariants: Readonly<Record<string, (o: Snapshot<F, S>) => boolean>>;
|
|
24
|
+
/** 由 fields 拼出的整体校验 */
|
|
25
|
+
readonly schema: z.ZodObject<any>;
|
|
26
|
+
readonly table: {
|
|
27
|
+
readonly schema: 'core' | 'sys';
|
|
28
|
+
readonly name: string;
|
|
29
|
+
};
|
|
30
|
+
readonly description?: string;
|
|
31
|
+
/** 内部选项:授权类聚合写入后通知各实例清缓存 */
|
|
32
|
+
readonly options: {
|
|
33
|
+
readonly grants?: 'self' | 'all';
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export type AnyAggregate = Aggregate<string, any, any>;
|
|
37
|
+
export type StateOf<A> = A extends Aggregate<any, any, infer S> ? S : never;
|
|
38
|
+
export type FieldsOf<A> = A extends Aggregate<any, infer F, any> ? F : never;
|
|
39
|
+
export type SnapshotOf<A> = A extends Aggregate<any, infer F, infer S> ? Snapshot<F, S> : never;
|
|
40
|
+
export type FieldValuesOf<A> = A extends Aggregate<any, infer F, any> ? FieldValues<F> : never;
|
|
41
|
+
type FieldInput = Record<string, AnyField | StatesDef<any>>;
|
|
42
|
+
type PlainFields<FI> = {
|
|
43
|
+
[K in keyof FI as FI[K] extends StatesDef<any> ? never : K]: FI[K] extends AnyField ? FI[K] : never;
|
|
44
|
+
};
|
|
45
|
+
type StatesIn<FI> = {
|
|
46
|
+
[K in keyof FI]: FI[K] extends StatesDef<infer S> ? S : never;
|
|
47
|
+
}[keyof FI];
|
|
48
|
+
type ResolveStates<FI, SS extends string> = [SS] extends [never] ? ([StatesIn<FI>] extends [never] ? 'active' | 'deleted' : StatesIn<FI>) : SS;
|
|
49
|
+
export interface AggregateDef<FI extends FieldInput, SS extends string> {
|
|
50
|
+
fields: FI;
|
|
51
|
+
/** 状态集;也可以写在 fields 里:status: POStatus */
|
|
52
|
+
states?: StatesDef<SS>;
|
|
53
|
+
/** 数据范围:哪个字段是部门、哪个是创建人 */
|
|
54
|
+
scope?: ScopeSpec;
|
|
55
|
+
/** 不变量:普通函数,每次写入后逐条校验 */
|
|
56
|
+
invariants?: Record<string, (o: Snapshot<PlainFields<FI>, ResolveStates<FI, SS>>) => boolean>;
|
|
57
|
+
description?: string;
|
|
58
|
+
/** 框架内部使用:系统聚合放在 sys 模式 */
|
|
59
|
+
table?: {
|
|
60
|
+
schema?: 'core' | 'sys';
|
|
61
|
+
name?: string;
|
|
62
|
+
};
|
|
63
|
+
/** 框架内部使用 */
|
|
64
|
+
options?: {
|
|
65
|
+
grants?: 'self' | 'all';
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
export declare function aggregate<const N extends string, FI extends FieldInput, SS extends string = never>(name: N, def: AggregateDef<FI, SS>): Aggregate<N, PlainFields<FI>, ResolveStates<FI, SS>>;
|
|
69
|
+
export declare function isAggregate(v: unknown): v is AnyAggregate;
|
|
70
|
+
export declare function hasStates(agg: AnyAggregate, s: string | readonly string[]): boolean;
|
|
71
|
+
/** 聚合中不对外暴露(hidden)的字段名 */
|
|
72
|
+
export declare function hiddenFields(agg: AnyAggregate): string[];
|
|
73
|
+
/** 聚合中的敏感字段:字段名 → 种类 */
|
|
74
|
+
export declare function sensitiveFields(agg: AnyAggregate): Record<string, string>;
|
|
75
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { AGGREGATE } from '../brands.js';
|
|
2
|
+
import { declError } from '../errors.js';
|
|
3
|
+
import { snake } from '../util/names.js';
|
|
4
|
+
import { isField, isRelation, objectSchema } from './fields.js';
|
|
5
|
+
import { DEFAULT_STATES, isStates } from './states.js';
|
|
6
|
+
export const RESERVED_FIELDS = ['id', 'version', 'status', 'createdAt', 'updatedAt', 'tenantId'];
|
|
7
|
+
export function aggregate(name, def) {
|
|
8
|
+
if (!/^[A-Z][A-Za-z0-9]*$/.test(name)) {
|
|
9
|
+
throw declError('CAGE-NAME-001', `聚合名 ${name} 必须是大驼峰`);
|
|
10
|
+
}
|
|
11
|
+
let st = def.states;
|
|
12
|
+
const fields = {};
|
|
13
|
+
for (const [k, v] of Object.entries(def.fields)) {
|
|
14
|
+
if (isStates(v)) {
|
|
15
|
+
if (k !== 'status')
|
|
16
|
+
throw declError('CAGE-STATE-005', `状态集只能放在 status 字段上(发现 ${name}.${k})`);
|
|
17
|
+
if (st)
|
|
18
|
+
throw declError('CAGE-STATE-005', `${name} 同时在 states 和 fields.status 里声明了状态`);
|
|
19
|
+
st = v;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (!isField(v))
|
|
23
|
+
throw declError('CAGE-FIELD-003', `${name}.${k} 不是字段原语`);
|
|
24
|
+
if (RESERVED_FIELDS.includes(k)) {
|
|
25
|
+
throw declError('CAGE-FIELD-004', `${name}.${k} 是保留字段名,由内核维护`);
|
|
26
|
+
}
|
|
27
|
+
if (!/^[a-z][A-Za-z0-9]*$/.test(k))
|
|
28
|
+
throw declError('CAGE-NAME-002', `字段名 ${name}.${k} 必须是小驼峰`);
|
|
29
|
+
fields[k] = v;
|
|
30
|
+
}
|
|
31
|
+
const states = st ?? DEFAULT_STATES;
|
|
32
|
+
// 范围:未写创建人字段时,自动识别 createdBy
|
|
33
|
+
const sc = def.scope ?? {};
|
|
34
|
+
const ownerField = sc.ownerField ?? ('createdBy' in fields ? 'createdBy' : undefined);
|
|
35
|
+
for (const f of [sc.deptField, ownerField]) {
|
|
36
|
+
if (f && f !== 'id' && !(f in fields))
|
|
37
|
+
throw declError('CAGE-SCOPE-002', `${name} 的范围字段 ${f} 不存在`);
|
|
38
|
+
if (f && f !== 'id' && isRelation(fields[f]))
|
|
39
|
+
throw declError('CAGE-REL-001', `${name} 的范围字段必须是主表列,不能使用集合关系 ${f}`);
|
|
40
|
+
}
|
|
41
|
+
const scope = Object.freeze({ ...sc, ownerField });
|
|
42
|
+
return Object.freeze({
|
|
43
|
+
[AGGREGATE]: true,
|
|
44
|
+
name,
|
|
45
|
+
fields: Object.freeze(fields),
|
|
46
|
+
states,
|
|
47
|
+
scope,
|
|
48
|
+
invariants: Object.freeze({ ...(def.invariants ?? {}) }),
|
|
49
|
+
schema: objectSchema(fields),
|
|
50
|
+
table: Object.freeze({ schema: def.table?.schema ?? 'core', name: def.table?.name ?? snake(name) }),
|
|
51
|
+
description: def.description,
|
|
52
|
+
options: Object.freeze({ ...(def.options ?? {}) }),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
export function isAggregate(v) {
|
|
56
|
+
return !!v && typeof v === 'object' && v[AGGREGATE] === true;
|
|
57
|
+
}
|
|
58
|
+
export function hasStates(agg, s) {
|
|
59
|
+
return [s].flat().every((x) => agg.states.values.includes(x));
|
|
60
|
+
}
|
|
61
|
+
/** 聚合中不对外暴露(hidden)的字段名 */
|
|
62
|
+
export function hiddenFields(agg) {
|
|
63
|
+
return Object.entries(agg.fields).filter(([, f]) => f.isHidden).map(([k]) => k);
|
|
64
|
+
}
|
|
65
|
+
/** 聚合中的敏感字段:字段名 → 种类 */
|
|
66
|
+
export function sensitiveFields(agg) {
|
|
67
|
+
return Object.fromEntries(Object.entries(agg.fields).filter(([, f]) => f.sensitiveKind).map(([k, f]) => [k, f.sensitiveKind]));
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=aggregate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aggregate.js","sourceRoot":"","sources":["../../src/primitives/aggregate.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { COMMAND } from '../brands.js';
|
|
3
|
+
import { type DomainError } from '../errors.js';
|
|
4
|
+
import type { MaybePromise } from '../types.js';
|
|
5
|
+
import { type AnyAggregate, type FieldValuesOf, type SnapshotOf, type StateOf } from './aggregate.js';
|
|
6
|
+
import type { Constraint } from './constraints.js';
|
|
7
|
+
import { type AnyEvent, type EmitSpec } from './event.js';
|
|
8
|
+
/** reads 的形状:别名 → 聚合 */
|
|
9
|
+
export type Reads = Record<string, AnyAggregate>;
|
|
10
|
+
export interface FindOptions {
|
|
11
|
+
limit?: number;
|
|
12
|
+
orderBy?: string;
|
|
13
|
+
desc?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/** 注入给 handler 的只读仓储:自动附加数据范围,范围外等于不存在 */
|
|
16
|
+
export interface ReadRepo<A extends AnyAggregate> {
|
|
17
|
+
/** 按 id 取一条,范围外返回 null */
|
|
18
|
+
get(id: string): Promise<SnapshotOf<A> | null>;
|
|
19
|
+
/** 等值条件查询,自动附加范围 */
|
|
20
|
+
find(where?: Partial<SnapshotOf<A>>, opts?: FindOptions): Promise<SnapshotOf<A>[]>;
|
|
21
|
+
/** 按条件取第一条 */
|
|
22
|
+
findOne(where: Partial<SnapshotOf<A>>): Promise<SnapshotOf<A> | null>;
|
|
23
|
+
/** 计数,同样自动附加范围 */
|
|
24
|
+
count(where?: Partial<SnapshotOf<A>>): Promise<number>;
|
|
25
|
+
}
|
|
26
|
+
export type DepsOf<R extends Reads> = {
|
|
27
|
+
readonly [K in keyof R]: ReadRepo<R[K]>;
|
|
28
|
+
};
|
|
29
|
+
/** handler 拿到的草稿:业务字段可改;状态、主键、版本号只读,handler 改不了 */
|
|
30
|
+
export type Draft<A extends AnyAggregate> = FieldValuesOf<A> & {
|
|
31
|
+
readonly id: string;
|
|
32
|
+
readonly status: StateOf<A>;
|
|
33
|
+
readonly version: number;
|
|
34
|
+
readonly createdAt: string;
|
|
35
|
+
readonly updatedAt: string;
|
|
36
|
+
};
|
|
37
|
+
/** handler 的第四个参数:只读的调用信息 */
|
|
38
|
+
export interface HandlerCtx {
|
|
39
|
+
/** 当前时间(同一次命令里固定) */
|
|
40
|
+
readonly now: Date;
|
|
41
|
+
/** 真人操作者(系统身份时为它代表的人) */
|
|
42
|
+
readonly actorUserId: string | null;
|
|
43
|
+
}
|
|
44
|
+
export type HandlerResult<A extends AnyAggregate> = Draft<A> | Partial<FieldValuesOf<A>> | DomainError | void;
|
|
45
|
+
export type Invoker = 'user' | 'process' | 'job' | 'external';
|
|
46
|
+
export interface Command<A extends AnyAggregate = AnyAggregate, R extends Reads = Reads, I extends z.ZodType = z.ZodType> {
|
|
47
|
+
readonly [COMMAND]: true;
|
|
48
|
+
readonly agg: A;
|
|
49
|
+
readonly from: readonly StateOf<A>[] | 'new';
|
|
50
|
+
/** null 表示保持加锁读到的当前状态;仍检查 from、权限、版本并写审计。 */
|
|
51
|
+
readonly to: StateOf<A> | null;
|
|
52
|
+
readonly input: I;
|
|
53
|
+
readonly reads: R;
|
|
54
|
+
readonly emits: readonly EmitSpec[];
|
|
55
|
+
readonly constraints: readonly Constraint<A>[];
|
|
56
|
+
readonly invokedBy: readonly Invoker[];
|
|
57
|
+
/** 匿名用户也能调用(极少使用) */
|
|
58
|
+
readonly public: boolean;
|
|
59
|
+
/** 显式命令名;不写时取导出名 */
|
|
60
|
+
readonly name?: string;
|
|
61
|
+
readonly description?: string;
|
|
62
|
+
readonly handler: (o: Draft<A>, input: z.output<I>, deps: DepsOf<R>, ctx: HandlerCtx) => MaybePromise<HandlerResult<A>>;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 注册表中的命令可以有不同聚合、输入与 reads,因此这里擦除具体泛型。
|
|
66
|
+
* 不能用 Command<AnyAggregate, Reads, ZodType>:函数参数逆变会要求业务
|
|
67
|
+
* handler 接受任意聚合,导致带 stock 等明确读依赖的命令无法传给 approval。
|
|
68
|
+
* command() 的返回值仍保留 A/R/I,调用处的类型推导不受这个内部容器类型影响。
|
|
69
|
+
*/
|
|
70
|
+
export type AnyCommand = Omit<Command<AnyAggregate, Reads, z.ZodType>, 'handler'> & {
|
|
71
|
+
readonly handler: (...args: any[]) => MaybePromise<any>;
|
|
72
|
+
};
|
|
73
|
+
export declare const idOnly: z.ZodObject<{
|
|
74
|
+
id: z.ZodString;
|
|
75
|
+
}, z.core.$strip>;
|
|
76
|
+
export declare const empty: z.ZodObject<{}, z.core.$strip>;
|
|
77
|
+
type DefaultInput<F> = F extends 'new' ? typeof empty : typeof idOnly;
|
|
78
|
+
export interface CommandDef<A extends AnyAggregate, R extends Reads, I extends z.ZodType, F> {
|
|
79
|
+
/** 起始状态;'new' 表示新建 */
|
|
80
|
+
from: F;
|
|
81
|
+
/** 目标状态;非新建命令可显式写 null,只更新业务字段而不迁移状态。 */
|
|
82
|
+
to: F extends 'new' ? StateOf<A> : StateOf<A> | null;
|
|
83
|
+
/** 输入校验;非新建命令的输入必须包含 id */
|
|
84
|
+
input?: I;
|
|
85
|
+
/** 读依赖,决定 handler 能拿到什么 */
|
|
86
|
+
reads?: R;
|
|
87
|
+
/** 发出的事件 */
|
|
88
|
+
emits?: (AnyEvent | EmitSpec)[];
|
|
89
|
+
/** 硬约束组合子 */
|
|
90
|
+
constraints?: NoInfer<Constraint<A>>[];
|
|
91
|
+
/** 谁能调用,默认 user */
|
|
92
|
+
invokedBy?: Invoker | Invoker[];
|
|
93
|
+
public?: boolean;
|
|
94
|
+
name?: string;
|
|
95
|
+
description?: string;
|
|
96
|
+
/** AI 写的业务逻辑:纯函数,返回新草稿、字段变更或业务错误 */
|
|
97
|
+
handler: (o: Draft<A>, input: z.output<[I] extends [never] ? DefaultInput<F> : I>, deps: DepsOf<R>, ctx: HandlerCtx) => MaybePromise<HandlerResult<A>>;
|
|
98
|
+
}
|
|
99
|
+
export declare function command<A extends AnyAggregate, const F extends StateOf<A> | readonly StateOf<A>[] | 'new', R extends Reads = {}, I extends z.ZodType = never>(agg: A, def: CommandDef<A, R, I, F>): Command<A, R, [I] extends [never] ? DefaultInput<F> : I>;
|
|
100
|
+
export declare function isCommand(v: unknown): v is AnyCommand;
|
|
101
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// command 原语:一次写操作。命令名由导出的变量名决定:export const approve = command(PurchaseOrder, …)
|
|
2
|
+
// 的能力点就是 PurchaseOrder.approve。
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { COMMAND } from '../brands.js';
|
|
5
|
+
import { declError } from '../errors.js';
|
|
6
|
+
import { hasStates, } from './aggregate.js';
|
|
7
|
+
import { toEmitSpec } from './event.js';
|
|
8
|
+
export const idOnly = z.object({ id: z.string().uuid() });
|
|
9
|
+
export const empty = z.object({});
|
|
10
|
+
export function command(agg, def) {
|
|
11
|
+
const from = def.from;
|
|
12
|
+
if (from !== 'new' && !hasStates(agg, from)) {
|
|
13
|
+
throw declError('CAGE-STATE-001', `${agg.name} 的命令 from 里有未声明的状态:${[from].flat().join(', ')}`);
|
|
14
|
+
}
|
|
15
|
+
// 回执等异步元数据可能在单据取消/完成后到达。允许显式保留当前状态,
|
|
16
|
+
// 避免业务 handler 通过篡改 status 绕过状态机;新建命令必须有实际目标状态。
|
|
17
|
+
if (from === 'new' && def.to === null) {
|
|
18
|
+
throw declError('CAGE-CMD-003', `${agg.name} 的新建命令必须指定目标状态,不能使用 to: null`);
|
|
19
|
+
}
|
|
20
|
+
if (def.to !== null && !hasStates(agg, def.to)) {
|
|
21
|
+
throw declError('CAGE-STATE-001', `${agg.name} 的命令 to 是未声明的状态:${String(def.to)}`);
|
|
22
|
+
}
|
|
23
|
+
const input = (def.input ?? (from === 'new' ? empty : idOnly));
|
|
24
|
+
if (from !== 'new' && input instanceof z.ZodObject && !('id' in input.shape)) {
|
|
25
|
+
throw declError('CAGE-CMD-002', `${agg.name} 的非新建命令输入必须包含 id`);
|
|
26
|
+
}
|
|
27
|
+
if (def.name !== undefined && !/^[a-z][A-Za-z0-9]*$/.test(def.name)) {
|
|
28
|
+
throw declError('CAGE-NAME-004', `命令名 ${def.name} 必须是小驼峰`);
|
|
29
|
+
}
|
|
30
|
+
return Object.freeze({
|
|
31
|
+
[COMMAND]: true,
|
|
32
|
+
agg,
|
|
33
|
+
from: (from === 'new' ? 'new' : Object.freeze([from].flat())),
|
|
34
|
+
to: def.to,
|
|
35
|
+
input: input,
|
|
36
|
+
reads: Object.freeze({ ...(def.reads ?? {}) }),
|
|
37
|
+
emits: Object.freeze((def.emits ?? []).map(toEmitSpec)),
|
|
38
|
+
constraints: Object.freeze([...(def.constraints ?? [])]),
|
|
39
|
+
invokedBy: Object.freeze([def.invokedBy ?? 'user'].flat()),
|
|
40
|
+
public: def.public ?? false,
|
|
41
|
+
name: def.name,
|
|
42
|
+
description: def.description,
|
|
43
|
+
handler: def.handler,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
export function isCommand(v) {
|
|
47
|
+
return !!v && typeof v === 'object' && v[COMMAND] === true;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=command.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"command.js","sourceRoot":"","sources":["../../src/primitives/command.ts"],"names":[],"mappings":"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"}
|