Skip to content

Commit e231abb

Browse files
os-zhuangclaude
andauthored
feat(objectql,metadata-protocol)!: single-source protocol assembly + drop re-exports — ADR-0076 Step 2 PR-C (#2462) (#3591)
- assembleMetadataProtocol() in metadata-protocol is now the ONE assembly (objects + protocol shim + D12 degraded analytics); createMetadataProtocolPlugin (cloud/delegated) and ObjectQLPlugin's registerProtocol convenience mode both mount it (~112 inline lines deleted from the engine plugin; the shim return feeds the engine's mutation-rebind subscription synchronously). - objectql's six protocol re-exports removed (breaking-as-minor per launch-window, #3486/#3488 precedent). Five known importers repointed (rest x3 tests, runtime seed-loader re-export + its integration test); runtime gains the direct metadata-protocol dep. - Recipe correction (recorded on #2462): the objectql→metadata-protocol DEPENDENCY stays — ./core was already protocol-free, and forcing ~20 framework boot sites to mount two plugins is a pure boilerplate tax. Step 2's goal lands as "zero assembly ownership, single source". Verified: full build; objectql 1086, rest 380, runtime 653, dogfood 60/61 in parallel (semantic-roles 10s-hook flake passes solo). Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent ea86843 commit e231abb

12 files changed

Lines changed: 66 additions & 131 deletions
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
"@objectstack/metadata-protocol": minor
3+
"@objectstack/objectql": minor
4+
"@objectstack/runtime": patch
5+
---
6+
7+
feat(objectql,metadata-protocol)!: single-source the protocol assembly; drop objectql's protocol re-exports — ADR-0076 Step 2 PR-C (#2462)
8+
9+
The ONE assembly now lives in `@objectstack/metadata-protocol` as
10+
`assembleMetadataProtocol()``createMetadataProtocolPlugin()` (delegated
11+
mode, cloud) and `ObjectQLPlugin`'s built-in convenience mode
12+
(`registerProtocol !== false`, single-kernel/dev boots) both mount the same
13+
code path (~112 inline lines deleted from the engine plugin). objectql's six
14+
protocol re-exports (`ObjectStackProtocolImplementation`,
15+
`SysMetadataRepository`, `SeedLoaderService`, `runBuildProbes` + types) are
16+
removed — import them from `@objectstack/metadata-protocol` directly
17+
(breaking, shipped as minor per the launch-window convention; the only known
18+
importers were five test files, repointed). Scope note vs the original Step-2
19+
recipe: the objectql→metadata-protocol dependency is deliberately KEPT for
20+
the convenience mount — `@objectstack/objectql/core` was already
21+
protocol-free, and forcing 20 framework boot sites to mount two plugins buys
22+
no runtime win. "Zero protocol dependency" lands as "zero assembly ownership,
23+
single source".

packages/metadata-protocol/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

33
export { ObjectStackProtocolImplementation, ConcurrentUpdateError, normalizeViewMetadata } from './protocol.js';
4-
export { createMetadataProtocolPlugin } from './plugin.js';
4+
export { createMetadataProtocolPlugin, assembleMetadataProtocol } from './plugin.js';
55
export type { MetadataProtocolPluginOptions } from './plugin.js';
66
export type { UninstallCleanup, UninstallCleanupOutcome } from './protocol.js';
77
export type { MetadataMutationEvent, MetadataMutationProjector, MutationProjectionOutcome } from './protocol.js';

packages/metadata-protocol/src/plugin.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,27 @@ export function createMetadataProtocolPlugin(options: MetadataProtocolPluginOpti
6666
);
6767
}
6868

