From fcf966124ff5c8ab5d6f5a928e97a54a78e68091 Mon Sep 17 00:00:00 2001 From: os-zhuang Date: Fri, 7 Aug 2026 16:46:11 +0000 Subject: [PATCH] =?UTF-8?q?docs(spec):=20SYNC=5FARCHITECTURE.md=20L3=20?= =?UTF-8?q?=E6=AE=B5=E5=81=9C=E6=AD=A2=E5=AE=A3=E4=BC=A0=E5=AD=97=E6=AE=B5?= =?UTF-8?q?=E6=98=A0=E5=B0=84=E7=9A=84=E5=80=BC=E8=BD=AC=E6=8D=A2=E8=83=BD?= =?UTF-8?q?=E5=8A=9B=20(#6384)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `FieldMapping.transform` —— 作者面上写作 `connector.fieldMappings[].transform` 与 `externalLookup.fieldMappings[].transform` —— 连同整个五成员 `FieldMappingTransform` 联合(`constant` / `cast` / `lookup` / `javascript` / `map`)已在 @objectstack/spec 17.0.0 按 #5552 / ADR-0049 退役:五个成员没有任何 一个有执行器,`javascript` 成员还在推荐 #3278 已退役的 `js` dialect。文档 L303 示例块的墓碑注释早已写对,散文却没跟着改 —— 与 #5554 / PR #6388 同型,只是换了 一次退役。 正文点名一处(Key Features 打勾行),实测为四处,全部改为如实说法: - L198 Key Features:打勾行 `With transformations and data type conversion` —— `data type conversion` 那半是真的(`dataType`),`With transformations` 那半不是。⛔ 未删行,改为 `dataType` 目标类型 + `syncMode` 逐字段方向,并在 行内显式写出 **no value transformation**,另加引用块。这里用显式否定而非静默 删除:删掉只是不再重复该断言,消不掉读者已经形成的"连接器字段映射能做值转换" 这一信念 —— 而这个信念比编译不过更贵,它会把值转换逻辑规划到一个不执行它的面上。 - L287 示例块内注释:`// Field Mappings with Transformations.` —— 与 15 行后 自己的墓碑注释直接矛盾。改为 `dataType` / `syncMode`,并指向那条墓碑。 - L387 Decision Matrix:⛔ 未删行(joins/aggregations → L2 那半是对的)。原行只 问 `complex transformations`,想做逐字段值转换的作者不会把自己读进"complex", 于是落到 L3 —— 正是本 issue 描述的失败路径。改写为"是否需要转换值(无论复杂 与否)",并写明 **Not** L3。 - L430 Migration Guide L3→L2 引导语:同上,`complex transformations` → "需要转换值(joins/aggregations,或 `fieldMappings` 做不到的逐字段转换)"。 措辞全部复用 `shared/mapping.zod.ts:89` 墓碑现成句,不另造第二种说法(同一次 退役出现两种描述,正是它们日后互相矛盾的成因)。 保留的每一条都对着 schema 核过,不是假定: - `ConnectorFieldMappingSchema`(`connector.zod.ts:121`)= `BaseFieldMappingSchema .extend({ dataType, required, syncMode })`,新增的确实只有这三个键;基类 `FieldMappingSchema`(`shared/mapping.zod.ts:72`)的 `transform` 是 `retiredKey(...)` 墓碑。故行文只说 schema **declares** `dataType`,不宣称运行时 执行 —— 无 `connector` liveness 记录可支撑执行侧断言。 - 墓碑指定的去处真实存在:`MappingSchema.fieldMapping` (`data/mapping.zod.ts:224`)= `z.array(ImportFieldMappingSchema)`,其 `transform: TransformType.default('none')`(:140)+ `params`(:147); `TransformType`(:84)含 `javascript`,而 REST import path 对它回 400 —— 故沿用 墓碑那份六成员列表 + "rejects `javascript` with a 400" 的补语,列表才是诚实的。 - `os migrate meta --from 16` 存在(`packages/cli/src/commands/migrate/meta.ts:146`), 且转换 `field-mapping-transform-removed` 的 `toMajor: 17` (`conversions/registry.ts:4197`)—— `--from 16` 是对的入口。 纯散文改动,未新增/删除任何 ```typescript 块:`grep -c '^```typescript'` 实测仍 为 6,`etl-author-shape.test.ts` 与 `connector-author-shape.test.ts` 两个门禁 30 tests 实跑通过(非假定);packages/spec 全量 338 files / 8644 tests 亦通过。 `packages/spec/docs/` 不在该包 `package.json` 的 `files` 白名单 (`dist`/`json-schema`/`liveness`/`prompts`/`llms.txt`/`README.md`/ `src/**/*.zod.ts`/`CHANGELOG.md`/`api-surface`/`spec-changes.json`)内,不随包 发布,故不写 changeset,改用 `skip-changeset` 标签。 Fixes #6384 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014wsZeReNTqiceBfLb5Pyf5 --- packages/spec/docs/SYNC_ARCHITECTURE.md | 30 +++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/packages/spec/docs/SYNC_ARCHITECTURE.md b/packages/spec/docs/SYNC_ARCHITECTURE.md index 2d8ffbe5e2..4fea98b967 100644 --- a/packages/spec/docs/SYNC_ARCHITECTURE.md +++ b/packages/spec/docs/SYNC_ARCHITECTURE.md @@ -195,7 +195,8 @@ Complete, production-grade integration with external systems. Includes authentic - ✅ **Authentication**: OAuth2, JWT, SAML, API Key, Basic Auth - ✅ **Webhooks**: Bidirectional event notifications - ✅ **Retry Policies**: Exponential backoff, circuit breaker -- ✅ **Field Mapping**: With transformations and data type conversion +- ✅ **Field Mapping**: `dataType` target type and `syncMode` per-field direction — + **no value transformation**; see below - ✅ **Conflict Resolution**: Multiple strategies (`ConnectorConflictResolution`) - ✅ **Security**: Signature verification, encryption - ✅ **Monitoring**: Health checks, metrics, logging @@ -215,6 +216,25 @@ Complete, production-grade integration with external systems. Includes authentic > rate-limited upstream is `retryConfig` — whose `retryableStatusCodes` default > `[408, 429, 500, 502, 503, 504]` includes `429` — and `health.circuitBreaker`. +> **Field mapping does not transform values.** The ticked line above used to read +> "With transformations and data type conversion". Only the second half was ever +> true: `ConnectorFieldMappingSchema` (`integration/connector.zod.ts`) extends the +> base mapping with exactly three keys — `dataType`, `required` and `syncMode`. +> `FieldMapping.transform` — authored as `connector.fieldMappings[].transform` and +> `externalLookup.fieldMappings[].transform` — was removed in `@objectstack/spec` +> 17.0.0 (#5552, ADR-0049), and the whole `FieldMappingTransform` union went with +> it (`constant` / `cast` / `lookup` / `javascript` / `map`) — **no runtime ever +> executed any of the five**, and the `javascript` member advertised +> `dialect: "js"`, a dialect retired in #3278. An L3 connector mapping moves a +> value from `source` to `target`; it does not compute one. **Value conversion +> belongs on a surface that runs it:** the L2 import mapping's own `transform` +> (`mapping.fieldMapping[].transform` in `data/mapping.zod.ts` — a string enum, +> `none`/`constant`/`map`/`split`/`join`/`lookup`, with its settings in `params`), +> applied row by row by the REST import path, which rejects its own `javascript` +> value with a 400 rather than pretending to run it — or an ETL transformation +> step (L2 above). Already authored the retired key? `os migrate meta --from 16` +> rewrites it. + ### Use Cases 1. **Enterprise SAP Integration** - Full bidirectional sync with complex business logic @@ -284,7 +304,8 @@ const sapConnector: ConnectorInput = { deleteMode: 'soft_delete' }, - // Field Mappings with Transformations. + // Field Mappings — `dataType` target type and `syncMode` direction. There is + // no value transformation here; see the tombstone on the second entry. // The keys are `source` / `target` — the canonical spelling of the base // protocol in `shared/mapping.zod.ts`, which every mapping surface extends. fieldMappings: [ @@ -384,7 +405,7 @@ const sapConnector: ConnectorInput = { | Question | Answer → Level | |----------|----------------| -| Do you need complex transformations (joins, aggregations)? | **Yes** → L2 (ETL) | +| Do you need to transform values at all — joins and aggregations, or just a per-field convert? | **Yes** → L2 (ETL) for joins/aggregations, or the import mapping's `fieldMapping[].transform` for per-field conversion. **Not** L3: a connector's `fieldMappings` declares `dataType` and `syncMode` and performs no value transformation (#5552) | | Do you need multi-source aggregation? | **Yes** → L2 (ETL) | | Do you need real-time webhooks? | **Yes** → L3 (Connector) | | Do you need advanced authentication (OAuth2, SAML)? | **Yes** → L3 (Connector) | @@ -427,7 +448,8 @@ Combine levels for complex scenarios. ### From L3 (`syncConfig`) to L2 -When a connector's declarative sync needs complex transformations: +When a connector's declarative sync needs to transform values — joins and +aggregations, or a per-field convert that `fieldMappings` cannot do (#5552): **Before (L3 `syncConfig`):** ```typescript