Skip to content

Commit 053c8bb

Browse files
os-zhuangclaude
andcommitted
feat(objectql): opt-in sys_metadata hydration for isolated project kernels
Boot Phase-2 hydration (restoreMetadataFromDb → loadMetaFromDb, which registers objects WITH their fields into the SchemaRegistry) is gated on `environmentId === undefined`. That gate assumes every project kernel sources metadata from a remote artifact / control-plane proxy and has no local sys_metadata. It's untrue for an isolated, proxy-free project kernel that persists its OWN sys_metadata locally (the cloud single-env tenant runtime on Turso): objects CREATED AT RUNTIME there — absent from the boot artifact manifest — never re-enter the registry after a restart, so `registry.getObject(name)` returns nothing for them and every registry consumer silently degrades (notably the engine.find unknown-$select guard, which then lets an unknown projected column zero the result set). Add an explicit `hydrateMetadataFromDb` plugin option (default false, so the control-plane/proxy path is untouched). When set, hydration runs even with `environmentId` defined. Safe because each engine owns its registry instance (no cross-kernel leakage — the historical process-wide singleton is gone, see engine.ts:274) and loadMetaFromDb already tolerates a missing table. The cloud artifact-kernel-factory — proxy-free, local sys_metadata, isolated registry — opts in. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c1dfe34 commit 053c8bb

2 files changed

Lines changed: 117 additions & 5 deletions

File tree

packages/objectql/src/plugin.integration.test.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -939,6 +939,86 @@ describe('ObjectQLPlugin - Metadata Service Integration', () => {
939939
});
940940
});
941941

