Skip to content

Commit 2c8051e

Browse files
committed
Merge origin/main; flip the #6562 tripwire (#6810)
#6562 (PR #6811) landed while this branch was in flight and factored the tenant column into `TENANT_SCOPE_FIELD_DEF`. Conflict resolved onto that shared table: the definition is now spread VERBATIM, with the `indexed` key gone and the tenant index declared in `indexes[]`. `protocol-meta-effective-schema.test.ts` carries the three-line tripwire #6562 pinned in both directions so this fix could not be forgotten. All three flip to the post-fix truth, annotated with #6810 and with the old expectation quoted in place so the reversal reads as a record, not a rewrite: divergences(...) ['organization_id.indexed'] → [] registryBacked._diagnostics valid: false → { valid: true } organization_id.indexed === multiTenant → undefined, read off `indexes[]` One residual is recorded rather than left to be rediscovered: the DECLARATION does not converge the way the field set does — the overlay-backed answer is rebuilt from the stored body, which declares no indexes. Inert on that surface (drivers materialize from the REGISTERED schema, never a served document) and both answers parse green either way. `metadata-core`'s `injected-system-columns.ts` header described the stamped key as live; corrected to describe it as closed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JE8Bbwb8qhau3yNtP3ftLJ
2 parents 482d847 + 5e247fd commit 2c8051e

7 files changed

Lines changed: 1333 additions & 116 deletions

File tree

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
---
2+
"@objectstack/metadata-core": minor
3+
"@objectstack/metadata-protocol": minor
4+
"@objectstack/objectql": patch
5+
---
6+
7+
fix(metadata-protocol): a `/meta` object read serves the effective runtime schema, whichever layer answered (#6562)
8+
9+
`GET /api/v1/meta/object/:name` answered a **different set of fields** depending
10+
on which link of its resolution chain produced the answer, for the same object:
11+
12+
- **registry-backed** → the schema AFTER `applySystemFields`, so it carried the
13+
injected system columns — `created_at`, `created_by`, `updated_at`,
14+
`updated_by`, `organization_id`, `owner_id`, `owning_business_unit_id` — even
15+
when the author declared none of them;
16+
- **overlay-backed** (a `sys_metadata` customization row, or a MetadataService
17+
body) → the stored document VERBATIM, so every one of those columns was simply
18+
absent.
19+
20+
Whether an object carries an overlay is invisible to the caller, so the same
21+
request reported the platform's own columns or not, and nothing in the response
22+
said which had happened. `/meta` is the machine-readable contract clients and AI
23+
authors code against: an author reading an overlay-backed object saw no
24+
`created_at` / `owner_id` / `organization_id` and reasonably concluded the
25+
columns do not exist — while every one of them is real in the database,
26+
filterable, orderable, and enforced read-only on write.
27+
28+
**Every `/meta` object read exit now serves the effective schema.** The
29+
single-item read, the list, the cached/ETag branch, both draft reads and the
30+
layered read's `effective` layer all report the injected columns, with the same
31+
`readonly` / `system` markers the engine enforces (`owner_id` stays
32+
`readonly: false` — ownership is transferable). This is the presence half of the
33+
seam #4513 closed the value half of.
34+
35+
Three things deliberately did **not** change:
36+
37+
- **`?layers=1`'s `overlay` layer stays byte-verbatim.** Injection happens at the
38+
read exits only, so Studio's "what you customised" diff never shows a column
39+
nobody wrote. Only `effective` is injected.
40+
- **A `GET``PUT` round-trip still persists a byte-identical body** (#4326).
41+
The write path gained the strip counterpart: a field byte-identical to the
42+
platform's own definition is removed again on save, so a served document handed
43+
straight back stores exactly what it stored before — same checksum, same
44+
history diff. A declared `owner_id` carrying the author's own label is *not*
45+
the platform's definition and survives untouched.
46+
- **A declared system column stays the author's.** Injection only ever adds a
47+
column nobody declared; it never rewrites one that was.
48+
49+
Which columns an object carries is `resolveInjectedSystemColumns`
50+
(`@objectstack/spec/data`, #5378) — the same derivation `applySystemFields`
51+
consumes — so every opt-out (`systemFields: false`, `managedBy: 'better-auth'`,
52+
`systemFields.audit`/`.tenant`, `tenancy.enabled: false`, the per-tier
53+
`ownership` table, the `sys_*` namespace) is answered in one place and re-derived
54+
in none. **What** each column looks like moves to `@objectstack/metadata-core`
55+
(`AUDIT_FIELD_DEFS` and the three tenancy/ownership anchors, re-exported from
56+
`@objectstack/objectql` so the symbols still resolve there) — the same relocation,
57+
for the same dependency cycle, as the audit-governance table in #4513:
58+
`@objectstack/objectql` depends on `@objectstack/metadata-protocol`, so the read
59+
path could not import the definitions from the registry that provisions them.
60+
One table now feeds the injection pass and the read exits, so they cannot drift.
61+
62+
One key is deliberately not carried onto a served document: `organization_id`'s
63+
`indexed`. It is not a `FieldSchema` key — removed in the 16.x line (#2377,
64+
ADR-0049) and rejected by name by the strict schema — and its only consumer is
65+
`driver-mongodb`'s schema builder, which reads the registered schema and never a
66+
served document. It stays at the injection site; that the registry-backed read
67+
answers `_diagnostics: { valid: false }` because of it is filed as #6810.

packages/metadata-core/src/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,15 @@ export * from './engine-update-dispatch.js';
3636
// reporting two.
3737
export * from './audit-field-governance.js';
3838

39+
// [#6562] The injected-system-column DEFINITION table and the served-document
40+
// injection/strip pair built on it, sunk here by the same criterion and for the
41+
// same cycle as the governance table above. `resolveInjectedSystemColumns`
42+
// (spec, #5378) says WHICH columns an object carries; this says WHAT each one
43+
// looks like — the half that used to exist only inside `applySystemFields`, one
44+
// import away from every `/meta` read exit and unreachable from all of them.
45+
// `@objectstack/objectql` now reads this table instead of its own literals.
46+
export * from './injected-system-columns.js';
47+
3948
// [ADR-0106 / #3682] The metadata-plane FLS projection — one masking function
4049
// and one fingerprint, shared by every object-schema exit in
4150
// `@objectstack/rest` and `@objectstack/runtime`. Sunk here by the same

0 commit comments

Comments
 (0)