Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .changeset/32717-rollback-door-one-answer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
'@objectstack/metadata': patch
'@objectstack/metadata-core': patch
---

fix(metadata)!: on a name one row slot holds, the manager's addressed history and rollback answer the versions attributed to no package and the environment's own as that slot's, a version's existing history row (never the restored body's package stamp) says whose version it is, and a rollback writes its body stamped for the row slot it lands in (#32717)

Clause-②: yes (narrowing) — for a type stored per `(package, name)` (`PACKAGE_SLOTTED_STORE_TYPES`; at this release `api`, `agent`, `skill`, `action`, `email_template`, `permission`, `flow`, `picklist`, `mapping`, `dataset`, `webhook`, `tool`). Widened: `MetadataManager.getHistory`, `diff` and `rollback`, and `DatabaseLoader.getHistoryRecord` and `queryHistory`, addressed (`{ packageId }`, a package or `null` for the environment) to the name's one row slot, left out a version attributed to no package, and (addressing a package) the environment's own version, answering `Version N not found in history for TYPE/NAME (package PKG)` (or `(environment)`), and now list, diff and restore it into that row, as naming no package does; a direct `DatabaseLoader.registerRollback` to a version attributed to no package, on a name one row slot holds, with a restored body stamped (`_packageId`, or the envelope's `packageId`) for a package that holds no row of the name, answered `409 RESOURCE_CONFLICT` (`not-held-by-qualifier`) and now lands in the one row. Narrowed: a direct `DatabaseLoader.registerRollback` addressing a package to a version attributed to no package, on a name two or more row slots hold, with a restored body stamped for a package, wrote it into that package's row (the address and the stamp agreeing), or answered a plain `Error` that the version was that package's, or `409 not-held-by-qualifier` naming a stamped package that holds no row, and now answers `409 RESOURCE_CONFLICT` (`ambiguous-in-environment`, naming every holder), writing nothing.
Metadata-protocol: none

<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed, renamed or re-shaped, and no stored row changes shape or is rewritten: what moves is which answer the metadata service's history and rollback methods give, and the package stamp of the body a rollback writes from now on, so there is nothing for `objectstack migrate meta` to rewrite and no tombstone. -->

**BREAKING** for a direct caller of `DatabaseLoader.registerRollback` that addresses a package and restores a version recorded with no attribution (a history row written before `sys_metadata_history.package_id`, or one `SysMetadataRepository` wrote before it recorded the column) on a name several row slots hold, relying on the restored body's package stamp to say whose version it is. Nothing records whose it is, so it is refused naming every holder, as `MetadataManager.rollback` already refused it. No first-party caller reaches it: `MetadataManager.rollback`, the one caller in the workspace, restores a history row's own body, and a row attributed to none has a body naming no package. The fix: write the body into the package's row with a save (`save(type, name, body)` with the body's `_packageId` naming that package), which records a version attributed to it.

ADR-0150 D3/D4, seat ruling 6885958183 (amended B). `MetadataManager` and `DatabaseLoader` give one answer, and on a name one row slot holds the `/meta` history and rollback doors (`SysMetadataRepository`, `isVersionOf`) give it too:

- **A sole row slot's versions.** On a name one row slot holds, a version attributed to no package and the environment's own version are that slot's, addressed or not: a package-less row at a held name is that one holder's customization (ADR-0150 D4), so the address and the bare name write the same row. An addressed history read lists exactly what an addressed rollback restores: FROM "not found" at `MetadataManager.rollback`, `diff` and the addressed `getHistory` (which left such a version out) while `DatabaseLoader.registerRollback` restored it, TO listed, diffed and restored at every door. On a name several row slots hold such a version is in no addressed history and its rollback is refused `409 ambiguous-in-environment` naming every holder, addressed or not, as before.
- **Whose version it is.** FROM the version's history row's attribution, else the restored body's package stamp when the row was attributed to none, TO the history row's attribution whenever the row exists; the body's stamp stands in only for a version with no history row at all (history not tracked, or no row of that version).
- **The body a rollback writes.** FROM the restored body as handed in, TO the body stamped for the row slot it lands in: a package stamp (`_packageId`, or the envelope's `packageId`) naming another package than the slot's is replaced by the slot's package, or removed from a body the environment's package-less row stores. A body naming no package, or the slot's own, is written byte for byte, as before. Before, a foreign stamp written into the environment's row handed that row to the stamped package (`DatabaseLoader` keys a row whose `package_id` names no package by its body's stamp), and one written into a package's row served that package's item stamped for another. `DatabaseLoader.registerRollback` now resolves to the body it wrote, and `MetadataManager.rollback` files and returns that body.

`@objectstack/metadata-core` ships a documentation change only: the `sys_metadata_history.package_id` doc comment (in its published `dist`) says NULL means "not attributed", never the environment, and that such a row is the version of the name's one row slot and of no slot while several hold the name. No field, type or export changes.
26 changes: 16 additions & 10 deletions packages/metadata-core/src/objects/sys-metadata-history.object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,12 @@ import { ObjectSchema, Field } from '@objectstack/spec/data';
* continues incrementing instead of restarting at 1.
*
* • `package_id` names whose row a snapshot of a type stored per
* `(package, name)` was taken of (ADR-0150 D3/D4); such a type's
* `version` is numbered per name across its packages' rows, so the
* unique `(type, name, version)` holds. NULL on every other row.
* `(package, name)` was taken of (ADR-0150 D3/D4): a package, or the
* `sys_metadata` mark for the environment's package-less row. Such a
* type's `version` is numbered per name across its rows, so the unique
* `(type, name, version)` holds. NULL means "not attributed", never the
* environment: every other type's row, and such a type's row written
* before a writer recorded the column.
*
* • `metadata` / `checksum` are nullable — DELETE rows have no body or
* hash. Readers must tolerate null on both columns.
Expand Down Expand Up @@ -102,13 +105,16 @@ export const SysMetadataHistoryObject = ObjectSchema.create({
* type stored per `(package, name)` (`PACKAGE_SLOTTED_STORE_TYPES`,
* `@objectstack/metadata`): its versions are numbered per name across the
* packages' rows, so this column, not the version, says whose version it is.
* NULL for every other type, and for a row written before the column
* existed (its package is then the one its stored body names, as for a
* `sys_metadata` row); a package-less row's snapshot carries the
* `sys_metadata` mark. [#32905] `SysMetadataRepository` (the `/meta`
* doors) records it as `DatabaseLoader` does, so the `/meta` history and
* rollback doors read whose version a row is; the rows it wrote before
* that carry NULL. Nullable: adding it rewrites no row.
* A package-less row's snapshot carries the `sys_metadata` mark. NULL
* means "not attributed", never the environment: every other type's row,
* and such a type's row written before the column existed, or before
* [#32905] `SysMetadataRepository` (the `/meta` doors) recorded it as
* `DatabaseLoader` does. Such a row is read as the package its stored
* body names (its `_packageId` stamp), if it names one, and else as
* attributed to none (`historyRowAttribution`, `@objectstack/metadata`):
* the version of the name's one row slot, whichever package or the
* environment holds it, and of no slot while several hold the name.
* Nullable: adding it rewrites no row.
*/
package_id: Field.text({
label: 'Package ID',
Expand Down
Loading
Loading