From fccd98aea076d1e229d35b1fc6c16acff96ddf30 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 14:31:53 +0000 Subject: [PATCH 1/2] fix(service-datasource): the datasource-admin plugin reads environment rows only (ADR-0131 D6) Every read of a stored datasource row in DatasourceAdminServicePlugin -- the boot restore, the by-name read behind convergence and the /meta projection, and the existence probes behind its own writes -- goes through one find filtered by isLegacyOrganizationRow, now exported from @objectstack/metadata-core; objectql's binders import the same predicate. Claude-Session: https://claude.ai/code/session_01JfJfBUC3cQ6hhgm9MQK76T Co-authored-by: Claude --- ...22835-datasource-reads-environment-only.md | 27 +++ packages/metadata-core/src/index.ts | 5 + .../src/legacy-organization-row.test.ts | 16 ++ .../src/legacy-organization-row.ts | 19 ++ packages/objectql/src/plugin.ts | 15 +- ...e-restore-environment-rows.dogfood.test.ts | 182 ++++++++++++++++++ .../__tests__/datasource-admin-plugin.test.ts | 56 ++++++ .../datasource-system-context.pin.test.ts | 5 +- .../src/datasource-admin-plugin.ts | 56 +++--- 9 files changed, 343 insertions(+), 38 deletions(-) create mode 100644 .changeset/22835-datasource-reads-environment-only.md create mode 100644 packages/metadata-core/src/legacy-organization-row.test.ts create mode 100644 packages/metadata-core/src/legacy-organization-row.ts create mode 100644 packages/qa/dogfood/test/datasource-restore-environment-rows.dogfood.test.ts diff --git a/.changeset/22835-datasource-reads-environment-only.md b/.changeset/22835-datasource-reads-environment-only.md new file mode 100644 index 00000000000..c6cbd4f35a2 --- /dev/null +++ b/.changeset/22835-datasource-reads-environment-only.md @@ -0,0 +1,27 @@ +--- +'@objectstack/service-datasource': minor +'@objectstack/metadata-core': minor +'@objectstack/objectql': patch +--- + +feat(service-datasource,metadata-core)!: the datasource-admin plugin reads the environment's stored `datasource` rows only; a legacy organization-scoped row is neither loaded at boot nor served (ADR-0131 D6) + +Clause-②: no (narrowing) + + + +**BREAKING (narrowing).** Graded `minor` on the v18 prerelease line, as the read narrowing it completes. + +**What changes.** `DatasourceAdminServicePlugin` reads stored `datasource` rows in five places — the boot restore (`restoreRuntimeDatasources`), the by-name read behind cluster convergence and the `/meta` door's datasource projection, and the existence probes behind its own create, update and remove. Each now reads the environment's rows only, judged by the same row-value test the metadata reads apply. A row stored organization-scoped by an earlier release: + +- is no longer registered at boot, so the datasource-admin door and the metadata read door no longer serve it, and no live pool is opened from it; +- is never updated or deleted by an admin create, update or remove under its name — those write and remove an environment row of their own; +- stays at rest, and the boot report `[metadata_org_scoped_unserved]` keeps naming it. Nothing is deleted or rewritten; the v18 migration ceremony (`os migrate`, ADR-0131 D10) carries it. + +**What moves for consumers.** + +| From | To | +|:--|:--| +| a legacy organization-scoped runtime datasource a deployment relied on | re-create it through the datasource-admin door or save it through the `/meta` door to make it environment-wide now, or wait for the v18 migration ceremony | + +`@objectstack/metadata-core` exports `isLegacyOrganizationRow`, the one row-value test for such a row; `@objectstack/objectql`'s authored hook and action binders now read it from there, with no change in what they bind. diff --git a/packages/metadata-core/src/index.ts b/packages/metadata-core/src/index.ts index 4b2f072d15b..6499ce03cb1 100644 --- a/packages/metadata-core/src/index.ts +++ b/packages/metadata-core/src/index.ts @@ -114,6 +114,11 @@ export * from './meta-write-capability.js'; // surface is unchanged. export * from './record-organization.js'; +// [ADR-0131 D6] The one row-value test for a `sys_metadata` row stored in a +// legacy organization's layer — read by `@objectstack/objectql`'s binders and +// `@objectstack/service-datasource`'s runtime-datasource reads. +export * from './legacy-organization-row.js'; + // [#16319] The field-`type` admission vocabulary — the predicate, the shared // sentence and the cross-package discriminator behind the ruled 「禁止加载」 // refusal. Sunk here by the same criterion as everything above: the enforcing diff --git a/packages/metadata-core/src/legacy-organization-row.test.ts b/packages/metadata-core/src/legacy-organization-row.test.ts new file mode 100644 index 00000000000..020754baa47 --- /dev/null +++ b/packages/metadata-core/src/legacy-organization-row.test.ts @@ -0,0 +1,16 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import { describe, it, expect } from 'vitest'; +import { isLegacyOrganizationRow } from './legacy-organization-row.js'; + +describe('[ADR-0131 D6] isLegacyOrganizationRow — the one row-value test', () => { + it('a row whose organization_id names an organization is a legacy organization row', () => { + expect(isLegacyOrganizationRow({ organization_id: 'org_a' })).toBe(true); + }); + + it('an absent, null or empty organization_id is the environment\'s row', () => { + for (const row of [{}, { organization_id: undefined }, { organization_id: null }, { organization_id: '' }, null, undefined]) { + expect(isLegacyOrganizationRow(row), JSON.stringify(row)).toBe(false); + } + }); +}); diff --git a/packages/metadata-core/src/legacy-organization-row.ts b/packages/metadata-core/src/legacy-organization-row.ts new file mode 100644 index 00000000000..1429f694a23 --- /dev/null +++ b/packages/metadata-core/src/legacy-organization-row.ts @@ -0,0 +1,19 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * [ADR-0131 D6] A `sys_metadata` row stored in a legacy organization's layer: + * its `organization_id` names an organization (neither absent, `null` nor the + * empty string). No metadata read serves one and no boot loads one; the boot + * report names them, and nothing deletes or rewrites them. + * + * Judged off the row in hand — `sys_metadata` is a platform table, never a + * federated object. Sunk here because its readers outside the metadata door — + * `@objectstack/objectql`'s authored hook and action binders, and + * `@objectstack/service-datasource`'s runtime-datasource reads — share no other + * common home, and a second spelling would let them disagree on which rows are + * the environment's. + */ +export function isLegacyOrganizationRow(row: { organization_id?: unknown } | null | undefined): boolean { + const organizationId = row?.organization_id; + return organizationId !== null && organizationId !== undefined && organizationId !== ''; +} diff --git a/packages/objectql/src/plugin.ts b/packages/objectql/src/plugin.ts index ec01ca7bb0b..4b445e3a1d8 100644 --- a/packages/objectql/src/plugin.ts +++ b/packages/objectql/src/plugin.ts @@ -9,6 +9,10 @@ import { resolveArtifactPackageOrder, artifactPackageId, readDeploymentOrgScopin // multi-package stack no body declares, and the id they take. See // `unclaimedTopLevelObjects` below. import { unclaimedTopLevel } from '@objectstack/metadata'; +// [ADR-0131 D6] The one row-value test for a legacy organization's +// `sys_metadata` row: no metadata read serves one, so the authored hook and +// action binders skip it too. +import { isLegacyOrganizationRow } from '@objectstack/metadata-core'; import { applyConversionsToStoredItem } from '@objectstack/spec'; import { StorageNameMapping } from '@objectstack/spec/system'; import { applyProtection } from '@objectstack/spec/shared'; @@ -116,17 +120,6 @@ function unclaimedTopLevelObjects( : undefined; } -/** - * [ADR-0131 D6] A `sys_metadata` row stored in a legacy organization's layer: - * no metadata read serves one, so the authored hook and action binders skip it - * too. Judged off the row in hand — `sys_metadata` is a platform table, never a - * federated object. - */ -function isLegacyOrganizationRow(row: { organization_id?: unknown } | null | undefined): boolean { - const organizationId = row?.organization_id; - return organizationId !== null && organizationId !== undefined && organizationId !== ''; -} - /** * Options for ObjectQLPlugin. * diff --git a/packages/qa/dogfood/test/datasource-restore-environment-rows.dogfood.test.ts b/packages/qa/dogfood/test/datasource-restore-environment-rows.dogfood.test.ts new file mode 100644 index 00000000000..43351bcd62b --- /dev/null +++ b/packages/qa/dogfood/test/datasource-restore-environment-rows.dogfood.test.ts @@ -0,0 +1,182 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// +// [#22835 · ADR-0131 D6] The datasource-admin plugin reads ENVIRONMENT rows +// only — over the real showcase composition, across a restart. +// +// ## What was broken +// +// Every metadata read serves the environment's `sys_metadata` rows only, and +// the boot report `[metadata_org_scoped_unserved]` names a row stored in a +// legacy organization's layer as served by no read and loaded by no boot. The +// datasource-admin plugin's own reads of stored `datasource` rows carried no +// such predicate: its boot restore registered a legacy organization-scoped +// row, and from there both the datasource-admin door and the metadata read +// door served it, and a live pool was opened from it. +// +// ## What each case pins +// +// - after a restart over a legacy organization-scoped row and an environment +// row, neither door serves the legacy row (`404`, absent from both lists) +// and no pool is opened from it; the environment row is served as before; +// - the boot report still names the legacy row, and the row is kept as it +// was — neither deleted nor rewritten; +// - the plugin's by-name reads behind its writes never reach the legacy row: +// an admin create and remove under its name write and remove an +// environment row of their own, and a `/meta` save under its name is what +// the admin door then serves. +// +// The verify harness composes the datasource-admin service but not its REST +// routes, so this file mounts `registerDatasourceAdminRoutes` the way +// `packages/cli/src/commands/serve.ts` does. + +import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'; +import { registerDatasourceAdminRoutes } from '@objectstack/service-datasource'; +import { type VerifyStack } from '@objectstack/verify'; +import { bootShowcase } from './showcase-boot.js'; +import { mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +/** A fixture datasource stored in a legacy organization's layer. */ +const LEGACY = 'dogfood_legacy_ds_22835'; +const LEGACY_ORG = 'org_dogfood_legacy_22835'; +const LEGACY_LABEL = 'Legacy organization row 22835'; +/** A fixture datasource stored in the environment's layer: the control. */ +const ENV = 'dogfood_env_ds_22835'; + +const SYS = { isSystem: true, positions: [], permissions: [] }; + +type Row = Record; +interface Answer { status: number; json: Record } + +describe('[#22835] the datasource-admin plugin reads environment rows only (showcase, restart)', () => { + let stack: VerifyStack; + let token: string; + let dir: string; + /** Every `console.warn` line the measured boot printed. */ + let bootWarnings: string[] = []; + /** The legacy row as it was stored, before the measured boot. */ + let plantedLegacy: Row | undefined; + + const routes = () => ({ + name: 'dogfood.datasource-admin-routes', + version: '1.0.0', + optionalDependencies: ['com.objectstack.server.hono'], + init: async (ctx: { getService?: (name: string) => unknown }) => { + const httpServer = ctx.getService?.('http.server') ?? ctx.getService?.('http-server'); + registerDatasourceAdminRoutes(httpServer as never, ctx as never, '/api/v1'); + }, + }); + const boot = async () => { + stack = await bootShowcase({ databaseFile: join(dir, 'showcase.db'), extraPlugins: [routes()] }); + token = await stack.signIn(); + }; + const ql = () => stack.kernel.getService('objectql') as { + find(object: string, query: unknown): Promise; + insert(object: string, row: Row, options: unknown): Promise; + }; + const storedRows = (name: string) => ql().find('sys_metadata', { where: { type: 'datasource', name }, context: SYS }); + const call = async (method: string, path: string, body?: unknown): Promise => { + const res = await stack.apiAs(token, method, path, body); + return { status: res.status, json: (await res.json().catch(() => ({}))) as Record }; + }; + const adminNames = async () => { + const listed = await call('GET', '/datasources'); + expect(listed.status, JSON.stringify(listed.json)).toBe(200); + return (listed.json.data?.datasources ?? []).map((d: Row) => d.name); + }; + const metaNames = async () => { + const listed = await call('GET', '/meta/datasource'); + expect(listed.status, JSON.stringify(listed.json)).toBe(200); + const data = listed.json.data ?? listed.json; + return ((Array.isArray(data) ? data : data?.items) ?? []).map((d: Row) => d?.name); + }; + const pooled = (name: string) => { + const connection = stack.kernel.getService('datasource-connection') as { + listConnectionStates(): Array<{ name: string; status: string }>; + }; + return connection.listConnectionStates().some((s) => s.name === name); + }; + const legacyRows = async () => (await storedRows(LEGACY)).filter((r) => r.organization_id === LEGACY_ORG); + const environmentRows = async (name: string) => (await storedRows(name)).filter((r) => r.organization_id == null); + + beforeAll(async () => { + dir = mkdtempSync(join(tmpdir(), 'dogfood-22835-')); + await boot(); + // The rows an earlier release left, written with system context the way + // that release's store held them: one in a legacy organization's layer, + // one in the environment's. + const now = new Date().toISOString(); + for (const [name, organizationId, label] of [ + [LEGACY, LEGACY_ORG, LEGACY_LABEL], + [ENV, null, 'Environment row 22835'], + ] as const) { + await ql().insert('sys_metadata', { + type: 'datasource', name, organization_id: organizationId, package_id: null, scope: 'platform', + state: 'active', version: 1, checksum: null, created_at: now, updated_at: now, + metadata: JSON.stringify({ name, label, driver: 'sqlite', config: { filename: join(dir, `${name}.db`) } }), + }, { context: SYS }); + } + [plantedLegacy] = await legacyRows(); + await stack.stop(); + + const warn = vi.spyOn(console, 'warn'); + try { + await boot(); + } finally { + bootWarnings = warn.mock.calls.map((args) => args.map(String).join(' ')); + warn.mockRestore(); + } + }, 240_000); + + afterAll(async () => { + await stack?.stop(); + if (dir) rmSync(dir, { recursive: true, force: true }); + }); + + it('premise: both rows are at rest, and the boot report names the legacy one', async () => { + expect(plantedLegacy).toBeDefined(); + expect(await environmentRows(ENV)).toHaveLength(1); + const report = bootWarnings.filter((line) => line.includes('[metadata_org_scoped_unserved]')); + expect(report).toHaveLength(1); + expect(report[0]).toContain(`${LEGACY}@${LEGACY_ORG}`); + }); + + it('neither door serves the legacy row, and no pool is opened from it; the environment row is served', async () => { + expect((await call('GET', `/datasources/${LEGACY}`)).status).toBe(404); + expect((await call('GET', `/meta/datasource/${LEGACY}`)).status).toBe(404); + expect(await adminNames()).not.toContain(LEGACY); + expect(await metaNames()).not.toContain(LEGACY); + expect(pooled(LEGACY)).toBe(false); + + expect((await call('GET', `/datasources/${ENV}`)).status).toBe(200); + expect((await call('GET', `/meta/datasource/${ENV}`)).status).toBe(200); + expect(await adminNames()).toContain(ENV); + expect(await metaNames()).toContain(ENV); + expect(pooled(ENV)).toBe(true); + }); + + it('the writes\' by-name reads never reach the legacy row, and the row is kept as it was', async () => { + // An admin create under the legacy row's name writes an environment row of + // its own; its remove takes that row away again. + const created = await call('POST', '/datasources', { + name: LEGACY, label: 'Admin create 22835', driver: 'sqlite', config: { filename: join(dir, 'admin.db') }, + }); + expect(created.status, JSON.stringify(created.json)).toBe(201); + expect(await environmentRows(LEGACY)).toHaveLength(1); + expect((await call('DELETE', `/datasources/${LEGACY}`)).status).toBe(204); + expect(await environmentRows(LEGACY)).toHaveLength(0); + + // A `/meta` save under the name: the admin door serves the saved body. + const saved = await call('PUT', `/meta/datasource/${LEGACY}`, { + name: LEGACY, label: 'Meta save 22835', driver: 'sqlite', config: { filename: join(dir, 'meta.db') }, + }); + expect(saved.status, JSON.stringify(saved.json)).toBe(200); + const served = await call('GET', `/datasources/${LEGACY}`); + expect(served.status, JSON.stringify(served.json)).toBe(200); + expect(served.json.data?.datasource?.label).toBe('Meta save 22835'); + + // Neither deleted nor rewritten. + expect(await legacyRows()).toEqual([plantedLegacy]); + }); +}); diff --git a/packages/services/service-datasource/src/__tests__/datasource-admin-plugin.test.ts b/packages/services/service-datasource/src/__tests__/datasource-admin-plugin.test.ts index 6d1e2801519..d71ac9bd261 100644 --- a/packages/services/service-datasource/src/__tests__/datasource-admin-plugin.test.ts +++ b/packages/services/service-datasource/src/__tests__/datasource-admin-plugin.test.ts @@ -732,4 +732,60 @@ describe('DatasourceAdminServicePlugin: runtime datasource durability', () => { expect(row().checksum).toBe(hashSpec(JSON.parse(row().metadata as string), 'datasource')); }); }); + + // [#22835 · ADR-0131 D6] Every read here of a stored `datasource` row reads + // the environment's rows only: a row stored in a legacy organization's layer + // is never restored, served, pooled, updated or deleted. A legacy row under + // the same name as an environment row is inserted FIRST, so a read that + // picked the first row it found would pick it. + describe('[#22835] a legacy organization-scoped row is read by none of the plugin\'s reads', () => { + const row = (name: string, organizationId: string | null, host: string) => ({ + id: `meta_${name}_${organizationId ?? 'env'}`, name, type: 'datasource', organization_id: organizationId, + metadata: JSON.stringify({ name, label: host, origin: 'runtime', driver: 'postgres', config: { host, database: 'db' } }), + state: 'active', version: 1, + }); + const host = async (rows: Array>) => { + const data = Object.assign(fakeSysMetadataEngine(), { drivers: [] as any[] }); + (data as any).registerDriver = (d: any) => data.drivers.push(d); + (data as any).getDriverByName = (n: string) => data.drivers.find((d) => d.name === n); + data.rows.push(...rows); + const built: any[] = []; + const factory: IDatasourceDriverFactory = { + supports: (id: string) => id === 'postgres', + create: async (spec) => { built.push(spec); return { connect: async () => {}, disconnect: async () => {} }; }, + }; + const projectors = new Map Promise>(); + const protocol = { registerMutationProjector: (t: string, p: (evt: unknown) => Promise) => projectors.set(t, p) }; + const b = await boot({ driverFactory: factory, services: { data, protocol } }); + await b.plugin.start(b.ctx); + const metaDoorWrote = (name: string) => projectors.get('datasource')!({ type: 'datasource', name, state: 'active' }); + return { ...b, data, built, metaDoorWrote }; + }; + const LEGACY = row('dup', 'org_legacy', 'legacy-host'); + + it('the boot restore registers and pools environment rows only', async () => { + const b = await host([row('org_ds', 'org_legacy', 'org-host'), row('env_ds', null, 'env-host'), row('blank_ds', '', 'blank-host')]); + expect(await b.metadata.get('datasource', 'org_ds')).toBeUndefined(); + expect((await b.service.listDatasources()).map((d) => d.name).sort()).toEqual(['blank_ds', 'env_ds']); + expect(b.built.map((s) => s.name).sort()).toEqual(['blank_ds', 'env_ds']); + expect(b.data.rows.find((r) => r.name === 'org_ds')).toEqual(row('org_ds', 'org_legacy', 'org-host')); + }); + + it('the by-name reads skip a legacy row: projection and admin writes reach the environment row, and the legacy row is kept as it was', async () => { + const b = await host([LEGACY, row('dup', null, 'env-host'), row('only_legacy', 'org_legacy', 'l-host')]); + await b.metaDoorWrote('dup'); + await b.metaDoorWrote('only_legacy'); + expect(await b.metadata.get('datasource', 'dup')).toMatchObject({ label: 'env-host' }); + expect(await b.metadata.get('datasource', 'only_legacy')).toBeUndefined(); + expect([...new Set(b.built.map((s) => s.config.host))]).toEqual(['env-host']); + + await b.service.updateDatasource('dup', { label: 'edited' }); + expect(JSON.parse(String(b.data.rows.find((r) => r.id === 'meta_dup_env')!.metadata)).label).toBe('edited'); + await b.service.removeDatasource('dup'); + expect(b.data.rows.some((r) => r.id === 'meta_dup_env')).toBe(false); + await b.service.createDatasource({ name: 'dup', driver: 'postgres', config: { host: 'new-host', database: 'db' } }); + expect(b.data.rows.filter((r) => r.name === 'dup' && r.organization_id == null)).toHaveLength(1); + expect(b.data.rows.find((r) => r.id === LEGACY.id)).toEqual(LEGACY); + }); + }); }); diff --git a/packages/services/service-datasource/src/__tests__/datasource-system-context.pin.test.ts b/packages/services/service-datasource/src/__tests__/datasource-system-context.pin.test.ts index 6edad3a0cef..1ac6d8a5d44 100644 --- a/packages/services/service-datasource/src/__tests__/datasource-system-context.pin.test.ts +++ b/packages/services/service-datasource/src/__tests__/datasource-system-context.pin.test.ts @@ -139,8 +139,9 @@ describe('[#21913] runtime-datasource sys_metadata helpers carry the explicit sy expect(verbs.filter((v) => v === 'insert')).toHaveLength(1); expect(verbs.filter((v) => v === 'update')).toHaveLength(1); expect(verbs.filter((v) => v === 'delete')).toHaveLength(1); - expect(verbs.filter((v) => v === 'find').length).toBeGreaterThanOrEqual(1); - expect(verbs.filter((v) => v === 'findOne').length).toBeGreaterThanOrEqual(4); + // [#22835] Every read is a `find` filtered to the environment's rows: + // two existence probes, the boot restore, convergence and the delete probe. + expect(verbs.filter((v) => v === 'find').length).toBeGreaterThanOrEqual(5); expect(onMetadata).toHaveLength(data.calls.length); expectAllSystem(onMetadata); }); diff --git a/packages/services/service-datasource/src/datasource-admin-plugin.ts b/packages/services/service-datasource/src/datasource-admin-plugin.ts index a7f28b52b96..ed525af25cf 100644 --- a/packages/services/service-datasource/src/datasource-admin-plugin.ts +++ b/packages/services/service-datasource/src/datasource-admin-plugin.ts @@ -1,7 +1,7 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import type { Plugin, PluginContext } from '@objectstack/core'; -import { hashSpec } from '@objectstack/metadata-core'; +import { hashSpec, isLegacyOrganizationRow } from '@objectstack/metadata-core'; import { applyConversionsToStoredItem } from '@objectstack/spec'; import { registerMetadataTypeActions } from '@objectstack/spec/kernel'; import type { @@ -68,7 +68,7 @@ interface MetadataServiceLike { * dropped rather than moved so there is one declaration of them, not two. */ type DataEngineLike = Partial< - Pick + Pick >; /** @@ -111,6 +111,27 @@ function newMetaId(): string { : `meta_${Date.now()}_${Math.random().toString(36).slice(2)}`; } +/** + * [#22835 · ADR-0131 D6] The environment's active `datasource` rows — every + * one, or one name's. EVERY read below of a stored datasource row goes through + * here: the boot restore, the by-name read behind convergence and the `/meta` + * projection, and the existence probes behind this plugin's own writes. A row + * stored in a legacy organization's layer ({@link isLegacyOrganizationRow}, + * the one test the metadata reads apply) is therefore never restored, served, + * pooled, updated or deleted here; the boot report names it, and it stays at + * rest. A `find` filtered here, never a `findOne`: a single row picked by the + * store could be the legacy one while the environment's row exists. + */ +async function findEnvironmentRows( + engine: DataEngineLike | undefined, + name?: string, +): Promise>> { + if (!engine?.find) return []; + const where = { type: DS_META_TYPE, ...(name === undefined ? {} : { name }), state: 'active' }; + const rows = await engine.find(SYS_METADATA, { where }, { context: SYSTEM_CTX }); + return (rows ?? []).filter((row) => !isLegacyOrganizationRow(row)); +} + /** * [#21923] Write a runtime datasource's durable row — WITH the `checksum` the * metadata door's repository stamps on every row it writes @@ -123,14 +144,10 @@ function newMetaId(): string { * answered `409 METADATA_CONFLICT` — the two doors disagreeing on one record. */ async function persistDatasourceRow(engine: DataEngineLike | undefined, record: { name: string }): Promise { - if (!engine?.insert || !engine.findOne) return; // no durable store — in-memory only + if (!engine?.insert || !engine.find) return; // no durable store — in-memory only const now = new Date().toISOString(); const checksum = hashSpec(record, DS_META_TYPE); - const existing = await engine.findOne( - SYS_METADATA, - { where: { type: DS_META_TYPE, name: record.name, state: 'active' } }, - { context: SYSTEM_CTX }, - ); + const [existing] = await findEnvironmentRows(engine, record.name); if (existing) { await engine.update?.( SYS_METADATA, @@ -154,22 +171,16 @@ async function persistDatasourceRow(engine: DataEngineLike | undefined, record: } async function deleteDatasourceRow(engine: DataEngineLike | undefined, name: string): Promise { - if (!engine?.findOne) return; - const existing = await engine.findOne( - SYS_METADATA, - { where: { type: DS_META_TYPE, name, state: 'active' } }, - { context: SYSTEM_CTX }, - ); + if (!engine?.find) return; + const [existing] = await findEnvironmentRows(engine, name); if (!existing) return; if (engine.delete) await engine.delete(SYS_METADATA, { where: { id: existing.id }, context: SYSTEM_CTX }); else await engine.update?.(SYS_METADATA, { state: 'inactive' }, { where: { id: existing.id }, context: SYSTEM_CTX }); } async function loadDatasourceRows(engine: DataEngineLike | undefined): Promise>> { - if (!engine?.find) return []; - const rows = await engine.find(SYS_METADATA, { where: { type: DS_META_TYPE, state: 'active' } }, { context: SYSTEM_CTX }); const out: Array> = []; - for (const r of rows ?? []) { + for (const r of await findEnvironmentRows(engine)) { const raw = (r as { metadata?: unknown }).metadata; try { const parsed = typeof raw === 'string' ? JSON.parse(raw) : (raw as Record); @@ -192,7 +203,7 @@ async function loadDatasourceRows(engine: DataEngineLike | undefined): Promise { - if (!engine?.findOne) return undefined; - const row = await engine.findOne( - SYS_METADATA, - { where: { type: DS_META_TYPE, name, state: 'active' } }, - { context: SYSTEM_CTX }, - ); - const raw = (row as { metadata?: unknown } | null | undefined)?.metadata; + const [row] = await findEnvironmentRows(engine, name); + const raw = (row as { metadata?: unknown } | undefined)?.metadata; if (raw == null) return undefined; let parsed: Record; try { From b02b3f8c16ea80da01d042242fd60bf83764f03b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 15:02:17 +0000 Subject: [PATCH 2/2] test(dogfood): condense the #22835 real-boot pin's header Claude-Session: https://claude.ai/code/session_01JfJfBUC3cQ6hhgm9MQK76T Co-authored-by: Claude --- ...e-restore-environment-rows.dogfood.test.ts | 34 +++++-------------- 1 file changed, 8 insertions(+), 26 deletions(-) diff --git a/packages/qa/dogfood/test/datasource-restore-environment-rows.dogfood.test.ts b/packages/qa/dogfood/test/datasource-restore-environment-rows.dogfood.test.ts index 43351bcd62b..35d0aeebcd5 100644 --- a/packages/qa/dogfood/test/datasource-restore-environment-rows.dogfood.test.ts +++ b/packages/qa/dogfood/test/datasource-restore-environment-rows.dogfood.test.ts @@ -1,33 +1,15 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. // // [#22835 · ADR-0131 D6] The datasource-admin plugin reads ENVIRONMENT rows -// only — over the real showcase composition, across a restart. +// only, over the real showcase composition across a restart. Its reads of +// stored `datasource` rows carried no organization predicate, so a row stored +// in a legacy organization's layer was restored, served by both doors and +// pooled, while the boot report named it as served by no read. // -// ## What was broken -// -// Every metadata read serves the environment's `sys_metadata` rows only, and -// the boot report `[metadata_org_scoped_unserved]` names a row stored in a -// legacy organization's layer as served by no read and loaded by no boot. The -// datasource-admin plugin's own reads of stored `datasource` rows carried no -// such predicate: its boot restore registered a legacy organization-scoped -// row, and from there both the datasource-admin door and the metadata read -// door served it, and a live pool was opened from it. -// -// ## What each case pins -// -// - after a restart over a legacy organization-scoped row and an environment -// row, neither door serves the legacy row (`404`, absent from both lists) -// and no pool is opened from it; the environment row is served as before; -// - the boot report still names the legacy row, and the row is kept as it -// was — neither deleted nor rewritten; -// - the plugin's by-name reads behind its writes never reach the legacy row: -// an admin create and remove under its name write and remove an -// environment row of their own, and a `/meta` save under its name is what -// the admin door then serves. -// -// The verify harness composes the datasource-admin service but not its REST -// routes, so this file mounts `registerDatasourceAdminRoutes` the way -// `packages/cli/src/commands/serve.ts` does. +// Pinned: neither door serves the legacy row and no pool opens from it; the +// environment row is served; the boot report still names the legacy row, and +// it stays at rest; the by-name reads behind the plugin's writes never reach +// it. The routes are mounted the way `packages/cli/src/commands/serve.ts` does. import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'; import { registerDatasourceAdminRoutes } from '@objectstack/service-datasource';