|
| 1 | +--- |
| 2 | +"@objectstack/spec": minor |
| 3 | +"@objectstack/objectql": patch |
| 4 | +"@objectstack/platform-objects": patch |
| 5 | +--- |
| 6 | + |
| 7 | +feat(spec)!: shrink the `ApiMethod` enum to the six primitives — legacy values are stripped at parse, never honored (#3543, P2 of #3391) |
| 8 | + |
| 9 | +**BREAKING** (ships as `minor` per the lockstep launch-window policy — every |
| 10 | +package versions together, so a `major` here would promote the entire |
| 11 | +monorepo; the `!` marker and this changeset are the breaking-change record): |
| 12 | +the authored `enable.apiMethods` enum is now exactly the six |
| 13 | +primitives (`get`, `list`, `create`, `update`, `delete`, `bulk`). The eight |
| 14 | +legacy values (`upsert`, `aggregate`, `history`, `search`, `restore`, `purge`, |
| 15 | +`import`, `export`) are no longer authorable — they are DERIVED effective |
| 16 | +operations, resolved by the server's single derivation table. |
| 17 | + |
| 18 | +**Migration (FROM → TO).** Replace each legacy value with the primitives it |
| 19 | +derives from, then de-duplicate; if the result names all six primitives, delete |
| 20 | +the `apiMethods` key entirely (equivalent to default-open, and it tracks future |
| 21 | +primitives): |
| 22 | + |
| 23 | +| FROM (legacy) | TO (primitives) | why | |
| 24 | +| ------------- | -------------------- | ------------------------------------------ | |
| 25 | +| `upsert` | `create`, `update` | upsert ⊆ create ∧ update | |
| 26 | +| `import` | `create`, `update` | import ⊆ create ∨ update (writeMode-precise) | |
| 27 | +| `export` | `list` | export ⊆ list | |
| 28 | +| `aggregate` | `list` | aggregate ⊆ list | |
| 29 | +| `search` | `list` | search ⊆ list ∧ `searchable` | |
| 30 | +| `history` | `get` | history ⊆ get ∧ `trackHistory` | |
| 31 | +| `restore` | *(delete the value)* | never derives — `enable.trash` retired (#2377) | |
| 32 | +| `purge` | *(delete the value)* | never derives — `enable.trash` retired (#2377) | |
| 33 | + |
| 34 | +Reporter codemod: `node scripts/codemod/apimethods-legacy-to-primitives.mjs` |
| 35 | +(scans, reports the exact replacement per site, and flags whitelists the |
| 36 | +mapping would WIDEN so the edit stays reviewable). |
| 37 | + |
| 38 | +**Stored metadata keeps parsing — permanent tolerance, narrowing only.** Real |
| 39 | +metadata does not upgrade in lockstep with the spec, so a stored legacy value |
| 40 | +is NOT a parse error: `stripLegacyApiMethods` (new export) strips it with a |
| 41 | +FROM→TO warning (canonicalize-and-warn). Stripping only ever NARROWS exposure — |
| 42 | +the derivation table still grants every legacy verb that derives from the |
| 43 | +primitives you declared. Two cliffs to know: |
| 44 | + |
| 45 | +1. A whitelist of ONLY legacy values (e.g. `['upsert']`) strips to `[]` = |
| 46 | + **deny-all** — the object's API closes instead of widening. The strip |
| 47 | + warning and the objectql registration diagnostic both call this out. |
| 48 | +2. A legacy value NOT derivable from your declared primitives (e.g. |
| 49 | + `['get', 'export']` — export needs `list`) was honored by the P1 |
| 50 | + "explicit wins" path and is now denied. Declare the underlying primitive. |
| 51 | + |
| 52 | +**Type split — authored vs effective vocabulary.** `ApiMethod` (authored) is |
| 53 | +now six values; the NEW `ApiOperation` type / `ApiOperationSchema` / |
| 54 | +`API_OPERATION_ORDER` (fourteen values, byte-stable pre-shrink wire order) |
| 55 | +carry the EFFECTIVE vocabulary. The wire contract is unchanged: the 405 |
| 56 | +`allowed` array and `/me/permissions` `apiOperations` still serialize derived |
| 57 | +verbs (`export`, `search`, …), and `EffectiveObjectPermissionSchema.apiOperations` |
| 58 | +now validates against `ApiOperationSchema`. `EffectiveApiMethods.explicitLegacy` |
| 59 | +is removed (nothing is honored verbatim anymore); `API_METHOD_ORDER` remains as |
| 60 | +a deprecated alias of `API_OPERATION_ORDER`. |
| 61 | + |
| 62 | +**Fail-closed tightening (#3545):** a PRESENT but non-array `apiMethods` (only |
| 63 | +producible by a raw/out-of-band metadata write) now resolves to `deny-all` |
| 64 | +instead of unrestricted — a policy that exists but cannot be read fails CLOSED. |
| 65 | + |
| 66 | +**Published JSON Schema diverges deliberately:** `data/ApiMethod.json` is the |
| 67 | +strict six-value enum (a `z.preprocess` is not representable in JSON Schema), |
| 68 | +so external JSON-Schema validators reject legacy values that the zod parse |
| 69 | +would strip-and-warn. Treat the JSON Schema as the authored contract; the zod |
| 70 | +tolerance exists for stored metadata. |
| 71 | + |
| 72 | +**objectql:** the P1 "explicit wins" transition is reclaimed — |
| 73 | +`warnDeprecatedExplicitApiMethods` is replaced by `warnStrippedLegacyApiMethods` |
| 74 | +(a permanent per-object diagnostic for schemas that reach the registry without |
| 75 | +passing through Zod; the parse-time strip warning carries no object name). |
| 76 | + |
| 77 | +**platform-objects:** whitelist audit — `sys_business_unit`, |
| 78 | +`sys_business_unit_member` (P1's explicit `import`/`export` reclaimed) and |
| 79 | +`sys_user_preference` dropped their `apiMethods` entirely (each named all six |
| 80 | +primitives = default-open). Read-only and deny-all whitelists are unchanged; |
| 81 | +the seven `[]` declarations are deliberately KEPT as defense-in-depth alongside |
| 82 | +`apiEnabled: false`. |
0 commit comments