Skip to content

Commit 66e7f58

Browse files
committed
Merge origin/main into claude/issue-22989-s2-host-config-metadata-door
Claude-Session: https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6 Co-authored-by: Claude <noreply@anthropic.com>
2 parents 8d4f551 + 0a29bec commit 66e7f58

15 files changed

Lines changed: 1680 additions & 27 deletions
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/objectql': patch
3+
---
4+
5+
fix(objectql): a `tree` field's `deleteBehavior` now takes effect when its parent record is deleted, and the default clears the reference
6+
7+
Clause-②: no
8+
9+
The spec gives a `tree` field (a record's parent in a hierarchy on its own object) a `deleteBehavior`, like a `lookup`. When the field declares none, the parse fills in `set_null`. The engine's referential delete pass only followed `master_detail` and `lookup` fields. So deleting a parent node returned success and left every child pointing at the deleted id. An authored `cascade` or `restrict` on a `tree` did nothing either.
10+
11+
A by-id delete now applies a `tree` field's behaviour the way it applies a `lookup`'s:
12+
13+
- `set_null` (the default): the children stay, and their parent field is cleared.
14+
- `cascade`: the whole subtree is deleted, at any depth, and nothing outside it. A cycle in the data (a row whose parent is its own descendant, or itself) ends: each row is deleted once.
15+
- `restrict`: deleting a node that still has children is refused with `DELETE_RESTRICTED` (409), and the dependent named is the tree's own object. A leaf node still deletes.
16+
- On a required `tree` field, `set_null` cannot clear the parent, so deleting a node that has children is refused with `DELETE_RESTRICTED`, as it is for a required `lookup`.
17+
18+
Deleting a record of an object that has a `tree` field now runs as one transaction when the object is on the default datasource, as every delete that can cascade already does. If a node deep in the subtree refuses its delete (a `beforeDelete` hook, say), the nodes already deleted are restored and the call is refused. `lookup` and `master_detail` behave exactly as before.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/metadata-core': patch
3+
'@objectstack/runtime': patch
4+
---
5+
6+
fix(metadata-core, runtime): a multi-package artifact built before a retirement boots at `os dev -a` and `os start`. The ADR-0087 forward conversion walks every `packages[].manifest` body, and the runtime's artifact load converts the bundle once, before any reader sees it
7+
8+
Clause-②: no
9+
10+
**What an operator saw.** A multi-package release artifact (ADR-0130 D4: `os build` of a `composeStacks(…, { manifest: 'preserve' })` project) carries its collections in `packages[i].manifest`. Take such an artifact built before a retirement, for example with a dashboard still spelled `refreshInterval`. `os dev -a dist/objectstack.json` and `os start` exited at boot with "`packages[1]` is not a package entry (ADR-0130 D4): manifest.dashboards.0.refreshInterval: `dashboard.refreshInterval` was renamed to `refreshIntervalSeconds`…". The metadata plugin's artifact door refused the same artifact at its strict parse. The ADR-0087 conversion converted that dashboard when it sat in the artifact's own `dashboards`, but it never read a package body, and the runtime read the bundle raw before any conversion ran.
11+
12+
**What an operator sees now.** The same artifact boots at `os dev -a` and at `os start`. Each body's items are served converted (`refreshIntervalSeconds: 300`, valid), each owned by its own package. The boot log names each conversion once, with the body it found it in: `[MetadataPlugin] artifact '…' … converted 2 site(s) forward via ADR-0087 conversion 'dashboard-refresh-interval-to-refresh-interval-seconds' (first at packages[0].manifest.dashboards[0].refreshIntervalSeconds) …`. The artifact file itself is unchanged, so it converts again on every boot until it is rebuilt with `os build`. A body built after the retirement converts nothing and logs nothing. An artifact authored past every retirement is still refused with the tombstone inside its body.
13+
14+
- **`@objectstack/metadata-core`.** `applyArtifactForwardConversions` runs the same conversion over each package body as over the artifact's own collections. It uses the same registry and the same window, decided once from the artifact's own `engines.protocol`; a body's own `engines.protocol` is not read. A notice from a body is located from the artifact root (`packages[1].manifest.…`).
15+
- **`@objectstack/runtime`.** `loadArtifactBundle` converts the loaded artifact once, before it merges the runtime module and before the stack, `AppPlugin` or the `manifest` service read it. The database resolver reads the artifact file itself, and it runs the same conversion on what it reads. So a multi-package artifact that declares its default datasource in a package body opens that database, not the unified default.
16+
17+
**Unchanged.** An artifact without `packages[]` converts exactly as before, with the same notices, paths and messages, and it is served the same way.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@objectstack/metadata-protocol': patch
3+
---
4+
5+
fix(metadata-protocol): a metadata row records who created it and who last changed it
6+
7+
Clause-②: no
8+
9+
- **What was wrong.** A metadata write that named its author stored no author on the `sys_metadata` row. `created_by` and `updated_by` read null on every row, though the repository's own schema mapping declared `actor → updated_by` and its read side reads that column. Only the history row's `recorded_by` named the writer, so "who changed this object?" could be answered from history alone.
10+
- **What it does now.** Every metadata write stamps the row from the write's actor: `created_by` when the row is first written, and `updated_by` on every write. Studio saves, `PUT /api/v1/meta/:type/:name`, publishing a draft, restoring a version and an in-process `saveMetaItem({ actor })` all write through this one path. A write with no actor (a boot sync, a migration with no user, an unauthenticated internal call) stores null in both columns, never a placeholder user. A save that changes nothing writes nothing, so it leaves both columns as they were.
11+
- **What an operator or a consumer sees.** A row written from now on names its creator and its last writer, so a consumer (such as cloud's AI build agent, whose staged drafts name their author) can read the author from the row itself. The pending-changes list (`listDrafts`' `updatedBy`) names the draft's last writer. Rows written before this release keep their null columns until their next write. History's `recorded_by` is unchanged.

0 commit comments

Comments
 (0)