69+
assembleMetadataProtocol(ctx, ql, environmentId);
70+
},
71+
};
72+
}
73+
74+
/**
75+
* The ONE protocol assembly (ADR-0076 Step 2 PR-C): metadata-storage platform
76+
* objects + `ObjectStackProtocolImplementation` as the `protocol` service +
77+
* the D12 `degraded` analytics fallback. Called by
78+
* {@link createMetadataProtocolPlugin} (delegated mode) AND by
79+
* `ObjectQLPlugin`'s built-in convenience mode (`registerProtocol !== false`)
80+
* — single source, two mounts, identical result.
81+
*
82+
* @returns the protocol shim, so the engine-side caller can arm its
83+
* mutation-rebind subscription synchronously.
84+
*/
85+
export function assembleMetadataProtocol(
86+
ctx: PluginContext,
87+
ql: any,
88+
environmentId?: string,
89+
): ObjectStackProtocolImplementation {
6990
// Metadata-storage platform objects (sys_metadata + history/audit
7091
// siblings + sys_view_definition). Same `environmentId === undefined`
7192
// gate as the historical assembly: platform / standalone kernels own
@@ -134,6 +155,6 @@ export function createMetadataProtocolPlugin(options: MetadataProtocolPluginOpti
134155
message: 'Analytics SQL generation not implemented by ObjectQL adapter',
135156
}),
136157
});
137-
},
138-
};
158+
159+
return protocolShim;
139160
}

