Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .changeset/step2-prc-single-source.md
Original file line number Diff line number Diff line change
@@ -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".
2 changes: 1 addition & 1 deletion packages/metadata-protocol/src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
25 changes: 23 additions & 2 deletions packages/metadata-protocol/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -134,6 +155,6 @@ export function createMetadataProtocolPlugin(options: MetadataProtocolPluginOpti
message: 'Analytics SQL generation not implemented by ObjectQL adapter',
}),
});
},
};

return protocolShim;
}
6 changes: 0 additions & 6 deletions packages/objectql/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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';
124 changes: 7 additions & 117 deletions packages/objectql/src/plugin.ts
Original file line number Diff line number Diff line change
@@ -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 };

Expand Down Expand Up @@ -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)');
}
Expand Down
3 changes: 2 additions & 1 deletion packages/rest/src/export-integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

// ---------------------------------------------------------------------------
Expand Down
3 changes: 2 additions & 1 deletion packages/rest/src/import-integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

// ---------------------------------------------------------------------------
Expand Down
3 changes: 2 additions & 1 deletion packages/rest/src/import-job-integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
1 change: 1 addition & 0 deletions packages/runtime/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:*",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime/src/seed-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.