Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
---
'@objectstack/metadata-protocol': minor
---
Expand All @@ -16,4 +16,4 @@

**The fix.** Drop the body's `name` (the door stamps the save name), or set it to the name the container is saved under.

Not judged here: a standalone view record (`viewKind`) and every other metadata type.
A standalone view record (`viewKind`) and every other metadata type are judged too, by the same release's every-type refusal at the save, restore and publish doors (its own entry).
2 changes: 1 addition & 1 deletion .changeset/21412-metadata-view-container-name-judge.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
---
'@objectstack/metadata': minor
---
Expand All @@ -6,5 +6,5 @@

Clause-②: yes

- New subpath `@objectstack/metadata/view-container-name`. It exports `viewContainerNameRefusal(container, sourceLabel, ownerId)`, the source registrars' entry, whose key is the object the container binds to (its own `object`, else `list.data.object` / `form.data.object`). It also exports `savedViewContainerNameRefusal(container, saveName)`, the runtime save door's entry, whose key is the name the row is saved under, and the `ViewContainerNameRefusal` type. Both return a `VALIDATION_ERROR` / 400 refusal for an aggregated view container whose own `name` is set and differs from that key, and `undefined` otherwise. A container with no `name`, and a standalone view record (`viewKind`), are not judged.
- New subpath `@objectstack/metadata/view-container-name`. It exports `viewContainerNameRefusal(container, sourceLabel, ownerId)`, the source registrars' entry, whose key is the object the container binds to (its own `object`, else `list.data.object` / `form.data.object`). It returns a `VALIDATION_ERROR` / 400 refusal for an aggregated view container whose own `name` is set and differs from that key, and `undefined` otherwise; a container with no `name`, and a standalone view record (`viewKind`), are not judged by it. The subpath also exports `savedItemNameRefusal(type, item, saveName, door)`, the runtime write doors' entry, which judges every metadata type against the name the row is written under, and the `ViewContainerNameRefusal` type both entries return.
- The artifact/HMR loader's container branch now refuses such a container through the judge, before it files anything. What it refuses and the envelope are unchanged (`VALIDATION_ERROR` / 400). The message is now the judge's, the words the ObjectQL boot loop and `os validate` print, where it was the generic `IMetadataService.register` contract's.
21 changes: 21 additions & 0 deletions .changeset/21470-metadata-protocol-every-write-door-item-name.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
'@objectstack/metadata-protocol': minor
---

Every runtime door that writes a metadata row refuses a body whose own `name` disagrees with the row's name, for every metadata type

Clause-②: no (narrowing)

<!-- adr-0087: not-required (no-migration-prescription) A validity narrowing at the runtime write doors over an existing key: no `name` key of any metadata schema is removed, renamed or re-shaped, so there is no tombstone and nothing mechanical for `objectstack migrate meta` to rewrite. Which of the two names a divergent body meant (its own, or the one it was saved under) is authoring intent no conversion entry can decide. The at-rest census found no such row: 0 `sys_metadata` and 0 `sys_metadata_history` rows in the four bootable example apps, booted with their seeds; the hosted-tenant shape was not measured. New writes are refused with the remedy; a row stored before this change keeps its bytes and stays readable. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id covers this rule and this diff adds none (not registered / already-registered); and the change narrows what runtime write doors accept, not a runtime interface or a type surface alone (not runtime-interface-only / type-surface-only). -->

**BREAKING** accept-set narrowing at the runtime write doors, shipped as `minor` under the repo's launch-window convention for breaking changes, the grade the view-container half of this refusal takes in the same release.

**What was accepted before.** The runtime stores a metadata row under the name the request names and registers its body under the body's own `name`. These doors accepted a body whose `name` was not the row's, so the row answered under a name nobody saved it under, and under none by its own:

- `saveMetaItem`, which `PUT /api/v1/meta/:type/:name` and the dispatcher's metadata save both call, for every type but a view container (a dashboard saved as `dash_a` with `name: 'dash_b'` registered as `dash_b`; a record view saved as `crm_lead.mine` with `name: 'crm_lead.other'` registered as `crm_lead.other`);
- `rollbackMetaItem` and `revertCommit`, which wrote such a stored history version back as the active row without passing `saveMetaItem`;
- `publishMetaItem` and `publishPackageDrafts`, which promoted such a stored draft the same way.

**What is refused now.** Each of those bodies, with `VALIDATION_ERROR` / 400, before anything is stored or registered, through the judge the view-container refusal already used (`savedItemNameRefusal`, `@objectstack/metadata/view-container-name`). `rollbackMetaItem` and `publishMetaItem` throw it. `revertCommit` reports the item in `failed[]` with `code: 'VALIDATION_ERROR'`. `publishPackageDrafts` aborts the batch on it, as it does on any refused draft: nothing in the batch is published. A body with no `name` is accepted as before. A `name` the body carries is judged whatever its value; a `translation` saved with `name: ''`, which its schema accepts, is now refused instead of being registered under the empty string. A view at the save door is the exception: a missing or empty view `name` is still stamped with the save name. A `field` written through the `OS_METADATA_WRITABLE` operator hatch is accepted only without a body `name`: its row is named `object.field`, which the column `name` cannot spell, and registered it answered under the column name alone. Where the type's schema already refused such a body (an empty or non-string `name` on most types, any `name` on a `seed`, whose schema declares none), the answer is now this refusal (`VALIDATION_ERROR` / 400) instead of the schema's `INVALID_METADATA` / 422; nothing is stored either way.

