Skip to content
12 changes: 12 additions & 0 deletions .changeset/21442-by-name-expanded-view.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@objectstack/metadata-protocol': patch
---

fix(metadata-protocol): a view a stored view container expands answers by name what the object door lists, on every kernel and for every container scope

Clause-②: no

- **What changed.** `GET /api/v1/meta/view?object=…` lists the views a stored view container expands, and the by-name read now answers each of those names with the same item. Before, `getMetaItem` expanded no container: it answered such a name only on an unscoped kernel and only for an environment-wide container, where the registry held a hydrated copy. On an environment-scoped kernel, and for an organization-scoped container on any kernel, it answered nothing. Where the name is one a package also ships, such as `<object>.default` under a tenant's overlay of that package's container, it answered the packaged view while the list served the overlay's.
- **How.** The by-name read selects the stored containers in the caller's scope with the list read's own row selection, and expands them with the list read's own expansion. Nothing is persisted or registered, and a stored row of the name itself still answers first.
- **Layers, history and diff for such a name.** `getMetaItemLayered` reports the container's own stored row as `overlay`, with the scope it was read from as `overlayScope`, and the expanded view as `effective`. `historyMetaItem` and `diffMetaItem` answer exactly what they answer under the container's own name, and say so: every event's `ref.name` and the diff's `name` are the container's. No history is made up for a name that was never stored.
- **What does not change.** The container's own name still answers its stored row. The save door is unchanged, including a write by an expanded name. No response shape gains or loses a key.
Original file line number Diff line number Diff line change
Expand Up @@ -97,33 +97,40 @@ const storedRow = (
* The engine double: `findOne` over a row table, plus the registry surface the
* layered read touches on its way past the overlay.
*
* ⛔ No `find` / `insert` / `update` / `delete`, deliberately — the read path
* under test issues exactly one verb, and a double declaring verbs no case
* exercises would owe `check:engine-double-contract` a dispatch contract that
* protects nothing. Same shape the two sibling read-gate pins drive.
* `find` is the second verb the read path issues, and only for a `view` name
* with no stored row of its own: the read then selects the stored view
* containers that might expand that name, through the list read's own row
* selection (#21442). It records into `finds` — the same partitions question
* asked of that read. ⛔ No `insert` / `update` / `delete`, deliberately — a
* double declaring verbs no case exercises would owe
* `check:engine-double-contract` a dispatch contract that protects nothing. Same shape the two sibling read-gate pins drive.
*/
function makeHarness(rows: StoredRow[]) {
const findOnes: Array<Record<string, unknown>> = [];
const finds: Array<Record<string, unknown>> = [];
const matching = (where: Record<string, unknown>) => {
// `check:where-matcher` — a hand-written matcher with no combinator
// branch reads `$and` as a field name and answers the wrong question
// rather than failing. Refuse the shape this double does not
// implement, matching the sibling doubles' convention.
for (const k of Object.keys(where)) {
if (k.startsWith('$')) {
throw new Error(`[test double] unsupported WHERE combinator '${k}'`);
}
}
return rows.filter((r) =>
Object.entries(where).every(([k, v]) => {
if (v === undefined) return true;
return (r as unknown as Record<string, unknown>)[k] === v;
}),
);
};
const engine: any = {
async findOne(table: string, opts?: { where?: Record<string, unknown> }) {
if (table !== 'sys_metadata') return undefined;
const where = opts?.where ?? {};
findOnes.push({ ...where });
// `check:where-matcher` — a hand-written matcher with no combinator
// branch reads `$and` as a field name and answers the wrong
// question rather than failing. Refuse the shape this double does
// not implement, matching the sibling doubles' convention.
for (const k of Object.keys(where)) {
if (k.startsWith('$')) {
throw new Error(`[test double] unsupported WHERE combinator '${k}'`);
}
}
return rows.find((r) =>
Object.entries(where).every(([k, v]) => {
if (v === undefined) return true;
return (r as unknown as Record<string, unknown>)[k] === v;
}),
);
return matching(where)[0];
},
registry: {
registerItem: () => undefined,
Expand All @@ -137,8 +144,14 @@ function makeHarness(rows: StoredRow[]) {
applyNavContributions: (app: unknown) => app,
},
};
engine.find = async (table: string, opts?: { where?: Record<string, unknown> }) => {
if (table !== 'sys_metadata') return [];
const where = opts?.where ?? {};
finds.push({ ...where });
return matching(where);
};
const protocol = new ObjectStackProtocolImplementation(engine, () => new Map()) as any;
return { protocol, findOnes };
return { protocol, findOnes, finds };
}

/** Every `organization_id` partition the engine was asked for, deduplicated. */
Expand Down Expand Up @@ -443,11 +456,14 @@ describe('§5 an already-gating caller receives the same scope it did before', (
// for `view` gets the org partition read, exactly as before.
const gated = organizationIdForMetaRead('view', ORG);
expect(gated).toBe(ORG);
const { protocol, findOnes } = makeHarness([]);
const { protocol, findOnes, finds } = makeHarness([]);
await protocol.getMetaItemLayered({
type: 'view', name: 'probe', organizationId: gated,
});
expect(partitions(findOnes)).toEqual([null, ORG]);
// [#21442] `probe` has no row of its own, so the read also selects the
// stored containers that might expand it — from the same partitions.
expect(partitions(finds)).toEqual([null, ORG]);
});
});

Expand Down
58 changes: 37 additions & 21 deletions packages/metadata-protocol/src/get-meta-item-org-read-gate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,33 +109,40 @@ const storedRow = (
* The engine double: `findOne` over a row table, plus the registry surface the
* single-item read path touches on its way past the overlay.
*
* ⛔ No `find` / `insert` / `update` / `delete`, deliberately — the read path
* under test issues exactly one verb, and a double declaring verbs no case
* exercises would owe `check:engine-double-contract` a dispatch contract that
* protects nothing. Same shape the sibling plural-door pin drives.
* `find` is the second verb the read path issues, and only for a `view` name
* with no stored row of its own: the read then selects the stored view
* containers that might expand that name, through the list read's own row
* selection (#21442). It records into `finds` — the same partitions question
* asked of that read. ⛔ No `insert` / `update` / `delete`, deliberately — a
* double declaring verbs no case exercises would owe
* `check:engine-double-contract` a dispatch contract that protects nothing. Same shape the sibling plural-door pin drives.
*/
function makeHarness(rows: StoredRow[]) {
const findOnes: Array<Record<string, unknown>> = [];
const finds: Array<Record<string, unknown>> = [];
const matching = (where: Record<string, unknown>) => {
// `check:where-matcher` — a hand-written matcher with no combinator
// branch reads `$and` as a field name and answers the wrong question
// rather than failing. Refuse the shape this double does not
// implement, matching the sibling doubles' convention.
for (const k of Object.keys(where)) {
if (k.startsWith('$')) {
throw new Error(`[test double] unsupported WHERE combinator '${k}'`);
}
}
return rows.filter((r) =>
Object.entries(where).every(([k, v]) => {
if (v === undefined) return true;
return (r as unknown as Record<string, unknown>)[k] === v;
}),
);
};
const engine: any = {
async findOne(table: string, opts?: { where?: Record<string, unknown> }) {
if (table !== 'sys_metadata') return undefined;
const where = opts?.where ?? {};
findOnes.push({ ...where });
// `check:where-matcher` — a hand-written matcher with no combinator
// branch reads `$and` as a field name and answers the wrong
// question rather than failing. Refuse the shape this double does
// not implement, matching the sibling doubles' convention.
for (const k of Object.keys(where)) {
if (k.startsWith('$')) {
throw new Error(`[test double] unsupported WHERE combinator '${k}'`);
}
}
return rows.find((r) =>
Object.entries(where).every(([k, v]) => {
if (v === undefined) return true;
return (r as unknown as Record<string, unknown>)[k] === v;
}),
);
return matching(where)[0];
},
registry: {
registerItem: () => undefined,
Expand All @@ -149,8 +156,14 @@ function makeHarness(rows: StoredRow[]) {
applyNavContributions: (app: unknown) => app,
},
};
engine.find = async (table: string, opts?: { where?: Record<string, unknown> }) => {
if (table !== 'sys_metadata') return [];
const where = opts?.where ?? {};
finds.push({ ...where });
return matching(where);
};
const protocol = new ObjectStackProtocolImplementation(engine, () => new Map()) as any;
return { protocol, findOnes };
return { protocol, findOnes, finds };
}

/** Every `organization_id` partition the engine was asked for, deduplicated. */
Expand Down Expand Up @@ -389,9 +402,12 @@ describe('§4 an already-gating caller receives the same scope it did before', (
// the org partition read, exactly as before this change.
const gated = organizationIdForMetaRead('view', ORG);
expect(gated).toBe(ORG);
const { protocol, findOnes } = makeHarness([]);
const { protocol, findOnes, finds } = makeHarness([]);
await protocol.getMetaItem({ type: 'view', name: 'probe', organizationId: gated });
expect(partitions(findOnes)).toEqual([null, ORG]);
// [#21442] `probe` has no row of its own, so the read also selects the
// stored containers that might expand it — from the same partitions.
expect(partitions(finds)).toEqual([null, ORG]);
});
});

Expand Down
Loading
Loading