942+
// A driver that serves one runtime-created object (with inline fields)
943+
// from sys_metadata — models an isolated, proxy-free project kernel.
944+
const makeLocalMetadataDriver = (findCalls: Array<{ object: string }>) => ({
945+
name: 'local-meta-driver',
946+
version: '1.0.0',
947+
connect: async () => {},
948+
disconnect: async () => {},
949+
find: async (object: string) => {
950+
findCalls.push({ object });
951+
if (object === 'sys_metadata') {
952+
return [
953+
{
954+
id: '1',
955+
type: 'object',
956+
name: 'product',
957+
state: 'active',
958+
metadata: JSON.stringify({
959+
name: 'product',
960+
label: 'Product',
961+
fields: { sku: { name: 'sku', label: 'SKU', type: 'text' } },
962+
}),
963+
packageId: 'inventory_pkg',
964+
},
965+
];
966+
}
967+
return [];
968+
},
969+
findOne: async () => null,
970+
create: async (_o: string, d: any) => d,
971+
update: async (_o: string, _i: any, d: any) => d,
972+
delete: async () => true,
973+
syncSchema: async () => {},
974+
});
975+
976+
it('does NOT hydrate a project kernel (environmentId set) without the opt-in', async () => {
977+
// Default behavior: a project kernel sources metadata from the artifact /
978+
// control-plane proxy, so boot must not read a local sys_metadata.
979+
const findCalls: Array<{ object: string }> = [];
980+
await kernel.use({
981+
name: 'mock-noopt-driver',
982+
type: 'driver',
983+
version: '1.0.0',
984+
init: async (ctx) => ctx.registerService('driver.noopt', makeLocalMetadataDriver(findCalls)),
985+
});
986+
987+
const plugin = new ObjectQLPlugin({ environmentId: 'env_1' });
988+
await kernel.use(plugin);
989+
await kernel.bootstrap();
990+
991+
expect(findCalls.find((c) => c.object === 'sys_metadata')).toBeUndefined();
992+
const registry = (kernel.getService('objectql') as any).registry;
993+
expect(registry.getObject('product')).toBeUndefined();
994+
});
995+
996+
it('hydrates a project kernel from local sys_metadata when hydrateMetadataFromDb is set (runtime objects regain their fields)', async () => {
997+
// The single-env tenant runtime case: an isolated, proxy-free kernel that
998+
// persists its OWN sys_metadata. Objects created at runtime (here:
999+
// `product`, not in any boot manifest) must re-enter the registry WITH
1000+
// their fields after a restart — otherwise registry.getObject('product')
1001+
// is empty and the engine.find unknown-$select guard can't fire.
1002+
const findCalls: Array<{ object: string }> = [];
1003+
await kernel.use({
1004+
name: 'mock-opt-driver',
1005+
type: 'driver',
1006+
version: '1.0.0',
1007+
init: async (ctx) => ctx.registerService('driver.opt', makeLocalMetadataDriver(findCalls)),
1008+
});
1009+
1010+
const plugin = new ObjectQLPlugin({ environmentId: 'env_1', hydrateMetadataFromDb: true });
1011+
await kernel.use(plugin);
1012+
await kernel.bootstrap();
1013+
1014+
expect(findCalls.find((c) => c.object === 'sys_metadata')).toBeDefined();
1015+
const registry = (kernel.getService('objectql') as any).registry;
1016+
const product = registry.getObject('product') as any;
1017+
expect(product).toBeDefined();
1018+
expect(product.fields).toBeDefined();
1019+
expect(Object.keys(product.fields)).toContain('sku');
1020+
});
1021+
9421022
it('should not throw when protocol.loadMetaFromDb fails (graceful degradation)', async () => {
9431023
// Arrange — driver that throws on find('sys_metadata')
9441024
const mockDriver = {

packages/objectql/src/plugin.ts

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,28 @@ export interface ObjectQLPluginOptions {
7575
* code change.
7676
*/
7777
skipSchemaSync?: boolean;
78+
/**
79+
* Hydrate the SchemaRegistry from this kernel's local `sys_metadata`
80+
* even when `environmentId` is set.
81+
*
82+
* By default Phase-2 hydration in `start()` is gated on
83+
* `environmentId === undefined`, because the original multi-environment
84+
* model assumed project kernels source metadata from a remote artifact /
85+
* control-plane proxy and have NO local `sys_metadata` to read. That is
86+
* NOT true for an isolated, proxy-free project kernel that persists its
87+
* OWN `sys_metadata` locally (e.g. the cloud single-env tenant runtime on
88+
* Turso): objects CREATED AT RUNTIME there — not present in the boot
89+
* artifact manifest — would otherwise never re-enter the registry after a
90+
* restart, so `registry.getObject(name)` returns nothing for them and any
91+
* registry consumer (the unknown-`$select` guard, hooks, relationships)
92+
* silently degrades.
93+
*
94+
* Set this ONLY when the kernel's registry is per-instance isolated AND
95+
* `sys_metadata` lives on the kernel's own local driver (no control-plane
96+
* proxy) — hydrating a proxied kernel would read the wrong database.
97+
* Safe to leave unset: hydration tolerates a missing table.
98+
*/
99+
hydrateMetadataFromDb?: boolean;
78100
}
79101

80102
export class ObjectQLPlugin implements Plugin {
@@ -92,6 +114,7 @@ export class ObjectQLPlugin implements Plugin {
92114
private hostContext?: Record<string, any>;
93115
private environmentId?: string;
94116
private skipSchemaSync = false;
117+
private hydrateMetadataFromDb = false;
95118
/** Unsubscribe handles for metadata-event subscriptions (ADR-0008 PR-7). */
96119
private metadataUnsubscribes: Array<() => void> = [];
97120

@@ -116,6 +139,7 @@ export class ObjectQLPlugin implements Plugin {
116139
typeof opts.skipSchemaSync === 'boolean'
117140
? opts.skipSchemaSync
118141
: process.env.OS_SKIP_SCHEMA_SYNC === '1';
142+
this.hydrateMetadataFromDb = opts.hydrateMetadataFromDb === true;
119143
}
120144

121145
init = async (ctx: PluginContext) => {
@@ -313,11 +337,19 @@ export class ObjectQLPlugin implements Plugin {
313337
}
314338

315339
// Phase 2: Hydrate SchemaRegistry from sys_metadata (loads custom/template objects).
316-
// Project kernels (environmentId set) never persist sys_metadata locally —
317-
// metadata is sourced from the artifact (MetadataPlugin) or routed to the
318-
// control plane via ControlPlaneProxyDriver. Skip to avoid querying a table
319-
// that does not exist on local project DBs.
320-
if (this.environmentId === undefined) {
340+
// Project kernels (environmentId set) USUALLY source metadata from the
341+
// artifact (MetadataPlugin) or a control-plane proxy and have no local
342+
// sys_metadata, so hydration is skipped to avoid querying a table that
343+
// does not exist (or, worse, a proxied remote one). EXCEPTION: an
344+
// isolated, proxy-free project kernel that persists its OWN sys_metadata
345+
// locally (the cloud single-env tenant runtime) opts in via
346+
// `hydrateMetadataFromDb` so objects CREATED AT RUNTIME there re-enter the
347+
// registry after a restart — otherwise registry.getObject() returns
348+
// nothing for them and every registry consumer (the unknown-$select
349+
// guard, hooks, relationships) silently degrades. Safe because each engine
350+
// owns its registry (no cross-kernel leakage) and hydration tolerates a
351+
// missing table.
352+
if (this.environmentId === undefined || this.hydrateMetadataFromDb) {
321353
await this.restoreMetadataFromDb(ctx);
322354
} else {
323355
ctx.logger.info('Project kernel — skipping sys_metadata hydration (metadata sourced from artifact)');

0 commit comments

Comments
 (0)