# 定义 DTO（四种方向）

DTO 描述接口出入参。方向决定可选性规则，由各方向工厂设置；**规则统一只在 `field.optional === undefined` 时生效**（作者已显式设置的跳过）：

| 构建器 | 方向 | 可选性规则 |
|---|---|---|
| `buildInput` | input | 按 DB 列规则：主键 → 必填；可空（未写 `optional` 或 `optional: true`）/ 有默认 → 可选；`optional: false` 无默认 → 必填 |
| `buildOutput` | output | 不设置（保持字段构建器/作者设置） |
| `buildQuery` | query | 全部可选 |
| `buildPk` | pk | 主键字段必填，其他字段可选 |

## 从字段集合投影

`from(source, fields)` 接受两类字段集合源：**表**（`TableSchema`）或**第三方方法消息**（`ThirdMethodSchema`，见 [third-service.md](./third-service.md)），投影字段包装为 DTO 字段，字段实例与源共享，`name/schema` 保持指向源。

```ts
import { buildInput, buildQuery, dtoField, from } from '@pylonts/dsl';

// 输入：新增订单
buildInput('OrderAddRequest', { ...from(order, [order.columns.mer_id, order.columns.amount]) });

// 输出：订单行
buildOutput('OrderRow', from(order, [order.columns.id, order.columns.order_no]));

// 查询：分页 + 过滤（query 字段恒为可选，.op() 声明比较操作符）
buildQuery('OrderPageQuery', {
  keyword: dtoField(stringField({ maxLength: 32 })).setOperator('like'),
  ...from(order, [order.columns.mer_id]),
});

// 主键：按 id 取详情
buildPk('OrderDetailRequest', from(order, [order.columns.id]));

// 转发：透传第三方方法消息（wire-format 字段名保持协议原样，不转 camelCase）
buildOutput('BalanceResult', from(queryBalance.results, [queryBalance.results.fields.balance]));
```

- **表源**：DTO 字段名转 camelCase（`mer_id` → `merId`），与 DB 列名（snake_case）分离。
- **第三方方法源**：字段名即线格式协议名（`out_trade_no`、`appId`），保持不变。
- `from()` 本身不做任何可选性推断——推断在各方向工厂，且按字段的 schema 判断：共享实体列按列规则推断（Rule A），wire 自有字段保留声明值。

## 独立字段

不来自表的内联字段直接用 `dtoField(...)` 包装任意字段构建器，可加 `pattern`、`optional`、`operator`、`default`。

```ts
dtoField(stringField({ maxLength: 32 })).setOperator('like')
dtoField(intField()).setDefault(0)        // TypeBox default 注解
```

## 容器字段：禁止内联嵌套（构建期校验）

**DTO 不内联**——嵌套结构必须命名引用，构建期（`buildInput/buildOutput/buildQuery/buildPk` 及 `defineUtils` args）机器校验：

| 形态 | 允许 | 禁止 |
|---|---|---|
| 数组字段 `dtoArrayField` | `items: 命名 DTO`（`items: orderItemDto` → `OrderItemDto[]`）或 `items: 标量字段`（`string[]`） | `items: dtoObjectField(...)` 内联对象元素、`items: dtoArrayField(...)` 内联数组元素 |
| 对象字段 | 裸 `objectField(...)`（wire 格式嵌套，渲染内联 `Type.Object({...})`） | `dtoObjectField(...)`、`dtoField(dtoObjectField(...))`——DtoField 类容器必须命名引用（数组元素），或改用裸 `objectField` |

**理由**：DtoField 类容器（`dtoObjectField` / `dtoArrayField`）内联时无法跨 DTO 复用，生成类型要么靠字符串拼接（`Array<{...}>`），要么塌缩（`any[]`）——命名引用让结构有单一事实来源（DTO 定义处）且生成精确类型。裸 `objectField` / `arrayField` 属于 wire 格式嵌套（随消息直接内联渲染），不在禁用范围。校验错误示例：

```
[dto] "OrderSubmitRequest" field "items": inline container elements are not allowed — array items must be a named DTO or a scalar field
```

```ts
// ✗ 内联对象元素
items: dtoArrayField({ items: dtoObjectField({ properties: { skuId: ..., qty: ... } }) })

// ✓ 命名 DTO 引用（OrderItemDto 单独 buildOutput 声明）
export const OrderItemDto = buildOutput('OrderItemDto', { skuId: ..., qty: ... });
items: dtoArrayField({ items: OrderItemDto })
```

