diff --git a/.changeset/21986-metadata-protocol-declines-stored-row-public.md b/.changeset/21986-metadata-protocol-declines-stored-row-public.md new file mode 100644 index 00000000000..79fad4e69de --- /dev/null +++ b/.changeset/21986-metadata-protocol-declines-stored-row-public.md @@ -0,0 +1,12 @@ +--- +"@objectstack/metadata-protocol": minor +--- + +`ObjectStackProtocolImplementation.declinesStoredRow(type, name)` is now public, so a door that serves a stored row out of the layered read can ask the same decision the reads make + +Clause-②: yes (widening) + +- The method answers `true` for exactly the names whose stored `sys_metadata` row the active reads (`getMetaItem`, the list, and the `effective` layer of `getMetaItemLayered`) do not adopt: a flow name a managed package ships (the answer `isShippedFlowName` gives), and a datasource name the host registers from code (one an installed package declares in `*.datasource.ts`, or the host's `default`). Every other type and name answers `false`. +- The `GET /meta/:type/:name/published` doors in `@objectstack/rest` and `@objectstack/runtime` now ask this method in place of `isShippedFlowName`. A door asks it, and does not restate either half or the host's code-datasource set. +- `isShippedFlowName` stays public and unchanged. +- The only change to the public surface is this one added method. No signature, schema or accept set changes, and no behaviour of this package changes. diff --git a/.changeset/21986-rest-published-door-code-datasource.md b/.changeset/21986-rest-published-door-code-datasource.md new file mode 100644 index 00000000000..95f62eea4f8 --- /dev/null +++ b/.changeset/21986-rest-published-door-code-datasource.md @@ -0,0 +1,10 @@ +--- +"@objectstack/rest": patch +--- + +`GET /api/v1/meta/datasource/:name/published` serves a code-defined datasource's code definition while a stored row under its name still exists + +Clause-②: no + +- For a datasource name the host registers from code (one an installed package declares in `*.datasource.ts`, or the host's `default`), the published door now serves the layered read's `effective` layer, which is the code definition. Before this change it served the leftover stored `sys_metadata` row, with that row's label, `origin` and connection settings, while `GET /api/v1/meta/datasource/:name`, the `/meta/datasource` list and `/layers` all served the code definition. The door now asks the protocol's `declinesStoredRow`, the one decision those reads make, in place of `isShippedFlowName`. A shipped flow name is answered as before. +- Unchanged: a runtime datasource's stored row is still what the door serves, and so is every stored row of every other type. A protocol that does not provide `declinesStoredRow` still gets the stored row. The row itself stays at rest, `/layers` still reports it in `overlay`, and `DELETE /api/v1/meta/datasource/:name` still removes it as the repair. diff --git a/.changeset/21986-runtime-published-door-code-datasource.md b/.changeset/21986-runtime-published-door-code-datasource.md new file mode 100644 index 00000000000..d721fe19249 --- /dev/null +++ b/.changeset/21986-runtime-published-door-code-datasource.md @@ -0,0 +1,10 @@ +--- +"@objectstack/runtime": patch +--- + +The runtime dispatcher's `GET /meta/datasource/:name/published` serves a code-defined datasource's code definition while a stored row under its name still exists + +Clause-②: no + +- This is the dispatcher twin of the `@objectstack/rest` published door, and it now answers the same way. For a datasource name the host registers from code (one an installed package declares in `*.datasource.ts`, or the host's `default`), the door serves the layered read's `effective` layer, which is the code definition, instead of the leftover stored row. It asks the protocol's `declinesStoredRow` in place of `isShippedFlowName`. A shipped flow name is answered as before. +- Unchanged: a runtime datasource's stored row, and every stored row of every other type, is served as before. So is every row when the protocol does not provide `declinesStoredRow`. diff --git a/packages/metadata-protocol/src/protocol.declines-stored-row-published.test.ts b/packages/metadata-protocol/src/protocol.declines-stored-row-published.test.ts new file mode 100644 index 00000000000..5173a9092c4 --- /dev/null +++ b/packages/metadata-protocol/src/protocol.declines-stored-row-published.test.ts @@ -0,0 +1,86 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * [#21986] `declinesStoredRow` is PUBLIC on `ObjectStackProtocolImplementation`, + * so the published-snapshot doors (`GET /meta/:type/:name/published`, the REST + * route and its dispatcher twin) can ask it instead of `isShippedFlowName`. + * + * What a door relies on, pinned here once: the one predicate answers BOTH name + * classes whose stored row the active reads decline, and nothing else. + * + * - a FLOW name the loader's set holds (`isShippedFlowName`'s answer); + * - a CODE-DEFINED DATASOURCE name: one an installed package declares, or one + * the host registers from code (`code-datasource-names`, here `default`). + * + * Every other name answers false: a flow no package ships, a runtime + * datasource, the same name under another type, a missing or empty name. + * + * The calls below go through the class's own declared type, never an `any` + * cast, so `tsc --noEmit` over this file (the package's `typecheck` includes + * `src/**`) fails if the member stops being public. + */ +import { describe, expect, it } from 'vitest'; +import { ObjectStackProtocolImplementation } from './protocol.js'; + +const FLOW_PACKAGE = 'com.example.pkg'; +const SHIPPED_FLOW = 'pkg_flow'; +const CUSTOMER_FLOW = 'customer_flow'; +const CODE_DS = 'showcase_external'; +const HOST_DS = 'default'; +const RUNTIME_DS = 'rt_datasource_21986'; + +/** + * A partial registry: the loader's entry for {@link SHIPPED_FLOW} carries its + * package id (the shape `lookupArtifactItem` reads off a registry with no + * `getArtifactItem`), and one installed package declares {@link CODE_DS}. The + * services registry carries the host's code-datasource set. + */ +function makeProtocol(): ObjectStackProtocolImplementation { + const loaderEntry = { name: SHIPPED_FLOW, label: 'Loader', _packageId: FLOW_PACKAGE }; + const engine = { + registry: { + getItem: (type: string, name: string) => + (type === 'flow' || type === 'flows') && name === SHIPPED_FLOW ? loaderEntry : undefined, + getAllPackages: () => [{ + manifest: { id: 'com.example.showcase', datasources: [{ name: CODE_DS, driver: 'sqlite', config: {} }] }, + }], + }, + }; + const services = new Map([['code-datasource-names', new Set([HOST_DS])]]); + return new ObjectStackProtocolImplementation(engine as never, () => services); +} + +describe('[#21986] the published predicate: declinesStoredRow answers both name classes, and only those', () => { + it('a shipped flow name, in either type spelling — what isShippedFlowName answers', () => { + const protocol = makeProtocol(); + for (const type of ['flow', 'flows']) { + expect(protocol.isShippedFlowName(type, SHIPPED_FLOW), type).toBe(true); + expect(protocol.declinesStoredRow(type, SHIPPED_FLOW), type).toBe(true); + } + }); + + it('a code-defined datasource name: one a package declares, and one the host registers from code', () => { + const protocol = makeProtocol(); + for (const type of ['datasource', 'datasources']) { + expect(protocol.declinesStoredRow(type, CODE_DS), type).toBe(true); + expect(protocol.declinesStoredRow(type, HOST_DS), type).toBe(true); + // Not a flow answer: the datasource half is its own. + expect(protocol.isShippedFlowName(type, CODE_DS), type).toBe(false); + } + }); + + it('control: every other name keeps its stored row', () => { + const protocol = makeProtocol(); + expect(protocol.declinesStoredRow('flow', CUSTOMER_FLOW)).toBe(false); + expect(protocol.declinesStoredRow('datasource', RUNTIME_DS)).toBe(false); + // The same names under another type. + expect(protocol.declinesStoredRow('object', CODE_DS)).toBe(false); + expect(protocol.declinesStoredRow('object', HOST_DS)).toBe(false); + expect(protocol.declinesStoredRow('view', SHIPPED_FLOW)).toBe(false); + // A name that is not one. + for (const name of [undefined, null, '', 42]) { + expect(protocol.declinesStoredRow('datasource', name), String(name)).toBe(false); + expect(protocol.declinesStoredRow('flow', name), String(name)).toBe(false); + } + }); +}); diff --git a/packages/metadata-protocol/src/protocol.ts b/packages/metadata-protocol/src/protocol.ts index 177e9170e4d..92d8053a6a5 100644 --- a/packages/metadata-protocol/src/protocol.ts +++ b/packages/metadata-protocol/src/protocol.ts @@ -16746,10 +16746,9 @@ export class ObjectStackProtocolImplementation implements * * [#21922] The layered read asks this predicate through * {@link declinesStoredRow}, which also declines the stored row of a - * code-defined datasource name. The published doors ask this predicate - * alone, so for such a name they still serve the stored row: the active - * overlay row, as the route's spec describes it. That door is not moved - * here. + * code-defined datasource name. [#21986] The published doors ask + * {@link declinesStoredRow} in its place, so for such a name they serve + * the code definition too. */ isShippedFlowName(type: string, name: unknown): boolean { if ((PLURAL_TO_SINGULAR[type] ?? type) !== 'flow') return false; @@ -16806,8 +16805,18 @@ export class ObjectStackProtocolImplementation implements * (`storedRowServed`, the fact {@link servedLockState}'s `deletable` * reads), the `_lock` gate's overlay layer ({@link overlayLockLayerAt}, * read whether or not the row is adopted), and the DELETE's own row probe. + * + * [#21986] PUBLIC so a door that serves a stored row out of the layered + * read can ASK it, never re-derive it: `GET /meta/:type/:name/published` + * (the REST route and its dispatcher twin) reads {@link getMetaItemLayered} + * and, when a stored row is present and this predicate holds for the + * answer's `type` and `name`, serves the `effective` layer (the loader's + * body, or the code definition) instead of the row. Every other stored row + * is served as before. A door asks this method alone: ⛔ no door restates + * either half, or the host's code-datasource set. A write door does not + * ask it; the writes keep their own refusals. */ - private declinesStoredRow(type: string, name: unknown): boolean { + declinesStoredRow(type: string, name: unknown): boolean { if (this.isShippedFlowName(type, name)) return true; return typeof name === 'string' && name !== '' && this.isDeclaredCodeDatasource(type, name); } diff --git a/packages/rest/src/meta-published-overlay.test.ts b/packages/rest/src/meta-published-overlay.test.ts index 5e03deb47af..0121e06c1af 100644 --- a/packages/rest/src/meta-published-overlay.test.ts +++ b/packages/rest/src/meta-published-overlay.test.ts @@ -586,11 +586,14 @@ describe('[#21002] the published door follows the layered read for a shipped flo it('control: a protocol that brings no such predicate keeps today\'s answer, the stored row', async () => { const { engine, metadata, protocol } = shippedHarness(); await storeActiveRow(engine, 'flow', SHIPPED, flowBody(SHIPPED, 'STORED')); - // The same protocol with the predicate hidden from the door only; its - // own methods keep calling it on the real instance. + // The same protocol with the predicate the door asks hidden from the + // door only; its own methods keep calling it on the real instance. + // [#21986] That predicate is `declinesStoredRow`. `isShippedFlowName` + // stays visible, so this also pins that the door does not fall back + // to it: a protocol without the one predicate gets the stored row. const withoutPredicate = new Proxy(protocol, { get(target, key) { - if (key === 'isShippedFlowName') return undefined; + if (key === 'declinesStoredRow') return undefined; const value = Reflect.get(target, key); return typeof value === 'function' ? value.bind(target) : value; }, @@ -602,3 +605,107 @@ describe('[#21002] the published door follows the layered read for a shipped flo expect(res.body).toMatchObject({ name: SHIPPED, label: 'STORED' }); }, 60_000); }); + +/** + * [#21986] The other name class whose stored row the layered read declines: a + * CODE-DEFINED DATASOURCE name (here one an installed package declares). The + * by-name read, the list and the layered read's effective layer serve the code + * definition, the MetadataService's registration, and the stored row is + * residue, still reported in `overlay`. This door asks the protocol's one + * predicate for both name classes, `declinesStoredRow`, with the answer's own + * `type` / `name`, so it serves that effective layer too. A runtime + * datasource's stored row is still served. + * + * The cold-boot reading over the real showcase composition is recorded on the + * PR; the predicate's two halves are pinned in `metadata-protocol`. + */ +describe('[#21986] the published door serves a code-defined datasource\'s code definition over a stored row', () => { + const CODE_DS = 'showcase_external'; + const RUNTIME_DS = 'rt_datasource_21986'; + const CODE_LABEL = 'External Analytics (SQLite)'; + const SHADOW_LABEL = 'Shadow 21986'; + const dsBody = (name: string, label: string, origin: 'code' | 'runtime', filename: string) => ({ + name, label, driver: 'sqlite', config: { filename }, origin, + }); + + /** + * The file's engine double, with an installed package that declares + * {@link CODE_DS}, and the MetadataService holding the code definition the + * runtime registers at boot (and a copy of the runtime datasource under a + * label its row does not carry, so the control can tell which layer answered). + */ + async function datasourceHarness() { + const { engine, rows } = makeStubEngine(); + engine.registry = { + registerItem: () => {}, + registerObject: () => {}, + getPackage: () => undefined, + getItem: () => undefined, + getAllPackages: () => [{ + manifest: { + id: 'com.example.showcase', + datasources: [{ name: CODE_DS, label: CODE_LABEL, driver: 'sqlite', config: { filename: 'x.db' } }], + }, + }], + }; + const metadata = new MetadataManager({}); + await metadata.register('datasource', CODE_DS, dsBody(CODE_DS, CODE_LABEL, 'code', `${CODE_DS}.db`)); + await metadata.register('datasource', RUNTIME_DS, dsBody(RUNTIME_DS, 'Runtime (MetadataService copy)', 'runtime', 'rt.db')); + const protocol = makeProtocol(engine, metadata); + return { engine, rows, metadata, protocol }; + } + + async function storeActiveRow(engine: any, name: string, body: unknown) { + await engine.insert('sys_metadata', { + type: 'datasource', name, organization_id: null, package_id: null, state: 'active', + metadata: JSON.stringify(body), checksum: 'sha256:stored', version: 1, + }); + } + + /** + * {@link setup}, read by a caller the `/meta` doors admit to a datasource + * read: `datasource` reads require `manage_platform_settings`, the + * capability the datasource admin door requires. + */ + function setupAsPlatformAdmin(protocol: unknown, metadata: unknown) { + const rest = setup(protocol, metadata); + (rest as any).resolveExecCtx = async () => ({ userId: 'u_platform', systemPermissions: ['manage_platform_settings'] }); + return rest; + } + + it('a stored row under a code-defined datasource name: the door answers the code definition, the layered effective layer', async () => { + const { engine, rows, metadata, protocol } = await datasourceHarness(); + await storeActiveRow(engine, CODE_DS, dsBody(CODE_DS, SHADOW_LABEL, 'runtime', 'shadow-external.db')); + expect(rows.size).toBe(1); + + // The decision this door follows: a stored layer IS present, and the + // layered read put the code definition over it. + const layered: any = await protocol.getMetaItemLayered({ type: 'datasource', name: CODE_DS }); + expect(layered.overlay).toMatchObject({ origin: 'runtime', label: SHADOW_LABEL }); + expect(layered.effective).toMatchObject({ origin: 'code', label: CODE_LABEL }); + + const rest = setupAsPlatformAdmin(protocol, metadata); + for (const type of ['datasource', 'datasources']) { + const res = await callPublished(rest, { type, name: CODE_DS }); + + expect(res.statusCode, type).toBe(200); + expect(res.body, type).toMatchObject({ name: CODE_DS, origin: 'code', label: CODE_LABEL }); + expect(JSON.stringify(res.body), type).not.toContain('shadow-external.db'); + expect(res.body, type).toEqual(layered.effective); + } + // The row stays at rest: the door read past it, nothing removed it. + expect(rows.size).toBe(1); + }, 60_000); + + it('control: a runtime datasource\'s stored row is still what the door serves', async () => { + const { engine, metadata, protocol } = await datasourceHarness(); + await storeActiveRow(engine, RUNTIME_DS, dsBody(RUNTIME_DS, 'Runtime (stored row)', 'runtime', 'rt.db')); + + const layered: any = await protocol.getMetaItemLayered({ type: 'datasource', name: RUNTIME_DS }); + const res = await callPublished(setupAsPlatformAdmin(protocol, metadata), { type: 'datasource', name: RUNTIME_DS }); + + expect(res.statusCode).toBe(200); + expect(res.body).toMatchObject({ name: RUNTIME_DS, label: 'Runtime (stored row)' }); + expect(res.body).toEqual(layered.overlay); + }, 60_000); +}); diff --git a/packages/rest/src/rest-server.ts b/packages/rest/src/rest-server.ts index b0076412c6f..bca1481694b 100644 --- a/packages/rest/src/rest-server.ts +++ b/packages/rest/src/rest-server.ts @@ -8621,24 +8621,30 @@ export class RestServer { : {}), }); if (layered?.overlay !== undefined && layered?.overlay !== null) { - // [#21002, ADR-0126 §2] When the layered - // read put the LOADER's body over this stored - // row — a shipped flow name, decided by the - // protocol's `isShippedFlowName` — this door - // serves that effective layer, not the row: - // `flow` is Regime C, "never an overlay read - // path". The predicate is ASKED of its owner - // with the answer's own `type` / `name`, - // never re-derived here, so this door and + // [#21002, #21986, ADR-0126 §2, ADR-0062 D4] + // When the layered read put a code layer + // over this stored row, this door serves + // that effective layer, not the row. The + // protocol decides it with one predicate, + // `declinesStoredRow`, for both name + // classes: a shipped flow name (the + // loader's body; `flow` is Regime C, + // "never an overlay read path") and a + // code-defined datasource name (the code + // definition; "code wins on collision"). + // The predicate is ASKED of its owner with + // the answer's own `type` / `name`, never + // re-derived here, so this door, the + // by-name read, the list and // `getMetaItemLayered` read one rule. Every // other stored row is served exactly as // before — an `object` too, whose effective // layer differs from its row by folding, not // by this decision — and so is every row of a // protocol that brings no such predicate. - const shippedFlow: { isShippedFlowName?(type: string, name: unknown): boolean } = publishedProtocol; - publishedOverlay = typeof shippedFlow.isShippedFlowName === 'function' - && shippedFlow.isShippedFlowName(layered.type, layered.name) + const decliner: { declinesStoredRow?(type: string, name: unknown): boolean } = publishedProtocol; + publishedOverlay = typeof decliner.declinesStoredRow === 'function' + && decliner.declinesStoredRow(layered.type, layered.name) ? layered.effective : layered.overlay; } diff --git a/packages/runtime/src/domains/meta-published-runtime-publish.test.ts b/packages/runtime/src/domains/meta-published-runtime-publish.test.ts index be231e603a6..18a5aaa93ca 100644 --- a/packages/runtime/src/domains/meta-published-runtime-publish.test.ts +++ b/packages/runtime/src/domains/meta-published-runtime-publish.test.ts @@ -440,11 +440,14 @@ describe('[#21002] the dispatcher published door follows the layered read for a it('control: a protocol that brings no such predicate keeps today\'s answer, the stored row', async () => { const { engine, metadata, protocol } = shippedHarness(); await storeActiveRow(engine, 'flow', SHIPPED, flowBody(SHIPPED, 'STORED')); - // The same protocol with the predicate hidden from the door only; its - // own methods keep calling it on the real instance. + // The same protocol with the predicate the door asks hidden from the + // door only; its own methods keep calling it on the real instance. + // [#21986] That predicate is `declinesStoredRow`. `isShippedFlowName` + // stays visible, so this also pins that the door does not fall back + // to it: a protocol without the one predicate gets the stored row. const withoutPredicate = new Proxy(protocol, { get(target, key) { - if (key === 'isShippedFlowName') return undefined; + if (key === 'declinesStoredRow') return undefined; const value = Reflect.get(target, key); return typeof value === 'function' ? value.bind(target) : value; }, @@ -457,3 +460,104 @@ describe('[#21002] the dispatcher published door follows the layered read for a expect(response.body.data).toMatchObject({ name: SHIPPED, label: 'STORED' }); }); }); + +/** + * [#21986] The dispatcher twin, for the other name class whose stored row the + * layered read declines: a CODE-DEFINED DATASOURCE name (here one an installed + * package declares). The layered read's effective layer for such a name is the + * code definition, the MetadataService's registration, and the stored row is + * residue, still reported in `overlay`. This door asks the protocol's one + * predicate for both name classes, `declinesStoredRow`, so it serves that + * effective layer, as `RestServer`'s `/published` does. A runtime datasource's + * stored row is still served. + */ +describe('[#21986] the dispatcher published door serves a code-defined datasource\'s code definition over a stored row', () => { + const CODE_DS = 'showcase_external'; + const RUNTIME_DS = 'rt_datasource_21986'; + const CODE_LABEL = 'External Analytics (SQLite)'; + const SHADOW_LABEL = 'Shadow 21986'; + const dsBody = (name: string, label: string, origin: 'code' | 'runtime', filename: string) => ({ + name, label, driver: 'sqlite', config: { filename }, origin, + }); + + /** + * This file's engine double, with an installed package that declares + * {@link CODE_DS}, and the MetadataService holding the code definition the + * runtime registers at boot (and a copy of the runtime datasource under a + * label its row does not carry, so the control can tell which layer answered). + */ + async function datasourceHarness() { + const { engine, rows } = makeStubEngine(); + engine.registry = { + registerItem: () => {}, + registerObject: () => {}, + getPackage: () => undefined, + getItem: () => undefined, + getAllPackages: () => [{ + manifest: { + id: 'com.example.showcase', + datasources: [{ name: CODE_DS, label: CODE_LABEL, driver: 'sqlite', config: { filename: 'x.db' } }], + }, + }], + }; + const metadata = new MetadataManager({}); + await metadata.register('datasource', CODE_DS, dsBody(CODE_DS, CODE_LABEL, 'code', `${CODE_DS}.db`)); + await metadata.register('datasource', RUNTIME_DS, dsBody(RUNTIME_DS, 'Runtime (MetadataService copy)', 'runtime', 'rt.db')); + const protocol = makeProtocol(engine, metadata); + return { engine, rows, metadata, protocol }; + } + + async function storeActiveRow(engine: any, name: string, body: unknown) { + await engine.insert('sys_metadata', { + type: 'datasource', name, organization_id: null, package_id: null, state: 'active', + metadata: JSON.stringify(body), checksum: 'sha256:stored', version: 1, + }); + } + + /** + * {@link ctx}, for a caller the `/meta` doors admit to a datasource read: + * `datasource` reads require `manage_platform_settings`, the capability + * the datasource admin door requires. + */ + const platformAdminCtx = (): any => ({ + ...ctx(), + executionContext: { userId: 'u_platform', systemPermissions: ['manage_platform_settings'] }, + }); + + it('a stored row under a code-defined datasource name: the door answers the code definition, the layered effective layer', async () => { + const { engine, rows, metadata, protocol } = await datasourceHarness(); + await storeActiveRow(engine, CODE_DS, dsBody(CODE_DS, SHADOW_LABEL, 'runtime', 'shadow-external.db')); + expect(rows.size).toBe(1); + + // The decision this door follows: a stored layer IS present, and the + // layered read put the code definition over it. + const layered: any = await protocol.getMetaItemLayered({ type: 'datasource', name: CODE_DS }); + expect(layered.overlay).toMatchObject({ origin: 'runtime', label: SHADOW_LABEL }); + expect(layered.effective).toMatchObject({ origin: 'code', label: CODE_LABEL }); + + const dispatcher = make({ protocol, metadata }); + for (const type of ['datasource', 'datasources']) { + const response = responseOf(await dispatcher.handleMetadata(`/${type}/${CODE_DS}/published`, platformAdminCtx(), 'GET')); + + expect(response.status, type).toBe(200); + expect(response.body.data, type).toMatchObject({ name: CODE_DS, origin: 'code', label: CODE_LABEL }); + expect(JSON.stringify(response.body.data), type).not.toContain('shadow-external.db'); + expect(response.body.data, type).toEqual(layered.effective); + } + // The row stays at rest: the door read past it, nothing removed it. + expect(rows.size).toBe(1); + }); + + it('control: a runtime datasource\'s stored row is still what the door serves', async () => { + const { engine, metadata, protocol } = await datasourceHarness(); + await storeActiveRow(engine, RUNTIME_DS, dsBody(RUNTIME_DS, 'Runtime (stored row)', 'runtime', 'rt.db')); + + const layered: any = await protocol.getMetaItemLayered({ type: 'datasource', name: RUNTIME_DS }); + const response = responseOf(await make({ protocol, metadata }) + .handleMetadata(`/datasource/${RUNTIME_DS}/published`, platformAdminCtx(), 'GET')); + + expect(response.status).toBe(200); + expect(response.body.data).toMatchObject({ name: RUNTIME_DS, label: 'Runtime (stored row)' }); + expect(response.body.data).toEqual(layered.overlay); + }); +}); diff --git a/packages/runtime/src/domains/meta.ts b/packages/runtime/src/domains/meta.ts index f3f2cb560cc..65882c0e5f0 100644 --- a/packages/runtime/src/domains/meta.ts +++ b/packages/runtime/src/domains/meta.ts @@ -43,8 +43,8 @@ import { // which is the whole reason `MetaDomainProtocol` below is `Pick`ed rather than // written out. Same move `domains/packages.ts` and `domains/mcp.ts` make. import type { MetadataProtocol } from '@objectstack/spec/api'; -// [#21002] The implementation class, for the ONE member it declares that this -// domain asks (`isShippedFlowName`) — `Pick`ed below, never restated. +// [#21002, #21986] The implementation class, for the ONE member it declares +// that this domain asks (`declinesStoredRow`) — `Pick`ed below, never restated. import type { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol'; // [#20193] THE per-caller read gate of a `/meta/:type/:name` document — the one // `RestServer` asks, published by `@objectstack/rest` so this transport asks it @@ -152,8 +152,9 @@ import type { DomainHandlerDeps, DomainRoute } from '../domain-handler-registry. * `any` requests — nothing declares its type, and the only thing that branch * asks of it is whether it is `undefined`. * - * [#21002] A third group: `isShippedFlowName`, the predicate the layered read - * decides its effective layer with, which `/published` asks so it follows that + * [#21002, #21986] A third group: `declinesStoredRow`, the one predicate the + * layered read decides its effective layer with (a shipped flow name, or a + * code-defined datasource name), which `/published` asks so it follows that * decision. Its signature is DECLARED — on `ObjectStackProtocolImplementation` * itself — so it is `Pick`ed from that class, never restated: a rename at the * producer is a compile error here, the same move `domains/automation.ts` @@ -162,7 +163,7 @@ import type { DomainHandlerDeps, DomainRoute } from '../domain-handler-registry. export type MetaDomainProtocol = Partial> - & Partial> + & Partial> & { /** ⚠️ Undeclared request shapes — see "Where the ledger honestly ends". */ listDrafts?(request: any): Promise; @@ -1179,17 +1180,20 @@ export async function handleMetadataRequest(deps: DomainHandlerDeps, path: strin ...(organizationId ? { organizationId } : {}), }); if (layered?.overlay !== undefined && layered?.overlay !== null) { - // [#21002, ADR-0126 §2] As `RestServer`'s `/published`: when - // the layered read put the LOADER's body over this stored - // row — a shipped flow name, decided by the protocol's - // `isShippedFlowName`, asked with the answer's own `type` / - // `name` and never re-derived here — serve that effective - // layer, not the row (`flow` is Regime C, "never an overlay - // read path"). Every other stored row, an `object`'s - // included, and every row of a protocol without the - // predicate, is served exactly as before. - publishedOverlay = typeof protocol.isShippedFlowName === 'function' - && protocol.isShippedFlowName(layered.type, layered.name) + // [#21002, #21986, ADR-0126 §2, ADR-0062 D4] As + // `RestServer`'s `/published`: when the layered read put a + // code layer over this stored row — a shipped flow name (the + // loader's body; `flow` is Regime C, "never an overlay read + // path") or a code-defined datasource name (the code + // definition; "code wins on collision"), decided by the + // protocol's one predicate, `declinesStoredRow`, asked with + // the answer's own `type` / `name` and never re-derived + // here — serve that effective layer, not the row. Every + // other stored row, an `object`'s included, and every row of + // a protocol without the predicate, is served exactly as + // before. + publishedOverlay = typeof protocol.declinesStoredRow === 'function' + && protocol.declinesStoredRow(layered.type, layered.name) ? layered.effective : layered.overlay; }