Skip to content

Commit 7a908c1

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-22013-osv-sharp-shell-quote
Re-run CI on a fresh head after Type Check · source gates was cancelled at its timeout during checkout. main brought no dependency file change. Claude-Session: https://claude.ai/code/session_01VF48aw8RPG6wzDnMgp6rtw Co-authored-by: Claude <noreply@anthropic.com>
2 parents d263b70 + 1fb274e commit 7a908c1

9 files changed

Lines changed: 387 additions & 39 deletions
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@objectstack/metadata-protocol": minor
3+
---
4+
5+
`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
6+
7+
Clause-②: yes (widening)
8+
9+
- 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`.
10+
- 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.
11+
- `isShippedFlowName` stays public and unchanged.
12+
- 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.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@objectstack/rest": patch
3+
---
4+
5+
`GET /api/v1/meta/datasource/:name/published` serves a code-defined datasource's code definition while a stored row under its name still exists
6+
7+
Clause-②: no
8+
9+
- 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.
10+
- 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.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@objectstack/runtime": patch
3+
---
4+
5+
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
6+
7+
Clause-②: no
8+
9+
- 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.
10+
- 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`.
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#21986] `declinesStoredRow` is PUBLIC on `ObjectStackProtocolImplementation`,
5+
* so the published-snapshot doors (`GET /meta/:type/:name/published`, the REST
6+
* route and its dispatcher twin) can ask it instead of `isShippedFlowName`.
7+
*
8+
* What a door relies on, pinned here once: the one predicate answers BOTH name
9+
* classes whose stored row the active reads decline, and nothing else.
10+
*
11+
* - a FLOW name the loader's set holds (`isShippedFlowName`'s answer);
12+
* - a CODE-DEFINED DATASOURCE name: one an installed package declares, or one
13+
* the host registers from code (`code-datasource-names`, here `default`).
14+
*
15+
* Every other name answers false: a flow no package ships, a runtime
16+
* datasource, the same name under another type, a missing or empty name.
17+
*
18+
* The calls below go through the class's own declared type, never an `any`
19+
* cast, so `tsc --noEmit` over this file (the package's `typecheck` includes
20+
* `src/**`) fails if the member stops being public.
21+
*/
22+
import { describe, expect, it } from 'vitest';
23+
import { ObjectStackProtocolImplementation } from './protocol.js';
24+
25+
const FLOW_PACKAGE = 'com.example.pkg';
26+
const SHIPPED_FLOW = 'pkg_flow';
27+
const CUSTOMER_FLOW = 'customer_flow';
28+
const CODE_DS = 'showcase_external';
29+
const HOST_DS = 'default';
30+
const RUNTIME_DS = 'rt_datasource_21986';
31+
32+
/**
33+
* A partial registry: the loader's entry for {@link SHIPPED_FLOW} carries its
34+
* package id (the shape `lookupArtifactItem` reads off a registry with no
35+
* `getArtifactItem`), and one installed package declares {@link CODE_DS}. The
36+
* services registry carries the host's code-datasource set.
37+
*/
38+
function makeProtocol(): ObjectStackProtocolImplementation {
39+
const loaderEntry = { name: SHIPPED_FLOW, label: 'Loader', _packageId: FLOW_PACKAGE };
40+
const engine = {
41+
registry: {
42+
getItem: (type: string, name: string) =>
43+
(type === 'flow' || type === 'flows') && name === SHIPPED_FLOW ? loaderEntry : undefined,
44+
getAllPackages: () => [{
45+
manifest: { id: 'com.example.showcase', datasources: [{ name: CODE_DS, driver: 'sqlite', config: {} }] },
46+
}],
47+
},
48+
};
49+
const services = new Map<string, unknown>([['code-datasource-names', new Set([HOST_DS])]]);
50+
return new ObjectStackProtocolImplementation(engine as never, () => services);
51+
}
52+
53+
describe('[#21986] the published predicate: declinesStoredRow answers both name classes, and only those', () => {
54+
it('a shipped flow name, in either type spelling — what isShippedFlowName answers', () => {
55+
const protocol = makeProtocol();
56+
for (const type of ['flow', 'flows']) {
57+
expect(protocol.isShippedFlowName(type, SHIPPED_FLOW), type).toBe(true);
58+
expect(protocol.declinesStoredRow(type, SHIPPED_FLOW), type).toBe(true);
59+
}
60+
});
61+
62+
it('a code-defined datasource name: one a package declares, and one the host registers from code', () => {
63+
const protocol = makeProtocol();
64+
for (const type of ['datasource', 'datasources']) {
65+
expect(protocol.declinesStoredRow(type, CODE_DS), type).toBe(true);
66+
expect(protocol.declinesStoredRow(type, HOST_DS), type).toBe(true);
67+
// Not a flow answer: the datasource half is its own.
68+
expect(protocol.isShippedFlowName(type, CODE_DS), type).toBe(false);
69+
}
70+
});
71+
72+
it('control: every other name keeps its stored row', () => {
73+
const protocol = makeProtocol();
74+
expect(protocol.declinesStoredRow('flow', CUSTOMER_FLOW)).toBe(false);
75+
expect(protocol.declinesStoredRow('datasource', RUNTIME_DS)).toBe(false);
76+
// The same names under another type.
77+
expect(protocol.declinesStoredRow('object', CODE_DS)).toBe(false);
78+
expect(protocol.declinesStoredRow('object', HOST_DS)).toBe(false);
79+
expect(protocol.declinesStoredRow('view', SHIPPED_FLOW)).toBe(false);
80+
// A name that is not one.
81+
for (const name of [undefined, null, '', 42]) {
82+
expect(protocol.declinesStoredRow('datasource', name), String(name)).toBe(false);
83+
expect(protocol.declinesStoredRow('flow', name), String(name)).toBe(false);
84+
}
85+
});
86+
});