## 字段引用规格（Reference Spec）

`DtoField` 通过 `setRef(other)` 或共享字段实例引用其他字段，表达"本字段来源于 X"。**引用不是任意的**——只有业务契约 DTO（controller/service 消费的 `dto_schema/{app}` 或 `dto_schema/common` DTO）的字段可以作为引用方，且只能引用三类被引用方：

| # | 引用 | 语法 | 语义 |
|---|---|---|---|
| 1 | **field 引用** | `dtoField(table.columns.x)`（共享实例）或 `setRef` 指向包装表列的 DtoField | 与数据库字段同义（来源：库） |
| 2 | **token 引用** | `fromToken(token, [...])` 或 `setRef(token 字段)` | 字段来源于登录身份（服务端注入） |
| 3 | **third 引用** | `setRef(third 消息字段)` | 字段来源于第三方消息（参数或结果） |

**方向约束**：

- 引用方：业务契约 DTO 的字段；
- 被引用方：token 字段 / third 消息字段 / 表列 field——**业务 DTO 之间不能互相引用**（convert 的 sources/target 各自独立，映射由搬运生成推导，不靠 DTO 互 ref）；
- third 字段、token 字段不可反向引用业务 DTO；
- ref 链无环（渲染期强校验，循环引用报错）。

**对 convert 自动搬运的意义**：同源判定 = ref 链终端 / 共享 field 实例，且只有规格内合法引用构成可推导搬运——业务 DTO ← third（防腐翻译，`setRef(thirdField)` = "本字段从 third 参数/结果来"）、业务 DTO ← entity（共享表列实例）都是可自动生成搬运的映射；越界引用是声明错误。

> entity 列是裸 `Field` 实例：DTO 字段 `dtoField(order.columns.x)` 与 entity 列共享同一实例即构成 field 引用。

### token 引用的注入与 readOnly

`fromToken(token, fields)` 是 token 引用的标准入口：每个投影字段 `setRef(token 字段)`（复用类型/约束）+ 标记 `injectFrom`（服务端注入）。typebox 生成物中注入字段渲染为 **`Type.Optional(...)` + `readOnly: true` 注解**（JSON Schema annotation）——服务器字段，客户端不得上送：

```ts
id: Type.Optional(Type.Integer({ readOnly: true })),
```

同时生成非枚举 `__inject` 适配器，fastify RPC 层在进 controller 前执行 `body.k = token.k` 从登录身份填充——**客户端即使上送也会被覆盖**（上送无效）。三层闭环：声明 readOnly（不可上送语义）+ Optional（可不传）+ 运行时覆盖（上送无效）。

注意：**只有 `fromToken()` 自动设置 `injectFrom`**；手写 `setRef(token 字段)` 只表达引用关系，不触发注入渲染（如需注入须显式标记或改走 fromToken）。

## 默认值

- `setDefault(v)` 设 DTO 层默认值，渲染为 TypeBox `default:` 注解（`Type.String({ default: 'PENDING' })`、`Type.Enum(OrderStatus, { default: OrderStatus.PENDING })`）。
- 枚举默认值必须是该枚举的成员值（value），渲染时解析为成员引用；非成员值直接报错。
- 未显式设置时，fallback 到字段构建器的 `default`（DB 默认值，string），`from()` 提取的字段自动带出。

## 继承基础 schema

```ts
buildQuery('OrderPageQuery', { ... })
  .include({ from: '@pylonts/core', name: 'PageRequest' });   // 渲染 Type.Intersect([PageRequest, ...])
```

## 关键语义

- **字段两层名**：`DtoField.name` 是接口字段名（DTO map key 反写）；`field.name` 是数据库列名（表反写）。
- **可选性优先级**：DTO 层 `optional` 优先于字段层；query 方向所有字段强制可选。
- **HTTP 传 string**：bigint / decimal / date / time 在接口层渲染为 `Type.String()`，保证精度与序列化语义。

## 文件组织

- `dto_schema/{module}/*.dto.ts`：DTO 源文件，一个文件可导出多个 DtoMessage；`module` = `project.config.ts` 的 `apps.name`，另加 `common/` 放跨 app 共享 DTO。
- `pylonts gen dto` 按 module 逐个扫描：`dto_schema/{module}/*.dto.ts` → 生成 `dto/{module}/{Name}.dto.ts`。
- 枚举字段引用 `enums/` 下生成的枚举源文件，DTO 文件通过 `../../enums/{JsName}.enum` 导入。