@pylonts/dsl 1.1.19 → 1.1.20
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/README.md +1 -0
- package/dist/dto.d.ts +3 -0
- package/dist/dto.js +14 -0
- package/dist/typebox-driver.js +19 -22
- package/dist/utils.d.ts +2 -2
- package/dist/utils.js +7 -3
- package/docs/dto.md +33 -0
- package/docs/utils.md +104 -0
- package/package.json +1 -1
- package/src/dto.ts +15 -0
- package/src/typebox-driver.ts +19 -23
- package/src/utils.ts +9 -5
package/README.md
CHANGED
|
@@ -22,6 +22,7 @@ project(地图:应用与 API 拓扑)
|
|
|
22
22
|
- [table.md](./docs/table.md) — 定义表(详细设计)
|
|
23
23
|
- [dto.md](./docs/dto.md) — 定义 DTO
|
|
24
24
|
- [curd.md](./docs/curd.md) — 管理端 CRUD 页面标准(CurdSchema)
|
|
25
|
+
- [utils.md](./docs/utils.md) — 工具模块与领域规则(UtilsSchema:防御 guard / 判断 / 计算)
|
|
25
26
|
- [aggregate.md](./docs/aggregate.md) — 聚合/仓储 DSL 扩展规划(声明层已落地:DomainAggregate/RepositorySchema + 存储校验,生成器待选型)
|
|
26
27
|
- [domain-event.md](./docs/domain-event.md) — 领域事件 DSL 扩展规划(声明层已落地:DomainEventSchema + event_schema 存储校验,发布/订阅待实现)
|
|
27
28
|
- [ddd-principles.md](./docs/ddd-principles.md) — DDD 落地原则(已决策)
|
package/dist/dto.d.ts
CHANGED
|
@@ -103,6 +103,9 @@ export declare function dtoField(field: Field | DtoArrayFieldDef | DtoObjectFiel
|
|
|
103
103
|
export declare function isDtoMessage(v: unknown): v is DtoMessage;
|
|
104
104
|
/** Structural check — a DtoField wraps a Field in a .field property and has no .type of its own. */
|
|
105
105
|
export declare function isDtoField(v: unknown): v is DtoField;
|
|
106
|
+
/** Resolve a ref chain to its terminal DtoField (the one without .ref).
|
|
107
|
+
* Cycles are a DSL definition error — fail loudly at render time. */
|
|
108
|
+
export declare function resolveDtoRefChain(f: DtoField): DtoField;
|
|
106
109
|
/** TS type of a DtoField in generated code — unwraps the wrapped field
|
|
107
110
|
* (enum → its JS name, date/datetime → string, containers → jsType). */
|
|
108
111
|
export declare function dtoFieldJsType(df: DtoField): string;
|
package/dist/dto.js
CHANGED
|
@@ -119,6 +119,20 @@ export function isDtoField(v) {
|
|
|
119
119
|
return false;
|
|
120
120
|
return 'field' in v && !('type' in v);
|
|
121
121
|
}
|
|
122
|
+
/** Resolve a ref chain to its terminal DtoField (the one without .ref).
|
|
123
|
+
* Cycles are a DSL definition error — fail loudly at render time. */
|
|
124
|
+
export function resolveDtoRefChain(f) {
|
|
125
|
+
const seen = new Set();
|
|
126
|
+
let cur = f;
|
|
127
|
+
while (cur.ref !== undefined) {
|
|
128
|
+
if (seen.has(cur.ref)) {
|
|
129
|
+
throw new Error(`dto field ${cur.name}: circular ref chain (field references itself)`);
|
|
130
|
+
}
|
|
131
|
+
seen.add(cur.ref);
|
|
132
|
+
cur = cur.ref;
|
|
133
|
+
}
|
|
134
|
+
return cur;
|
|
135
|
+
}
|
|
122
136
|
/** TS type of a DtoField in generated code — unwraps the wrapped field
|
|
123
137
|
* (enum → its JS name, date/datetime → string, containers → jsType). */
|
|
124
138
|
export function dtoFieldJsType(df) {
|
package/dist/typebox-driver.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isDtoField, isDtoMessage } from './dto.js';
|
|
1
|
+
import { isDtoField, isDtoMessage, resolveDtoRefChain } from './dto.js';
|
|
2
2
|
import { collectEnumRefs } from './dsl.js';
|
|
3
3
|
function renderString(s) {
|
|
4
4
|
return `'${s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
@@ -16,20 +16,6 @@ function fieldDescription(field) {
|
|
|
16
16
|
function dtoFieldDescription(f) {
|
|
17
17
|
return f.description ?? fieldDescription(f.field);
|
|
18
18
|
}
|
|
19
|
-
/** Resolve a ref chain to its terminal field (the one without .ref).
|
|
20
|
-
* Cycles are a DSL definition error — fail loudly at render time. */
|
|
21
|
-
function resolveRefChain(f) {
|
|
22
|
-
const seen = new Set();
|
|
23
|
-
let cur = f;
|
|
24
|
-
while (cur.ref !== undefined) {
|
|
25
|
-
if (seen.has(cur.ref)) {
|
|
26
|
-
throw new Error(`dto field ${cur.name}: circular ref chain (field references itself)`);
|
|
27
|
-
}
|
|
28
|
-
seen.add(cur.ref);
|
|
29
|
-
cur = cur.ref;
|
|
30
|
-
}
|
|
31
|
-
return cur;
|
|
32
|
-
}
|
|
33
19
|
function renderBasic(field, pattern, defaultValue, resolver, indent = 0, description) {
|
|
34
20
|
if (pattern !== undefined && field.type !== 'string') {
|
|
35
21
|
throw new Error(`pattern is only supported on string fields, got ${field.type} (${field.name})`);
|
|
@@ -176,7 +162,7 @@ function renderField(f, indent, resolver) {
|
|
|
176
162
|
* chain, inherit the terminal field's type/constraints, keep the referencing
|
|
177
163
|
* field's own overrides (pattern / default / description). */
|
|
178
164
|
function renderRefBase(f, indent, resolver) {
|
|
179
|
-
const target =
|
|
165
|
+
const target = resolveDtoRefChain(f);
|
|
180
166
|
const targetField = target.field;
|
|
181
167
|
const pattern = f.pattern ?? target.pattern;
|
|
182
168
|
const defaultValue = f.default ?? target.default;
|
|
@@ -187,7 +173,7 @@ function renderRefBase(f, indent, resolver) {
|
|
|
187
173
|
* override first, then the chain's DtoField-level optional, then the bare
|
|
188
174
|
* column optionality. */
|
|
189
175
|
function renderRefField(f, indent, resolver) {
|
|
190
|
-
const target =
|
|
176
|
+
const target = resolveDtoRefChain(f);
|
|
191
177
|
const optional = f.optional ?? target.optional ?? target.field.optional ?? false;
|
|
192
178
|
const base = renderRefBase(f, indent, resolver);
|
|
193
179
|
return optional ? `Type.Optional(${base})` : base;
|
|
@@ -225,7 +211,7 @@ function renderValue(f, indent, resolver) {
|
|
|
225
211
|
}
|
|
226
212
|
function collectEnumImports(f, resolver, out) {
|
|
227
213
|
if (f.ref !== undefined) {
|
|
228
|
-
collectEnumImports(
|
|
214
|
+
collectEnumImports(resolveDtoRefChain(f), resolver, out);
|
|
229
215
|
return;
|
|
230
216
|
}
|
|
231
217
|
if (f.field.type === 'array') {
|
|
@@ -270,14 +256,25 @@ export function collectDtoImports(schema, resolver, out) {
|
|
|
270
256
|
for (const f of Object.values(schema.fields))
|
|
271
257
|
collectEnumImports(f, resolver, out);
|
|
272
258
|
}
|
|
259
|
+
/** JSON Schema readOnly annotation on a rendered scalar schema: the token
|
|
260
|
+
* owns the field, the client must not send it (the __inject adapter
|
|
261
|
+
* overwrites any client-supplied value anyway). Injection fields are always
|
|
262
|
+
* scalar columns (tables forbid nested columns), so the only object literal
|
|
263
|
+
* in a rendered scalar base is its options block. */
|
|
264
|
+
function withReadOnly(base) {
|
|
265
|
+
const idx = base.lastIndexOf('{');
|
|
266
|
+
if (idx === -1)
|
|
267
|
+
return base.replace(/\(\s*\)$/, '({ readOnly: true })');
|
|
268
|
+
return `${base.slice(0, idx + 1)} readOnly: true,${base.slice(idx + 1)}`;
|
|
269
|
+
}
|
|
273
270
|
/** Render the server-injection base: token-injected fields as Optional
|
|
274
|
-
* properties of a TypeBox object, plus a non-enumerable __inject
|
|
275
|
-
* (same mechanism as hand-written bases, see pylon __inject docs)
|
|
276
|
-
* each field from the token at runtime. */
|
|
271
|
+
* readOnly properties of a TypeBox object, plus a non-enumerable __inject
|
|
272
|
+
* adapter (same mechanism as hand-written bases, see pylon __inject docs)
|
|
273
|
+
* that fills each field from the token at runtime. */
|
|
277
274
|
function renderInjectBase(fields, resolver) {
|
|
278
275
|
const entries = Object.entries(fields).map(([name, f]) => {
|
|
279
276
|
const base = f.ref !== undefined ? renderRefBase(f, 1, resolver) : renderValue(f, 1, resolver);
|
|
280
|
-
return ` ${name}: Type.Optional(${base})`;
|
|
277
|
+
return ` ${name}: Type.Optional(${withReadOnly(base)})`;
|
|
281
278
|
});
|
|
282
279
|
const inner = `Type.Object({\n${entries.join(',\n')}\n})`;
|
|
283
280
|
const assigns = Object.keys(fields)
|
package/dist/utils.d.ts
CHANGED
|
@@ -10,8 +10,8 @@ export interface UtilsMethodSchema extends SchemaBase {
|
|
|
10
10
|
args: Record<string, DtoField>;
|
|
11
11
|
/** Output field — a plain inline Field (boolean for checks, decimal for
|
|
12
12
|
* computed amounts, ...). The method output is a fresh value, never a
|
|
13
|
-
* shared table column. */
|
|
14
|
-
result
|
|
13
|
+
* shared table column. Omit for void methods (pure actions). */
|
|
14
|
+
result?: Field;
|
|
15
15
|
}
|
|
16
16
|
/** Method input for defineUtils: type/schema/name are set by the builder. */
|
|
17
17
|
export type UtilsMethodDef = Omit<UtilsMethodSchema, 'type' | 'schema' | 'name'>;
|
package/dist/utils.js
CHANGED
|
@@ -34,9 +34,13 @@ export function defineUtils(options) {
|
|
|
34
34
|
df.field.schema = schema;
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
-
// Result: a plain inline Field — write name/schema back
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
// Result (optional): a plain inline Field — write name/schema back only
|
|
38
|
+
// when it is unowned (schema === undefined); shared instances (table
|
|
39
|
+
// columns, fields already claimed by another container) stay untouched.
|
|
40
|
+
if (methodSchema.result !== undefined && methodSchema.result.schema === undefined) {
|
|
41
|
+
methodSchema.result.name = key;
|
|
42
|
+
methodSchema.result.schema = schema;
|
|
43
|
+
}
|
|
40
44
|
schema.methods[key] = methodSchema;
|
|
41
45
|
}
|
|
42
46
|
return schema;
|
package/docs/dto.md
CHANGED
|
@@ -48,6 +48,39 @@ dtoField(stringField({ maxLength: 32 })).setOperator('like')
|
|
|
48
48
|
dtoField(intField()).setDefault(0) // TypeBox default 注解
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
+
## 字段引用规格(Reference Spec)
|
|
52
|
+
|
|
53
|
+
`DtoField` 通过 `setRef(other)` 或共享字段实例引用其他字段,表达"本字段来源于 X"。**引用不是任意的**——只有业务契约 DTO(controller/service 消费的 `dto_schema/{app}` 或 `dto_schema/common` DTO)的字段可以作为引用方,且只能引用三类被引用方:
|
|
54
|
+
|
|
55
|
+
| # | 引用 | 语法 | 语义 |
|
|
56
|
+
|---|---|---|---|
|
|
57
|
+
| 1 | **field 引用** | `dtoField(table.columns.x)`(共享实例)或 `setRef` 指向包装表列的 DtoField | 与数据库字段同义(来源:库) |
|
|
58
|
+
| 2 | **token 引用** | `fromToken(token, [...])` 或 `setRef(token 字段)` | 字段来源于登录身份(服务端注入) |
|
|
59
|
+
| 3 | **third 引用** | `setRef(third 消息字段)` | 字段来源于第三方消息(参数或结果) |
|
|
60
|
+
|
|
61
|
+
**方向约束**:
|
|
62
|
+
|
|
63
|
+
- 引用方:业务契约 DTO 的字段;
|
|
64
|
+
- 被引用方:token 字段 / third 消息字段 / 表列 field——**业务 DTO 之间不能互相引用**(convert 的 sources/target 各自独立,映射由搬运生成推导,不靠 DTO 互 ref);
|
|
65
|
+
- third 字段、token 字段不可反向引用业务 DTO;
|
|
66
|
+
- ref 链无环(渲染期强校验,循环引用报错)。
|
|
67
|
+
|
|
68
|
+
**对 convert 自动搬运的意义**:同源判定 = ref 链终端 / 共享 field 实例,且只有规格内合法引用构成可推导搬运——业务 DTO ← third(防腐翻译,`setRef(thirdField)` = "本字段从 third 参数/结果来")、业务 DTO ← entity(共享表列实例)都是可自动生成搬运的映射;越界引用是声明错误。
|
|
69
|
+
|
|
70
|
+
> entity 列是裸 `Field` 实例:DTO 字段 `dtoField(order.columns.x)` 与 entity 列共享同一实例即构成 field 引用。
|
|
71
|
+
|
|
72
|
+
### token 引用的注入与 readOnly
|
|
73
|
+
|
|
74
|
+
`fromToken(token, fields)` 是 token 引用的标准入口:每个投影字段 `setRef(token 字段)`(复用类型/约束)+ 标记 `injectFrom`(服务端注入)。typebox 生成物中注入字段渲染为 **`Type.Optional(...)` + `readOnly: true` 注解**(JSON Schema annotation)——服务器字段,客户端不得上送:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
id: Type.Optional(Type.Integer({ readOnly: true })),
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
同时生成非枚举 `__inject` 适配器,fastify RPC 层在进 controller 前执行 `body.k = token.k` 从登录身份填充——**客户端即使上送也会被覆盖**(上送无效)。三层闭环:声明 readOnly(不可上送语义)+ Optional(可不传)+ 运行时覆盖(上送无效)。
|
|
81
|
+
|
|
82
|
+
注意:**只有 `fromToken()` 自动设置 `injectFrom`**;手写 `setRef(token 字段)` 只表达引用关系,不触发注入渲染(如需注入须显式标记或改走 fromToken)。
|
|
83
|
+
|
|
51
84
|
## 默认值
|
|
52
85
|
|
|
53
86
|
- `setDefault(v)` 设 DTO 层默认值,渲染为 TypeBox `default:` 注解(`Type.String({ default: 'PENDING' })`、`Type.Enum(OrderStatus, { default: OrderStatus.PENDING })`)。
|
package/docs/utils.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# 工具模块与领域规则(UtilsSchema)
|
|
2
|
+
|
|
3
|
+
UtilsSchema 是**业务无关工具模块**(base utility modules)的声明:带完整签名的纯函数集合,在 service 层被调用(含 flow 的 `invoke`)。方法签名(args/result)由 schema 声明,方法体由用户实现。
|
|
4
|
+
|
|
5
|
+
**当 utils 名称与某张表同名时,它就是该实体的领域规则集**(DDD 领域服务落点)——命名即归属、位置即角色:`utils_schema/.../order.utils.ts` = order 表的领域规则(`OrderUtils`)。
|
|
6
|
+
|
|
7
|
+
## 定义
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { defineUtils, booleanField, decimalField, dtoField, intField, stringField } from '@pylonts/dsl';
|
|
11
|
+
import { api, miniuser } from '../../../../project.config';
|
|
12
|
+
import { order } from '../../../../schema/order.table';
|
|
13
|
+
|
|
14
|
+
const orderUtils = defineUtils({
|
|
15
|
+
name: 'OrderUtils',
|
|
16
|
+
api,
|
|
17
|
+
app: miniuser,
|
|
18
|
+
description: '订单领域规则',
|
|
19
|
+
methods: {
|
|
20
|
+
// 防御 guard:条件抛错,通过无返回值(void),失败即 throw
|
|
21
|
+
assertCancelable: {
|
|
22
|
+
args: { status: dtoField(order.columns.status) },
|
|
23
|
+
},
|
|
24
|
+
// 判断:返回 boolean 判定值,供 flow IF 条件位使用
|
|
25
|
+
canCancel: {
|
|
26
|
+
args: { status: dtoField(order.columns.status) },
|
|
27
|
+
result: booleanField(),
|
|
28
|
+
},
|
|
29
|
+
// 计算:返回派生标量
|
|
30
|
+
calcTotal: {
|
|
31
|
+
args: { unitPrice: dtoField(decimalField({ precision: 10, scale: 2 })), qty: dtoField(intField({ min: 1 })) },
|
|
32
|
+
result: decimalField({ precision: 10, scale: 2 }),
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// 必须 default-export 方法引用对象(XXUtils 约定),loader 从 refs 反推 schema
|
|
38
|
+
export default {
|
|
39
|
+
assertCancelable: orderUtils.methods.assertCancelable,
|
|
40
|
+
canCancel: orderUtils.methods.canCancel,
|
|
41
|
+
calcTotal: orderUtils.methods.calcTotal,
|
|
42
|
+
};
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 方法形态
|
|
46
|
+
|
|
47
|
+
- **args**:`Record<string, DtoField>`——用 `dtoField(...)` 包装。可包装**表列**(`dtoField(order.columns.status)`,领域规则引用表字段的标准方式)或内联字段(`dtoField(stringField(...))`)。包装层反写不污染共享表列实例(与 `buildMessage` 同规则:仅内联字段写回底层)。
|
|
48
|
+
- **result**:`Field | undefined`——输出是全新值(boolean 判断、decimal 计算、string 派生文案),**不是共享表列**;省略 = void 方法(防御 guard 通过时无返回值)。
|
|
49
|
+
- **失败语义**:规则失败直接 `throw`(配合 flow 的 `BusinessException`/`CodeException` 通道),**不返回错误码结构**。
|
|
50
|
+
|
|
51
|
+
## 文件与存储约定
|
|
52
|
+
|
|
53
|
+
- 一文件一 utils;文件必须 **default-export 方法引用对象**(`export default { m: xxUtils.methods.m }`),schema 变量模块私有不导出。
|
|
54
|
+
- 文件名 = 名字去 `Utils`/`Util` 后缀转 kebab:`OrderUtils → order.utils.ts`、`AmtUtils → amt.utils.ts`。
|
|
55
|
+
- 存储位置按绑定(机器校验,`lint utils`):
|
|
56
|
+
|
|
57
|
+
| 绑定 | 目录(utils_schema/) | 生成物 |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| api + app | `{api}/{app}/utils/` | `{api}/src/modules/{app}/utils/{Name}.ts` |
|
|
60
|
+
| 仅 api | `{api}/common/utils/` | `{api}/src/modules/common/utils/`(后端公共) |
|
|
61
|
+
| 仅 app | `{app}/utils/` | `{app.dir}/src/utils/`(前端) |
|
|
62
|
+
| 无绑定 | `common/utils/` 或 `shared/utils/` | `{root}/shared/utils/`(前后端共享) |
|
|
63
|
+
|
|
64
|
+
## 领域规则三分类
|
|
65
|
+
|
|
66
|
+
从纯函数视角,方法的出口只有两种:**返回值**或**抛异常**。因此领域规则方法恰好三类:
|
|
67
|
+
|
|
68
|
+
| 分类 | 命名 | 本质 | result | 失败语义 | 典型示例 |
|
|
69
|
+
|---|---|---|---|---|---|
|
|
70
|
+
| **1 防御 guard** | `assertXX` / `validateXX` / `ensureXX` | 条件抛错(前置校验) | `void` | **失败必抛错**,通过无返回值 | `assertCancelable`——已完成/已取消订单 throw |
|
|
71
|
+
| **2 判断** | `canXX` / `isXX` / `hasXX` | 返回判定值(谓词) | `boolean` / 枚举 | 返回 false/枚举值,不抛 | `canCancel`、`isExpired`——供 flow `IF(...)` 条件位 |
|
|
72
|
+
| **3 计算** | `calcXX` / `formatXX` / 动词 | 派生值计算 | 标量(金额/数量/文案) | 一般不抛(入参合法前提) | `calcTotal`——金额计算 |
|
|
73
|
+
|
|
74
|
+
### 为什么这就是全集
|
|
75
|
+
|
|
76
|
+
- **判断 = 计算的特例**:boolean/枚举也是值,只是返回值窄化——单独分类是为 flow 谓词位(`IF(canCancel(slots.status))`)的语义清晰,不是本质区别。
|
|
77
|
+
- **防御 guard = 判断 + 抛错**:guard 内部必然先判断再 throw,是"判断"失败分支的显式化;通过时没有调用方需要的值,所以返回 `void`。
|
|
78
|
+
- **void 方法只能是防御 guard**:纯函数无副作用,不返回也不抛 = 什么都没做——void 方法若默认通过、特殊状态才抛,就是 guard 的语义。
|
|
79
|
+
|
|
80
|
+
### 命名与返回的一致性(可机器校验)
|
|
81
|
+
|
|
82
|
+
- `assert/validate/ensure` 前缀 → **必须 void**(guard:通过无值、失败抛错);
|
|
83
|
+
- `can/is/has` 前缀 → **必须 boolean 或枚举**(判断谓词);
|
|
84
|
+
- 计算类 → 标量 result。
|
|
85
|
+
- 命名与 result 形态不匹配(如 `canXX` 返回 void)即声明错误——lint 可按此机器校验。
|
|
86
|
+
|
|
87
|
+
## 建议(best practices)
|
|
88
|
+
|
|
89
|
+
1. **命名即归属**:utils 名与表名一致 = 该实体领域规则集(`OrderUtils` ↔ `order`)。业务无关的通用工具(`AmtUtils`/`DateTimeUtils`)不绑定表,放共享目录。
|
|
90
|
+
2. **规则二分定性**(既定决策):**不查库 → utils 谓词**(纯函数);**查库 → service 断言方法**(`invoke(dao.findByName) → IF(isNotNull) → THROW`);**硬不变量 → 表约束**(TableSchema unique/FK,DB 物理兜底)。不建 RuleSchema——规则已有 schema:表/谓词/守卫。
|
|
91
|
+
3. **失败抛错,不返回错误码结构**:校验失败 `throw new Error('ORDER_CANNOT_CANCEL: ...')` 或业务异常类;flow 的 TRY-CATCH 按异常名捕获。
|
|
92
|
+
4. **复用判据**:规则被 ≥2 处使用才提取为 utils;单处使用留在 flow 守卫片段("三行相似胜过过早抽象")。
|
|
93
|
+
5. **纯函数、无 I/O**:utils 不做数据库/网络访问;需要 I/O 的规则属于 service。
|
|
94
|
+
6. **表列引用用 `dtoField(table.columns.x)` 包装**:包装层反写安全,共享列实例永不被动;参数名可以与列名不同(如 `state` 包装 `status` 列)。
|
|
95
|
+
7. **result 选型与命名一致**:防御 guard → **`void`** + `assert/validate/ensure` 前缀(抛错即语义);判断 → `booleanField()`(或枚举)+ `can/is/has` 前缀(供 flow 谓词位);计算 → `decimalField`/`intField`/`stringField` 标量。
|
|
96
|
+
8. **flow 集成**:判断类可直接作 `IF(cond)` 条件位谓词(多入参靠 invoke 多槽位);防御 guard 通过 `invoke` 在流程前置位调用。
|
|
97
|
+
|
|
98
|
+
## 生成与合并
|
|
99
|
+
|
|
100
|
+
`pylonts gen utils`:生成 `export class OrderUtils { static ... }` 骨架(每方法 `Not implemented` throw),签名由 schema 推导(enum → 枚举类型名 + 自动 import;date/datetime → string;无 result → `void`)。已存在文件**方法级 MERGE**(用户填充的方法体保留、私有 helper 不碰、契约删方法移除、新方法追加 stub);`--force` 整文件覆盖。覆盖规范见 [gen-utils-overwrite.md](../../docs/gen-utils-overwrite.md)。
|
|
101
|
+
|
|
102
|
+
## 校验
|
|
103
|
+
|
|
104
|
+
`pylonts lint utils`:存储位置 + 绑定共享实例 + 文件名机器校验(`lint all` 已包含)。
|
package/package.json
CHANGED
package/src/dto.ts
CHANGED
|
@@ -185,6 +185,21 @@ export function isDtoField(v: unknown): v is DtoField {
|
|
|
185
185
|
return 'field' in v && !('type' in v);
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
/** Resolve a ref chain to its terminal DtoField (the one without .ref).
|
|
189
|
+
* Cycles are a DSL definition error — fail loudly at render time. */
|
|
190
|
+
export function resolveDtoRefChain(f: DtoField): DtoField {
|
|
191
|
+
const seen = new Set<DtoField>();
|
|
192
|
+
let cur: DtoField = f;
|
|
193
|
+
while (cur.ref !== undefined) {
|
|
194
|
+
if (seen.has(cur.ref)) {
|
|
195
|
+
throw new Error(`dto field ${cur.name}: circular ref chain (field references itself)`);
|
|
196
|
+
}
|
|
197
|
+
seen.add(cur.ref);
|
|
198
|
+
cur = cur.ref;
|
|
199
|
+
}
|
|
200
|
+
return cur;
|
|
201
|
+
}
|
|
202
|
+
|
|
188
203
|
/** TS type of a DtoField in generated code — unwraps the wrapped field
|
|
189
204
|
* (enum → its JS name, date/datetime → string, containers → jsType). */
|
|
190
205
|
export function dtoFieldJsType(df: DtoField): string {
|
package/src/typebox-driver.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DtoArrayField, DtoField, DtoMessage, DtoObjectField, ImportBase, ImportRef, isDtoField, isDtoMessage } from './dto.js';
|
|
1
|
+
import { DtoArrayField, DtoField, DtoMessage, DtoObjectField, ImportBase, ImportRef, isDtoField, isDtoMessage, resolveDtoRefChain } from './dto.js';
|
|
2
2
|
import { collectEnumRefs, EnumField, Field } from './dsl.js';
|
|
3
3
|
import type { TableSchema } from './db.js';
|
|
4
4
|
import type { TokenSchema } from './token.js';
|
|
@@ -34,21 +34,6 @@ function dtoFieldDescription(f: DtoField): string | undefined {
|
|
|
34
34
|
return f.description ?? fieldDescription(f.field);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
/** Resolve a ref chain to its terminal field (the one without .ref).
|
|
38
|
-
* Cycles are a DSL definition error — fail loudly at render time. */
|
|
39
|
-
function resolveRefChain(f: DtoField): DtoField {
|
|
40
|
-
const seen = new Set<DtoField>();
|
|
41
|
-
let cur: DtoField = f;
|
|
42
|
-
while (cur.ref !== undefined) {
|
|
43
|
-
if (seen.has(cur.ref)) {
|
|
44
|
-
throw new Error(`dto field ${cur.name}: circular ref chain (field references itself)`);
|
|
45
|
-
}
|
|
46
|
-
seen.add(cur.ref);
|
|
47
|
-
cur = cur.ref;
|
|
48
|
-
}
|
|
49
|
-
return cur;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
37
|
function renderBasic(
|
|
53
38
|
field: Field,
|
|
54
39
|
pattern: string | undefined,
|
|
@@ -198,7 +183,7 @@ function renderField(f: DtoField, indent: number, resolver: EnumResolver | undef
|
|
|
198
183
|
* chain, inherit the terminal field's type/constraints, keep the referencing
|
|
199
184
|
* field's own overrides (pattern / default / description). */
|
|
200
185
|
function renderRefBase(f: DtoField, indent: number, resolver: EnumResolver | undefined): string {
|
|
201
|
-
const target =
|
|
186
|
+
const target = resolveDtoRefChain(f);
|
|
202
187
|
const targetField = target.field as Field;
|
|
203
188
|
const pattern = f.pattern ?? target.pattern;
|
|
204
189
|
const defaultValue = f.default ?? target.default;
|
|
@@ -210,7 +195,7 @@ function renderRefBase(f: DtoField, indent: number, resolver: EnumResolver | und
|
|
|
210
195
|
* override first, then the chain's DtoField-level optional, then the bare
|
|
211
196
|
* column optionality. */
|
|
212
197
|
function renderRefField(f: DtoField, indent: number, resolver: EnumResolver | undefined): string {
|
|
213
|
-
const target =
|
|
198
|
+
const target = resolveDtoRefChain(f);
|
|
214
199
|
const optional = f.optional ?? target.optional ?? (target.field as Field).optional ?? false;
|
|
215
200
|
const base = renderRefBase(f, indent, resolver);
|
|
216
201
|
return optional ? `Type.Optional(${base})` : base;
|
|
@@ -253,7 +238,7 @@ function collectEnumImports(
|
|
|
253
238
|
out: Map<string, ImportBase>,
|
|
254
239
|
): void {
|
|
255
240
|
if (f.ref !== undefined) {
|
|
256
|
-
collectEnumImports(
|
|
241
|
+
collectEnumImports(resolveDtoRefChain(f), resolver, out);
|
|
257
242
|
return;
|
|
258
243
|
}
|
|
259
244
|
if (f.field.type === 'array') {
|
|
@@ -303,14 +288,25 @@ export function collectDtoImports(
|
|
|
303
288
|
for (const f of Object.values(schema.fields)) collectEnumImports(f, resolver, out);
|
|
304
289
|
}
|
|
305
290
|
|
|
291
|
+
/** JSON Schema readOnly annotation on a rendered scalar schema: the token
|
|
292
|
+
* owns the field, the client must not send it (the __inject adapter
|
|
293
|
+
* overwrites any client-supplied value anyway). Injection fields are always
|
|
294
|
+
* scalar columns (tables forbid nested columns), so the only object literal
|
|
295
|
+
* in a rendered scalar base is its options block. */
|
|
296
|
+
function withReadOnly(base: string): string {
|
|
297
|
+
const idx = base.lastIndexOf('{');
|
|
298
|
+
if (idx === -1) return base.replace(/\(\s*\)$/, '({ readOnly: true })');
|
|
299
|
+
return `${base.slice(0, idx + 1)} readOnly: true,${base.slice(idx + 1)}`;
|
|
300
|
+
}
|
|
301
|
+
|
|
306
302
|
/** Render the server-injection base: token-injected fields as Optional
|
|
307
|
-
* properties of a TypeBox object, plus a non-enumerable __inject
|
|
308
|
-
* (same mechanism as hand-written bases, see pylon __inject docs)
|
|
309
|
-
* each field from the token at runtime. */
|
|
303
|
+
* readOnly properties of a TypeBox object, plus a non-enumerable __inject
|
|
304
|
+
* adapter (same mechanism as hand-written bases, see pylon __inject docs)
|
|
305
|
+
* that fills each field from the token at runtime. */
|
|
310
306
|
function renderInjectBase(fields: Record<string, DtoField>, resolver: EnumResolver | undefined): string {
|
|
311
307
|
const entries = Object.entries(fields).map(([name, f]) => {
|
|
312
308
|
const base = f.ref !== undefined ? renderRefBase(f, 1, resolver) : renderValue(f, 1, resolver);
|
|
313
|
-
return ` ${name}: Type.Optional(${base})`;
|
|
309
|
+
return ` ${name}: Type.Optional(${withReadOnly(base)})`;
|
|
314
310
|
});
|
|
315
311
|
const inner = `Type.Object({\n${entries.join(',\n')}\n})`;
|
|
316
312
|
const assigns = Object.keys(fields)
|
package/src/utils.ts
CHANGED
|
@@ -20,8 +20,8 @@ export interface UtilsMethodSchema extends SchemaBase {
|
|
|
20
20
|
args: Record<string, DtoField>;
|
|
21
21
|
/** Output field — a plain inline Field (boolean for checks, decimal for
|
|
22
22
|
* computed amounts, ...). The method output is a fresh value, never a
|
|
23
|
-
* shared table column. */
|
|
24
|
-
result
|
|
23
|
+
* shared table column. Omit for void methods (pure actions). */
|
|
24
|
+
result?: Field;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/** Method input for defineUtils: type/schema/name are set by the builder. */
|
|
@@ -84,9 +84,13 @@ export function defineUtils(options: {
|
|
|
84
84
|
df.field.schema = schema;
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
|
-
// Result: a plain inline Field — write name/schema back
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
// Result (optional): a plain inline Field — write name/schema back only
|
|
88
|
+
// when it is unowned (schema === undefined); shared instances (table
|
|
89
|
+
// columns, fields already claimed by another container) stay untouched.
|
|
90
|
+
if (methodSchema.result !== undefined && methodSchema.result.schema === undefined) {
|
|
91
|
+
methodSchema.result.name = key;
|
|
92
|
+
methodSchema.result.schema = schema;
|
|
93
|
+
}
|
|
90
94
|
schema.methods[key] = methodSchema;
|
|
91
95
|
}
|
|
92
96
|
return schema;
|