|
| 1 | +--- |
| 2 | +"@objectstack/spec": major |
| 3 | +--- |
| 4 | + |
| 5 | +BREAKING(spec): `FieldMapping` named three declarations — the two domain-specific |
| 6 | +sides are renamed to `ConnectorFieldMapping` and `ImportFieldMapping` (#4703, #4535 C12) |
| 7 | + |
| 8 | +`FieldMapping` / `FieldMappingSchema` were exported by **three** entry points for |
| 9 | +**three different declarations**, so which type you got depended only on the import |
| 10 | +path — the #4411 trap, one entry worse than the usual pair: |
| 11 | + |
| 12 | +| entry | declaration | keys | shape | |
| 13 | +|:--|:--|:--|:--| |
| 14 | +| `@objectstack/spec/shared` (**unchanged**) | `shared/mapping.zod.ts` | 4 | the base — plain `z.object` | |
| 15 | +| `@objectstack/spec/integration` (**renamed**) | `integration/connector.zod.ts` | 7 | `Base.extend({ dataType, required, syncMode })` | |
| 16 | +| `@objectstack/spec/data` (**renamed**) | `data/mapping.zod.ts` | 4 | an independent `strictObject` | |
| 17 | + |
| 18 | +The first two are base-and-superset. The third is **not the same concept at all**: it |
| 19 | +is the column mapping of a CSV/table import (`mapping.fieldMapping[]`), not a |
| 20 | +connector's remote-field mapping. Three ways the two are mutually unparseable: |
| 21 | + |
| 22 | +1. **`transform` is the same key name with incompatible value types.** `shared` / |
| 23 | + `integration` take the discriminated union `FieldMappingTransformSchema` |
| 24 | + (`{ type: 'cast', targetType: 'string' }`); `data` takes a flat `TransformType` |
| 25 | + enum defaulting to `'none'`, steering a separate `params` bag. |
| 26 | +2. **Different cardinality.** `data` accepts `string | string[]` for `source` and |
| 27 | + `target` — one target field may be composed from several columns (`split` / |
| 28 | + `join`). The other two accept a single `string`. |
| 29 | +3. **Opposite failure modes for an unknown key.** `data` is a `strictObject` |
| 30 | + (#4001): it **throws**, naming the canonical spelling. The other two are plain |
| 31 | + `z.object`: they **strip silently**. Under one shared name, the same typo is a |
| 32 | + hard error in one domain and a no-op in the other. |
| 33 | + |
| 34 | +Per **ADR-0112 D9(a)** the domain-specific sides take a domain prefix and the base |
| 35 | +keeps the bare name — the same ruling that produced `ConnectorRateLimitConfig` |
| 36 | +(#4684), `ConnectorErrorCategory` and `ConnectorRetryStrategy`. This is not a new |
| 37 | +convention: `data/ExternalFieldMappingSchema` already extends the same base and, |
| 38 | +purely because it carries a prefix, never entered the dual-source baseline at all. |
| 39 | + |
| 40 | +The dual-source baseline shrinks **16 → 14**. |
| 41 | + |
| 42 | +## FROM → TO |
| 43 | + |
| 44 | +```ts |
| 45 | +// before — @objectstack/spec/integration |
| 46 | +import { FieldMappingSchema, type FieldMapping } from '@objectstack/spec/integration'; |
| 47 | +// after |
| 48 | +import { |
| 49 | + ConnectorFieldMappingSchema, |
| 50 | + type ConnectorFieldMapping, |
| 51 | +} from '@objectstack/spec/integration'; |
| 52 | + |
| 53 | +// before — @objectstack/spec/data |
| 54 | +import { FieldMappingSchema, type FieldMapping } from '@objectstack/spec/data'; |
| 55 | +// after |
| 56 | +import { |
| 57 | + ImportFieldMappingSchema, |
| 58 | + type ImportFieldMapping, |
| 59 | +} from '@objectstack/spec/data'; |
| 60 | +``` |
| 61 | + |
| 62 | +**Importing from `@objectstack/spec/shared`? Nothing changes** — that `FieldMapping` |
| 63 | +is the base, keeps its name, its four keys and its plain-`z.object` behaviour. |
| 64 | + |
| 65 | +No deprecated aliases are kept on either renamed entry: re-exporting the old name |
| 66 | +would be a third declaration of it and would re-open the trap this change closes. |
| 67 | + |
| 68 | +⚠️ **Do not "fix" the compile error by re-pointing the import at |
| 69 | +`@objectstack/spec/shared`.** That name resolves, and it is the wrong schema. On the |
| 70 | +connector side it silently costs you `dataType` / `required` / `syncMode` — the base |
| 71 | +is not `.strict()`, so those keys are **stripped at parse time** and the mapping runs |
| 72 | +without them. On the import side the base rejects arrays and the enum form of |
| 73 | +`transform` outright. Take the prefixed name for the domain you are in. |
| 74 | + |
| 75 | +## Authored metadata needs no migration |
| 76 | + |
| 77 | +This renames TypeScript exports and two internal JSON Schema `$def`s — **not a single |
| 78 | +authorable key**. All eleven keys carry over unchanged, verified by the |
| 79 | +`authorable-surface.json` ratchet rather than by inspection: |
| 80 | + |
| 81 | +- `connectors[].fieldMappings[]` — `source`, `target`, `transform`, `defaultValue`, |
| 82 | + `dataType`, `required`, `syncMode` (7) |
| 83 | +- `mapping.fieldMapping[]` — `source`, `target`, `transform`, `params` (4) |
| 84 | + |
| 85 | +Same names, same types, same defaults, same strictness. Existing stack metadata, |
| 86 | +stored `sys_metadata` rows and published apps are byte-for-byte unaffected, which is |
| 87 | +why this ships with **no ADR-0087 conversion and no tombstone**: nothing was retired. |
| 88 | +The `major` is for the two renamed TypeScript exports alone — the only edit an upgrade |
| 89 | +needs is the import above. |
| 90 | + |
| 91 | +The published JSON Schema `$id`s move with the defs: |
| 92 | +`…/integration/FieldMapping.json` → `…/integration/ConnectorFieldMapping.json`, and |
| 93 | +`…/data/FieldMapping.json` → `…/data/ImportFieldMapping.json`. |
| 94 | + |
| 95 | +## Gate change riding along |
| 96 | + |
| 97 | +`scripts/lib/renamed-defs.ts` (the #4684 carry-over table) gets its first entries |
| 98 | +beyond the original one, and with them the first rules that only bind when the table |
| 99 | +holds **more than one**: |
| 100 | + |
| 101 | +- **two sources onto one target is rejected.** That is a merge, not two renames, and |
| 102 | + it defeats the table's purpose: `build-schemas.ts` carries the snapshot into a map |
| 103 | + keyed by the *new* key, so two defs' entries for one property name collapse — and |
| 104 | + the surviving `[RETIRED]` state is whichever was carried last. A key live under one |
| 105 | + def and tombstoned under the other would then read as already-retired, and the |
| 106 | + "every live → retired transition needs a registered conversion" check would never |
| 107 | + fire for it. |
| 108 | +- **a chained rename (A → B → C) is rejected by name.** It was already red as |
| 109 | + "B is not emitted", which is true but misdiagnoses it as a typo; the carry is a |
| 110 | + single pass, so chains are unsupported outright. |
0 commit comments