**The fix.** Set the body's `name` to the name you save it under, or save the item under the body's own `name`; for a view or a `field`, dropping `name` works too. To bring back a version or a draft that carries another `name`, save the item again with that fix, and publish that save if it is a draft.
12 changes: 12 additions & 0 deletions .changeset/21470-metadata-write-door-item-name-judge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@objectstack/metadata': minor
---

The runtime write doors' `name` judge covers every metadata type: `savedItemNameRefusal` replaces `savedViewContainerNameRefusal` on `@objectstack/metadata/view-container-name`

Clause-②: yes

- `savedItemNameRefusal(type, item, saveName, door)` is the one entry the runtime write doors of `@objectstack/metadata-protocol` call. It returns a `VALIDATION_ERROR` / 400 refusal when a body of any type carries its own `name` and that `name` differs from the name the row is written under, and `undefined` otherwise. `door` is `'save'`, `'restore'` or `'publish'`. A body with no `name` passes. A `name` the body does carry is judged whatever its value (`''`, `null` and non-strings included), with one exception: a `view` at the `'save'` door, which stamps a missing name there, is judged only on a non-empty string `name`.
- It replaces `savedViewContainerNameRefusal(container, saveName)`, which judged view containers only. That export was added to this subpath in this same release cycle and never shipped in a published version, so no published export is removed.
- The words name the type and give a remedy that works for it: "drop `name`, or set it to KEY" for a view, whose missing name the save door stamps; "drop `name`" for a `field`, whose row is named `object.field`, which its dot-free column `name` cannot spell; "set `name` to KEY, or save the item under NAME" for every other type; and at the restore and publish doors, whose caller cannot edit the stored body in place, the save that fixes it. For a view container at the save door the message is byte for byte the one `savedViewContainerNameRefusal` returned.
- `viewContainerNameRefusal` (the source registrars' entry), its words and the `ViewContainerNameRefusal` type are unchanged.
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,15 @@ const PROBES: Record<string, { name: string; item: Record<string, unknown> }> =
// Schema-valid on purpose, like the four above: `field` resolves
// `FieldSchema`, so a malformed body would 422 before the registry consult
// and the probe would prove nothing about the code-only gate.
//
// [#21470] And NAMELESS on purpose: the row is named `<object>.<field>`,
// which a dot-free `FieldSchema` `name` can never spell, so a body `name`
// would now be refused by the write doors' name judge (`VALIDATION_ERROR`
// / 400) once the hatch below opens the door — a different refusal from
// the one this probe measures. `FieldSchema` does not require `name`.
field: {
name: 'rc3_field_probe.zz_probe',
item: {
name: 'zz_probe',
label: 'Probe',
type: 'text',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,19 @@ const fieldRow = (type: string): Row => row({
metadata: JSON.stringify({ name: FIELD, label: 'Probe', type }),
});

/**
* [#21470] The body a hatch-path save SENDS: a field definition with no
* `name`. A `field` row is named `<object>.<field>`, which a `FieldSchema`
* `name` (dot-free) can never spell, so every runtime write door now refuses a
* `field` body whose `name` is set (`VALIDATION_ERROR` / 400, before this
* file's gates) — the write would register the row under the column name.
* `FieldSchema` does not require `name`, so the nameless body is the one the
* hatch can carry, and what these cases measure is unchanged: the destructive
* gate's reach, not the body's name. The STORED rows above keep theirs, as a
* row written before that refusal sits at rest.
*/
const HATCH_BODY = (type: 'text' | 'number') => ({ label: 'Probe', type });

/** An `object` row carrying a real `fields` map — the only diffable shape. */
const objectRow = (name: string, fields: readonly string[]): Row => row({
type: 'object',
Expand Down Expand Up @@ -364,7 +377,7 @@ describe('[#11014 §2 reason 1] a `field` body has no `fields` map to diff', ()
const r = await save(protocol, {
type: 'field',
name: DOTTED,
item: { name: FIELD, label: 'Probe', type: 'number' },
item: HATCH_BODY('number'),
});

// It persisted — so the request reached, and passed, the gate.
Expand All @@ -379,7 +392,7 @@ describe('[#11014 §2 reason 1] a `field` body has no `fields` map to diff', ()
const r = await save(protocol, {
type: 'field',
name: DOTTED,
item: { name: FIELD, label: 'Probe', type: 'number' },
item: HATCH_BODY('number'),
});

expect(r.outcome).toBe('resolved');
Expand Down Expand Up @@ -469,7 +482,7 @@ describe('[#11014 §3] the double fault the trimmed limb used to catch', () => {
const r = await save(protocol, {
type: 'field',
name: DOTTED,
item: { name: FIELD, label: 'Probe', type: 'text' },
item: HATCH_BODY('text'),
});

expect(r.outcome).toBe('resolved');
Expand Down
Loading
Loading