diff --git a/.changeset/step2-prc-single-source.md b/.changeset/step2-prc-single-source.md new file mode 100644 index 0000000000..63b7a5aaf3 --- /dev/null +++ b/.changeset/step2-prc-single-source.md @@ -0,0 +1,23 @@ +--- +"@objectstack/metadata-protocol": minor +"@objectstack/objectql": minor +"@objectstack/runtime": patch +--- + +feat(objectql,metadata-protocol)!: single-source the protocol assembly; drop objectql's protocol re-exports — ADR-0076 Step 2 PR-C (#2462) + +The ONE assembly now lives in `@objectstack/metadata-protocol` as +`assembleMetadataProtocol()` — `createMetadataProtocolPlugin()` (delegated +mode, cloud) and `ObjectQLPlugin`'s built-in convenience mode +(`registerProtocol !== false`, single-kernel/dev boots) both mount the same +code path (~112 inline lines deleted from the engine plugin). objectql's six +protocol re-exports (`ObjectStackProtocolImplementation`, +`SysMetadataRepository`, `SeedLoaderService`, `runBuildProbes` + types) are +removed — import them from `@objectstack/metadata-protocol` directly +(breaking, shipped as minor per the launch-window convention; the only known +importers were five test files, repointed). Scope note vs the original Step-2 +recipe: the objectql→metadata-protocol dependency is deliberately KEPT for +the convenience mount — `@objectstack/objectql/core` was already +protocol-free, and forcing 20 framework boot sites to mount two plugins buys +no runtime win. "Zero protocol dependency" lands as "zero assembly ownership, +single source". diff --git a/packages/metadata-protocol/src/index.ts b/packages/metadata-protocol/src/index.ts index 0f51a3587f..4e13d3fdaa 100644 --- a/packages/metadata-protocol/src/index.ts +++ b/packages/metadata-protocol/src/index.ts @@ -1,7 +1,7 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. export { ObjectStackProtocolImplementation, ConcurrentUpdateError, normalizeViewMetadata } from './protocol.js'; -export { createMetadataProtocolPlugin } from './plugin.js'; +export { createMetadataProtocolPlugin, assembleMetadataProtocol } from './plugin.js'; export type { MetadataProtocolPluginOptions } from './plugin.js'; export type { UninstallCleanup, UninstallCleanupOutcome } from './protocol.js'; export type { MetadataMutationEvent, MetadataMutationProjector, MutationProjectionOutcome } from './protocol.js'; diff --git a/packages/metadata-protocol/src/plugin.ts b/packages/metadata-protocol/src/plugin.ts index e2ef303d07..1363a4b5bf 100644 --- a/packages/metadata-protocol/src/plugin.ts +++ b/packages/metadata-protocol/src/plugin.ts @@ -66,6 +66,27 @@ export function createMetadataProtocolPlugin(options: MetadataProtocolPluginOpti ); } + assembleMetadataProtocol(ctx, ql, environmentId); + }, + }; +} + +/** + * The ONE protocol assembly (ADR-0076 Step 2 PR-C): metadata-storage platform + * objects + `ObjectStackProtocolImplementation` as the `protocol` service + + * the D12 `degraded` analytics fallback. Called by + * {@link createMetadataProtocolPlugin} (delegated mode) AND by + * `ObjectQLPlugin`'s built-in convenience mode (`registerProtocol !== false`) + * — single source, two mounts, identical result. + * + * @returns the protocol shim, so the engine-side caller can arm its + * mutation-rebind subscription synchronously. + */ +export function assembleMetadataProtocol( + ctx: PluginContext, + ql: any, + environmentId?: string, +): ObjectStackProtocolImplementation { // Metadata-storage platform objects (sys_metadata + history/audit // siblings + sys_view_definition). Same `environmentId === undefined` // gate as the historical assembly: platform / standalone kernels own @@ -134,6 +155,6 @@ export function createMetadataProtocolPlugin(options: MetadataProtocolPluginOpti message: 'Analytics SQL generation not implemented by ObjectQL adapter', }), }); - }, - }; + + return protocolShim; } diff --git a/packages/objectql/src/index.ts b/packages/objectql/src/index.ts index a31501fd2a..7273bce60a 100644 --- a/packages/objectql/src/index.ts +++ b/packages/objectql/src/index.ts @@ -27,11 +27,8 @@ export { export type { CompanionFieldMeta, CompanionObjectMeta } from './search-companion.js'; // Export Protocol Implementation -export { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol'; // ADR-0008 PR-10b: MetadataRepository wrapper over the existing sys_metadata table. -export { SysMetadataRepository } from '@objectstack/metadata-protocol'; -export type { SysMetadataEngine, SysMetadataRepositoryOptions } from '@objectstack/metadata-protocol'; // Export Engine export { ObjectQL, ObjectRepository, ScopedContext } from './engine.js'; @@ -121,9 +118,6 @@ export type { // Seed loader — materializes `seed` metadata into rows (used by publishMetaItem // and the runtime dispatcher/app plugins). -export { SeedLoaderService } from '@objectstack/metadata-protocol'; // ADR-0038 L3 — post-publish runtime probes (one real read per published // artifact); findings are BuildIssue-shaped with layer 'runtime'. -export { runBuildProbes } from '@objectstack/metadata-protocol'; -export type { RuntimeBuildIssue, BuildProbeReport, RunBuildProbesOptions } from '@objectstack/metadata-protocol'; diff --git a/packages/objectql/src/plugin.ts b/packages/objectql/src/plugin.ts index afcbd297d9..f88b576def 100644 --- a/packages/objectql/src/plugin.ts +++ b/packages/objectql/src/plugin.ts @@ -1,19 +1,11 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import { ObjectQL } from './engine.js'; -import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol'; +import { assembleMetadataProtocol } from '@objectstack/metadata-protocol'; import { Plugin, PluginContext } from '@objectstack/core'; import { StorageNameMapping } from '@objectstack/spec/system'; -import { SERVICE_SELF_INFO_KEY, type ServiceSelfInfo } from '@objectstack/spec/api'; import { LifecycleService } from './lifecycle/lifecycle-service.js'; import { lifecycleSettingsManifest } from './lifecycle/lifecycle-settings.js'; -import { - SysMetadataObject, - SysMetadataHistoryObject, - SysMetadataCommitObject, - SysMetadataAuditObject, - SysViewDefinitionObject, -} from '@objectstack/metadata-core'; export type { Plugin, PluginContext }; @@ -263,114 +255,12 @@ export class ObjectQLPlugin implements Plugin { }); if (this.registerProtocol) { - // Register the metadata-storage objects this engine's own protocol reads - // and writes — `sys_metadata` (loadMetaFromDb / getMetaItems / saveMetaItem), - // its history/audit siblings, and `sys_view_definition`. Doing it here - // guarantees their tables get schema-synced in start() even when no - // MetadataPlugin is present (e.g. standalone "host config" apps, where the - // CLI auto-registers a bare ObjectQLPlugin and nothing else owns these - // tables → "no such table: sys_metadata" on every read). - // - // Gated on `environmentId === undefined` — the SAME condition that gates - // `restoreMetadataFromDb` below: platform / standalone kernels own their - // local sys_metadata, whereas per-project (cloud) kernels source metadata - // from the control plane and must NOT provision these tables locally. - // Definitions live in @objectstack/metadata-core (shared by this protocol - // and the metadata layer's DatabaseLoader). registerApp is idempotent, so - // a MetadataPlugin that also registers them is harmless. - if (this.environmentId === undefined) { - this.ql.registerApp({ - id: 'com.objectstack.metadata-objects', - name: 'Metadata Platform Objects', - version: '1.0.0', - type: 'plugin', - scope: 'system', - objects: [ - SysMetadataObject, - SysMetadataHistoryObject, - SysMetadataCommitObject, - SysMetadataAuditObject, - SysViewDefinitionObject, - ], - }); - } - - // Register Protocol Implementation - const protocolShim = new ObjectStackProtocolImplementation( - this.ql, - () => ctx.getServices ? ctx.getServices() : new Map(), - this.environmentId, - ); - - ctx.registerService('protocol', protocolShim); - ctx.logger.info('Protocol service registered'); - - // ── Runtime-authored hook/action rebind on authoring (#2588, #2605) ── - // The protocol is the ONE choke point every metadata-authoring surface - // funnels through (rest-server PUT /meta, dispatcher, publish-drafts, AI - // builders). When a `hook` or `action` row lands (direct-active save, - // publish) or is deleted, re-sync the authored set so the change is live - // without a restart. Draft saves are skipped — drafts are not live by - // design. Fire-and-forget: a resync failure is logged, never fails the - // write. - this.subscribeMetadataRebind(ctx, protocolShim); - - // Register an `analytics` service adapter that maps the dispatcher's - // expected interface (query / getMeta / generateSql) onto the - // protocol shim's `analyticsQuery`. Without this, HttpDispatcher's - // `handleAnalytics` cannot resolve a service and `/api/v1/analytics/*` - // returns ROUTE_NOT_FOUND, even though discovery advertises the route - // (objectql's getDiscovery hardcodes `analytics: enabled:true`). The - // adapter delegates `query` to the cube → engine.aggregate translator - // already implemented in protocol.ts; getMeta/generateSql return a - // structured "not implemented" payload so callers see something - // useful instead of a 500. - ctx.registerService('analytics', { - // Honest capabilities (ADR-0076 D12, #2462): this adapter is a - // deliberate lightweight fallback, not the full analytics engine — - // self-identify so discovery reports it as 'degraded' instead of - // 'available'. AnalyticsServicePlugin replaces this service (via - // ctx.replaceService) with the real engine, which carries no marker. - [SERVICE_SELF_INFO_KEY]: { - status: 'degraded', - handlerReady: true, - message: 'Lightweight ObjectQL analytics fallback — install @objectstack/service-analytics for the full engine', - } satisfies ServiceSelfInfo, - // HttpDispatcher passes the raw POST body (AnalyticsQuery shape: - // `{ cube, measures, dimensions, where?, filters?, ... }`). The - // protocol shim's `analyticsQuery` expects the wrapped envelope - // `{ cube, query }` and destructures `request.query` for dims / - // measures. Reshape here so the destructure resolves to the - // analytics query instead of `undefined` (which caused - // "Cannot read properties of undefined (reading 'dimensions')"). - // - // `analyticsQuery` also returns its own `{ success, data: { rows, - // fields } }` envelope. HttpDispatcher wraps service responses - // again with `success(result)`, so without unwrapping here the - // client sees `{success, data:{success, data:{rows, fields}}}` — - // KPI widgets read `data.rows` and silently get nothing. Unwrap - // to the inner `{ rows, fields }` payload so a single wrap from - // the dispatcher yields the canonical shape. - query: async (body: any) => { - const envelope = body && typeof body === 'object' && 'query' in body && 'cube' in body - ? body - : { cube: body?.cube, query: body }; - const result = await protocolShim.analyticsQuery(envelope); - // Unwrap an inner `{ success, data }` envelope (one level only). - if (result && typeof result === 'object' && 'success' in result && 'data' in result) { - return (result as any).data; - } - return result; - }, - getMeta: async () => ({ - cubes: [], - message: 'Analytics meta endpoint not implemented by ObjectQL adapter', - }), - generateSql: async (_body: any) => ({ - sql: null, - message: 'Analytics SQL generation not implemented by ObjectQL adapter', - }), - }); + // ADR-0076 Step 2 PR-C: the ONE assembly lives in + // @objectstack/metadata-protocol — this built-in mode is the + // single-kernel convenience mount of the same code path the + // MetadataProtocolPlugin uses (identical objects/protocol/analytics). + const protocolShim = assembleMetadataProtocol(ctx, this.ql, this.environmentId); + this.subscribeMetadataRebind(ctx, protocolShim); } else { ctx.logger.info('registerProtocol=false — protocol assembly delegated to MetadataProtocolPlugin (ADR-0076 Step 2, #2462)'); } diff --git a/packages/rest/src/export-integration.test.ts b/packages/rest/src/export-integration.test.ts index 9fcf228a21..3b52c0d612 100644 --- a/packages/rest/src/export-integration.test.ts +++ b/packages/rest/src/export-integration.test.ts @@ -23,7 +23,8 @@ import { describe, it, expect, beforeEach } from 'vitest'; import ExcelJS from 'exceljs'; -import { ObjectQL, ObjectStackProtocolImplementation } from '@objectstack/objectql'; +import { ObjectQL } from '@objectstack/objectql'; +import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol'; import { RestServer } from './rest-server'; // --------------------------------------------------------------------------- diff --git a/packages/rest/src/import-integration.test.ts b/packages/rest/src/import-integration.test.ts index ce977e2f91..2281804a88 100644 --- a/packages/rest/src/import-integration.test.ts +++ b/packages/rest/src/import-integration.test.ts @@ -13,7 +13,8 @@ */ import { describe, it, expect, beforeEach } from 'vitest'; -import { ObjectQL, ObjectStackProtocolImplementation } from '@objectstack/objectql'; +import { ObjectQL } from '@objectstack/objectql'; +import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol'; import { RestServer } from './rest-server'; // --------------------------------------------------------------------------- diff --git a/packages/rest/src/import-job-integration.test.ts b/packages/rest/src/import-job-integration.test.ts index 66b6e6bc4a..370da4e292 100644 --- a/packages/rest/src/import-job-integration.test.ts +++ b/packages/rest/src/import-job-integration.test.ts @@ -13,7 +13,8 @@ */ import { describe, it, expect, beforeEach } from 'vitest'; -import { ObjectQL, ObjectStackProtocolImplementation } from '@objectstack/objectql'; +import { ObjectQL } from '@objectstack/objectql'; +import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol'; import { RestServer } from './rest-server'; // In-memory driver — equality + `$in`, with skip/limit (mirrors import-integration). diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 033effb240..56b7e62a6e 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -26,6 +26,7 @@ "@objectstack/formula": "workspace:*", "@objectstack/metadata": "workspace:*", "@objectstack/objectql": "workspace:*", + "@objectstack/metadata-protocol": "workspace:*", "@objectstack/observability": "workspace:*", "@objectstack/plugin-auth": "workspace:*", "@objectstack/plugin-security": "workspace:*", diff --git a/packages/runtime/src/bulk-write-real-driver.integration.test.ts b/packages/runtime/src/bulk-write-real-driver.integration.test.ts index 7042cc2d86..6daf3c8dc0 100644 --- a/packages/runtime/src/bulk-write-real-driver.integration.test.ts +++ b/packages/runtime/src/bulk-write-real-driver.integration.test.ts @@ -14,7 +14,7 @@ import { mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { ObjectQL } from '@objectstack/objectql'; -import { SeedLoaderService } from '@objectstack/objectql'; +import { SeedLoaderService } from '@objectstack/metadata-protocol'; import { SqlDriver } from '@objectstack/driver-sql'; /** diff --git a/packages/runtime/src/seed-loader.ts b/packages/runtime/src/seed-loader.ts index 1de8117681..75787d031b 100644 --- a/packages/runtime/src/seed-loader.ts +++ b/packages/runtime/src/seed-loader.ts @@ -6,4 +6,4 @@ // packages/rest cannot depend on runtime (runtime → rest), and objectql is the // layer that owns both the engine and the publish primitive. This shim keeps // the historical runtime import path working. -export { SeedLoaderService } from '@objectstack/objectql'; +export { SeedLoaderService } from '@objectstack/metadata-protocol'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6b6cdc7fd8..0c819099bb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1738,6 +1738,9 @@ importers: '@objectstack/metadata-core': specifier: workspace:* version: link:../metadata-core + '@objectstack/metadata-protocol': + specifier: workspace:* + version: link:../metadata-protocol '@objectstack/objectql': specifier: workspace:* version: link:../objectql