‎packages/metadata-protocol/src/protocol.ts‎

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16746,10 +16746,9 @@ export class ObjectStackProtocolImplementation implements
1674616746
*
1674716747
* [#21922] The layered read asks this predicate through
1674816748
* {@link declinesStoredRow}, which also declines the stored row of a
16749-
* code-defined datasource name. The published doors ask this predicate
16750-
* alone, so for such a name they still serve the stored row: the active
16751-
* overlay row, as the route's spec describes it. That door is not moved
16752-
* here.
16749+
* code-defined datasource name. [#21986] The published doors ask
16750+
* {@link declinesStoredRow} in its place, so for such a name they serve
16751+
* the code definition too.
1675316752
*/
1675416753
isShippedFlowName(type: string, name: unknown): boolean {
1675516754
if ((PLURAL_TO_SINGULAR[type] ?? type) !== 'flow') return false;
@@ -16806,8 +16805,18 @@ export class ObjectStackProtocolImplementation implements
1680616805
* (`storedRowServed`, the fact {@link servedLockState}'s `deletable`
1680716806
* reads), the `_lock` gate's overlay layer ({@link overlayLockLayerAt},
1680816807
* read whether or not the row is adopted), and the DELETE's own row probe.
16808+
*
16809+
* [#21986] PUBLIC so a door that serves a stored row out of the layered
16810+
* read can ASK it, never re-derive it: `GET /meta/:type/:name/published`
16811+
* (the REST route and its dispatcher twin) reads {@link getMetaItemLayered}
16812+
* and, when a stored row is present and this predicate holds for the
16813+
* answer's `type` and `name`, serves the `effective` layer (the loader's
16814+
* body, or the code definition) instead of the row. Every other stored row
16815+
* is served as before. A door asks this method alone: ⛔ no door restates
16816+
* either half, or the host's code-datasource set. A write door does not
16817+
* ask it; the writes keep their own refusals.
1680916818
*/
16810-
private declinesStoredRow(type: string, name: unknown): boolean {
16819+
declinesStoredRow(type: string, name: unknown): boolean {
1681116820
if (this.isShippedFlowName(type, name)) return true;
1681216821
return typeof name === 'string' && name !== '' && this.isDeclaredCodeDatasource(type, name);
1681316822
}

‎packages/rest/src/meta-published-overlay.test.ts‎

Lines changed: 110 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -586,11 +586,14 @@ describe('[#21002] the published door follows the layered read for a shipped flo
586586
it('control: a protocol that brings no such predicate keeps today\'s answer, the stored row', async () => {
587587
const { engine, metadata, protocol } = shippedHarness();
588588
await storeActiveRow(engine, 'flow', SHIPPED, flowBody(SHIPPED, 'STORED'));
589-
// The same protocol with the predicate hidden from the door only; its
590-
// own methods keep calling it on the real instance.
589+
// The same protocol with the predicate the door asks hidden from the
590+
// door only; its own methods keep calling it on the real instance.
591+
// [#21986] That predicate is `declinesStoredRow`. `isShippedFlowName`
592+
// stays visible, so this also pins that the door does not fall back
593+
// to it: a protocol without the one predicate gets the stored row.
591594
const withoutPredicate = new Proxy(protocol, {
592595
get(target, key) {
593-
if (key === 'isShippedFlowName') return undefined;
596+
if (key === 'declinesStoredRow') return undefined;
594597
const value = Reflect.get(target, key);
595598
return typeof value === 'function' ? value.bind(target) : value;
596599
},
@@ -602,3 +605,107 @@ describe('[#21002] the published door follows the layered read for a shipped flo
602605
expect(res.body).toMatchObject({ name: SHIPPED, label: 'STORED' });
603606
}, 60_000);
604607
});
608+
609+
/**
610+
* [#21986] The other name class whose stored row the layered read declines: a
611+
* CODE-DEFINED DATASOURCE name (here one an installed package declares). The
612+
* by-name read, the list and the layered read's effective layer serve the code
613+
* definition, the MetadataService's registration, and the stored row is
614+
* residue, still reported in `overlay`. This door asks the protocol's one
615+
* predicate for both name classes, `declinesStoredRow`, with the answer's own
616+
* `type` / `name`, so it serves that effective layer too. A runtime
617+
* datasource's stored row is still served.
618+
*
619+
* The cold-boot reading over the real showcase composition is recorded on the
620+
* PR; the predicate's two halves are pinned in `metadata-protocol`.
621+
*/
622+
describe('[#21986] the published door serves a code-defined datasource\'s code definition over a stored row', () => {
623+
const CODE_DS = 'showcase_external';
624+
const RUNTIME_DS = 'rt_datasource_21986';
625+
const CODE_LABEL = 'External Analytics (SQLite)';
626+
const SHADOW_LABEL = 'Shadow 21986';
627+
const dsBody = (name: string, label: string, origin: 'code' | 'runtime', filename: string) => ({
628+
name, label, driver: 'sqlite', config: { filename }, origin,
629+
});
630+
631+
/**
632+
* The file's engine double, with an installed package that declares
633+
* {@link CODE_DS}, and the MetadataService holding the code definition the
634+
* runtime registers at boot (and a copy of the runtime datasource under a
635+
* label its row does not carry, so the control can tell which layer answered).
636+
*/
637+
async function datasourceHarness() {
638+
const { engine, rows } = makeStubEngine();
639+
engine.registry = {
640+
registerItem: () => {},
641+
registerObject: () => {},
642+
getPackage: () => undefined,
643+
getItem: () => undefined,
644+
getAllPackages: () => [{
645+
manifest: {
646+
id: 'com.example.showcase',
647+
datasources: [{ name: CODE_DS, label: CODE_LABEL, driver: 'sqlite', config: { filename: 'x.db' } }],
648+
},
649+
}],
650+
};
651+
const metadata = new MetadataManager({});
652+
await metadata.register('datasource', CODE_DS, dsBody(CODE_DS, CODE_LABEL, 'code', `${CODE_DS}.db`));
653+
await metadata.register('datasource', RUNTIME_DS, dsBody(RUNTIME_DS, 'Runtime (MetadataService copy)', 'runtime', 'rt.db'));
654+
const protocol = makeProtocol(engine, metadata);
655+
return { engine, rows, metadata, protocol };
656+
}
657+
658+
async function storeActiveRow(engine: any, name: string, body: unknown) {
659+
await engine.insert('sys_metadata', {
660+
type: 'datasource', name, organization_id: null, package_id: null, state: 'active',
661+
metadata: JSON.stringify(body), checksum: 'sha256:stored', version: 1,
662+
});
663+
}
664+
665+
/**
666+
* {@link setup}, read by a caller the `/meta` doors admit to a datasource
667+
* read: `datasource` reads require `manage_platform_settings`, the
668+
* capability the datasource admin door requires.
669+
*/
670+
function setupAsPlatformAdmin(protocol: unknown, metadata: unknown) {
671+
const rest = setup(protocol, metadata);
672+
(rest as any).resolveExecCtx = async () => ({ userId: 'u_platform', systemPermissions: ['manage_platform_settings'] });
673+
return rest;
674+
}
675+
676+
it('a stored row under a code-defined datasource name: the door answers the code definition, the layered effective layer', async () => {
677+
const { engine, rows, metadata, protocol } = await datasourceHarness();
678+
await storeActiveRow(engine, CODE_DS, dsBody(CODE_DS, SHADOW_LABEL, 'runtime', 'shadow-external.db'));
679+
expect(rows.size).toBe(1);
680+
681+
// The decision this door follows: a stored layer IS present, and the
682+
// layered read put the code definition over it.
683+
const layered: any = await protocol.getMetaItemLayered({ type: 'datasource', name: CODE_DS });
684+
expect(layered.overlay).toMatchObject({ origin: 'runtime', label: SHADOW_LABEL });
685+
expect(layered.effective).toMatchObject({ origin: 'code', label: CODE_LABEL });
686+
687+
const rest = setupAsPlatformAdmin(protocol, metadata);
688+
for (const type of ['datasource', 'datasources']) {
689+
const res = await callPublished(rest, { type, name: CODE_DS });
690+
691+
expect(res.statusCode, type).toBe(200);
692+
expect(res.body, type).toMatchObject({ name: CODE_DS, origin: 'code', label: CODE_LABEL });
693+
expect(JSON.stringify(res.body), type).not.toContain('shadow-external.db');
694+
expect(res.body, type).toEqual(layered.effective);
695+
}
696+
// The row stays at rest: the door read past it, nothing removed it.
697+
expect(rows.size).toBe(1);
698+
}, 60_000);
699+
700+
it('control: a runtime datasource\'s stored row is still what the door serves', async () => {
701+
const { engine, metadata, protocol } = await datasourceHarness();
702+
await storeActiveRow(engine, RUNTIME_DS, dsBody(RUNTIME_DS, 'Runtime (stored row)', 'runtime', 'rt.db'));
703+
704+
const layered: any = await protocol.getMetaItemLayered({ type: 'datasource', name: RUNTIME_DS });
705+
const res = await callPublished(setupAsPlatformAdmin(protocol, metadata), { type: 'datasource', name: RUNTIME_DS });
706+
707+
expect(res.statusCode).toBe(200);
708+
expect(res.body).toMatchObject({ name: RUNTIME_DS, label: 'Runtime (stored row)' });
709+
expect(res.body).toEqual(layered.overlay);
710+
}, 60_000);
711+
});

‎packages/rest/src/rest-server.ts‎

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8621,24 +8621,30 @@ export class RestServer {
86218621
: {}),
86228622
});
86238623
if (layered?.overlay !== undefined && layered?.overlay !== null) {
8624-
// [#21002, ADR-0126 §2] When the layered
8625-
// read put the LOADER's body over this stored
8626-
// row — a shipped flow name, decided by the
8627-
// protocol's `isShippedFlowName` — this door
8628-
// serves that effective layer, not the row:
8629-
// `flow` is Regime C, "never an overlay read
8630-
// path". The predicate is ASKED of its owner
8631-
// with the answer's own `type` / `name`,
8632-
// never re-derived here, so this door and
8624+
// [#21002, #21986, ADR-0126 §2, ADR-0062 D4]
8625+
// When the layered read put a code layer
8626+
// over this stored row, this door serves
8627+
// that effective layer, not the row. The
8628+
// protocol decides it with one predicate,
8629+
// `declinesStoredRow`, for both name
8630+
// classes: a shipped flow name (the
8631+
// loader's body; `flow` is Regime C,
8632+
// "never an overlay read path") and a
8633+
// code-defined datasource name (the code
8634+
// definition; "code wins on collision").
8635+
// The predicate is ASKED of its owner with
8636+
// the answer's own `type` / `name`, never
8637+
// re-derived here, so this door, the
8638+
// by-name read, the list and
86338639
// `getMetaItemLayered` read one rule. Every
86348640
// other stored row is served exactly as
86358641
// before — an `object` too, whose effective
86368642
// layer differs from its row by folding, not
86378643
// by this decision — and so is every row of a
86388644
// protocol that brings no such predicate.
8639-
const shippedFlow: { isShippedFlowName?(type: string, name: unknown): boolean } = publishedProtocol;
8640-
publishedOverlay = typeof shippedFlow.isShippedFlowName === 'function'
8641-
&& shippedFlow.isShippedFlowName(layered.type, layered.name)
8645+
const decliner: { declinesStoredRow?(type: string, name: unknown): boolean } = publishedProtocol;
8646+
publishedOverlay = typeof decliner.declinesStoredRow === 'function'
8647+
&& decliner.declinesStoredRow(layered.type, layered.name)
86428648
? layered.effective
86438649
: layered.overlay;
86448650
}

0 commit comments

Comments
 (0)