packages/objectql/src/index.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,8 @@ export {
2727
export type { CompanionFieldMeta, CompanionObjectMeta } from './search-companion.js';
2828

2929
// Export Protocol Implementation
30-
export { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
3130

3231
// ADR-0008 PR-10b: MetadataRepository wrapper over the existing sys_metadata table.
33-
export { SysMetadataRepository } from '@objectstack/metadata-protocol';
34-
export type { SysMetadataEngine, SysMetadataRepositoryOptions } from '@objectstack/metadata-protocol';
3532

3633
// Export Engine
3734
export { ObjectQL, ObjectRepository, ScopedContext } from './engine.js';
@@ -121,9 +118,6 @@ export type {
121118

122119
// Seed loader — materializes `seed` metadata into rows (used by publishMetaItem
123120
// and the runtime dispatcher/app plugins).
124-
export { SeedLoaderService } from '@objectstack/metadata-protocol';
125121

126122
// ADR-0038 L3 — post-publish runtime probes (one real read per published
127123
// artifact); findings are BuildIssue-shaped with layer 'runtime'.
128-
export { runBuildProbes } from '@objectstack/metadata-protocol';
129-
export type { RuntimeBuildIssue, BuildProbeReport, RunBuildProbesOptions } from '@objectstack/metadata-protocol';

packages/objectql/src/plugin.ts

Lines changed: 7 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,11 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

33
import { ObjectQL } from './engine.js';
4-
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
4+
import { assembleMetadataProtocol } from '@objectstack/metadata-protocol';
55
import { Plugin, PluginContext } from '@objectstack/core';
66
import { StorageNameMapping } from '@objectstack/spec/system';
7-
import { SERVICE_SELF_INFO_KEY, type ServiceSelfInfo } from '@objectstack/spec/api';
87
import { LifecycleService } from './lifecycle/lifecycle-service.js';
98
import { lifecycleSettingsManifest } from './lifecycle/lifecycle-settings.js';
10-
import {
11-
SysMetadataObject,
12-
SysMetadataHistoryObject,
13-
SysMetadataCommitObject,
14-
SysMetadataAuditObject,
15-
SysViewDefinitionObject,
16-
} from '@objectstack/metadata-core';
179

1810
export type { Plugin, PluginContext };
1911

@@ -263,114 +255,12 @@ export class ObjectQLPlugin implements Plugin {
263255
});
264256

265257
if (this.registerProtocol) {
266-
// Register the metadata-storage objects this engine's own protocol reads
267-
// and writes — `sys_metadata` (loadMetaFromDb / getMetaItems / saveMetaItem),
268-
// its history/audit siblings, and `sys_view_definition`. Doing it here
269-
// guarantees their tables get schema-synced in start() even when no
270-
// MetadataPlugin is present (e.g. standalone "host config" apps, where the
271-
// CLI auto-registers a bare ObjectQLPlugin and nothing else owns these
272-
// tables → "no such table: sys_metadata" on every read).
273-
//
274-
// Gated on `environmentId === undefined` — the SAME condition that gates
275-
// `restoreMetadataFromDb` below: platform / standalone kernels own their
276-
// local sys_metadata, whereas per-project (cloud) kernels source metadata
277-
// from the control plane and must NOT provision these tables locally.
278-
// Definitions live in @objectstack/metadata-core (shared by this protocol
279-
// and the metadata layer's DatabaseLoader). registerApp is idempotent, so
280-
// a MetadataPlugin that also registers them is harmless.
281-
if (this.environmentId === undefined) {
282-
this.ql.registerApp({
283-
id: 'com.objectstack.metadata-objects',
284-
name: 'Metadata Platform Objects',
285-
version: '1.0.0',
286-
type: 'plugin',
287-
scope: 'system',
288-
objects: [
289-
SysMetadataObject,
290-
SysMetadataHistoryObject,
291-
SysMetadataCommitObject,
292-
SysMetadataAuditObject,
293-
SysViewDefinitionObject,
294-
],
295-
});
296-
}
297-
298-
// Register Protocol Implementation
299-
const protocolShim = new ObjectStackProtocolImplementation(
300-
this.ql,
301-
() => ctx.getServices ? ctx.getServices() : new Map(),
302-
this.environmentId,
303-
);
304-
305-
ctx.registerService('protocol', protocolShim);
306-
ctx.logger.info('Protocol service registered');
307-
308-
// ── Runtime-authored hook/action rebind on authoring (#2588, #2605) ──
309-
// The protocol is the ONE choke point every metadata-authoring surface
310-
// funnels through (rest-server PUT /meta, dispatcher, publish-drafts, AI
311-
// builders). When a `hook` or `action` row lands (direct-active save,
312-
// publish) or is deleted, re-sync the authored set so the change is live
313-
// without a restart. Draft saves are skipped — drafts are not live by
314-
// design. Fire-and-forget: a resync failure is logged, never fails the
315-
// write.
316-
this.subscribeMetadataRebind(ctx, protocolShim);
317-
318-
// Register an `analytics` service adapter that maps the dispatcher's
319-
// expected interface (query / getMeta / generateSql) onto the
320-
// protocol shim's `analyticsQuery`. Without this, HttpDispatcher's
321-
// `handleAnalytics` cannot resolve a service and `/api/v1/analytics/*`
322-
// returns ROUTE_NOT_FOUND, even though discovery advertises the route
323-
// (objectql's getDiscovery hardcodes `analytics: enabled:true`). The
324-
// adapter delegates `query` to the cube → engine.aggregate translator
325-
// already implemented in protocol.ts; getMeta/generateSql return a
326-
// structured "not implemented" payload so callers see something
327-
// useful instead of a 500.
328-
ctx.registerService('analytics', {
329-
// Honest capabilities (ADR-0076 D12, #2462): this adapter is a
330-
// deliberate lightweight fallback, not the full analytics engine —
331-
// self-identify so discovery reports it as 'degraded' instead of
332-
// 'available'. AnalyticsServicePlugin replaces this service (via
333-
// ctx.replaceService) with the real engine, which carries no marker.
334-
[SERVICE_SELF_INFO_KEY]: {
335-
status: 'degraded',
336-
handlerReady: true,
337-
message: 'Lightweight ObjectQL analytics fallback — install @objectstack/service-analytics for the full engine',
338-
} satisfies ServiceSelfInfo,
339-
// HttpDispatcher passes the raw POST body (AnalyticsQuery shape:
340-
// `{ cube, measures, dimensions, where?, filters?, ... }`). The
341-
// protocol shim's `analyticsQuery` expects the wrapped envelope
342-
// `{ cube, query }` and destructures `request.query` for dims /
343-
// measures. Reshape here so the destructure resolves to the
344-
// analytics query instead of `undefined` (which caused
345-
// "Cannot read properties of undefined (reading 'dimensions')").
346-
//
347-
// `analyticsQuery` also returns its own `{ success, data: { rows,
348-
// fields } }` envelope. HttpDispatcher wraps service responses
349-
// again with `success(result)`, so without unwrapping here the
350-
// client sees `{success, data:{success, data:{rows, fields}}}` —
351-
// KPI widgets read `data.rows` and silently get nothing. Unwrap
352-
// to the inner `{ rows, fields }` payload so a single wrap from
353-
// the dispatcher yields the canonical shape.
354-
query: async (body: any) => {
355-
const envelope = body && typeof body === 'object' && 'query' in body && 'cube' in body
356-
? body
357-
: { cube: body?.cube, query: body };
358-
const result = await protocolShim.analyticsQuery(envelope);
359-
// Unwrap an inner `{ success, data }` envelope (one level only).
360-
if (result && typeof result === 'object' && 'success' in result && 'data' in result) {
361-
return (result as any).data;
362-
}
363-
return result;
364-
},
365-
getMeta: async () => ({
366-
cubes: [],
367-
message: 'Analytics meta endpoint not implemented by ObjectQL adapter',
368-
}),
369-
generateSql: async (_body: any) => ({
370-
sql: null,
371-
message: 'Analytics SQL generation not implemented by ObjectQL adapter',
372-
}),
373-
});
258+
// ADR-0076 Step 2 PR-C: the ONE assembly lives in
259+
// @objectstack/metadata-protocol — this built-in mode is the
260+
// single-kernel convenience mount of the same code path the
261+
// MetadataProtocolPlugin uses (identical objects/protocol/analytics).
262+
const protocolShim = assembleMetadataProtocol(ctx, this.ql, this.environmentId);
263+
this.subscribeMetadataRebind(ctx, protocolShim);
374264
} else {
375265
ctx.logger.info('registerProtocol=false — protocol assembly delegated to MetadataProtocolPlugin (ADR-0076 Step 2, #2462)');
376266
}

packages/rest/src/export-integration.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323

2424
import { describe, it, expect, beforeEach } from 'vitest';
2525
import ExcelJS from 'exceljs';
26-
import { ObjectQL, ObjectStackProtocolImplementation } from '@objectstack/objectql';
26+
import { ObjectQL } from '@objectstack/objectql';
27+
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
2728
import { RestServer } from './rest-server';
2829

2930
// ---------------------------------------------------------------------------

packages/rest/src/import-integration.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
*/
1414

1515
import { describe, it, expect, beforeEach } from 'vitest';
16-
import { ObjectQL, ObjectStackProtocolImplementation } from '@objectstack/objectql';
16+
import { ObjectQL } from '@objectstack/objectql';
17+
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
1718
import { RestServer } from './rest-server';
1819

1920
// ---------------------------------------------------------------------------

packages/rest/src/import-job-integration.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
*/
1414

1515
import { describe, it, expect, beforeEach } from 'vitest';
16-
import { ObjectQL, ObjectStackProtocolImplementation } from '@objectstack/objectql';
16+
import { ObjectQL } from '@objectstack/objectql';
17+
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
1718
import { RestServer } from './rest-server';
1819

1920
// In-memory driver — equality + `$in`, with skip/limit (mirrors import-integration).

packages/runtime/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"@objectstack/formula": "workspace:*",
2727
"@objectstack/metadata": "workspace:*",
2828
"@objectstack/objectql": "workspace:*",
29+
"@objectstack/metadata-protocol": "workspace:*",
2930
"@objectstack/observability": "workspace:*",
3031
"@objectstack/plugin-auth": "workspace:*",
3132
"@objectstack/plugin-security": "workspace:*",

packages/runtime/src/bulk-write-real-driver.integration.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { mkdtempSync, rmSync } from 'node:fs';
1414
import { tmpdir } from 'node:os';
1515
import { join } from 'node:path';
1616
import { ObjectQL } from '@objectstack/objectql';
17-
import { SeedLoaderService } from '@objectstack/objectql';
17+
import { SeedLoaderService } from '@objectstack/metadata-protocol';
1818
import { SqlDriver } from '@objectstack/driver-sql';
1919

2020
/**

0 commit comments

Comments
 (0)