Skip to content

Commit 692d768

Browse files
feat(app-host): load all kernel plugins to match studio configuration
app-host was missing several kernel plugins when deployed to Vercel, causing incomplete functionality. This commit adds all the kernel plugins that studio uses to ensure feature parity. Changes: - Added missing plugin dependencies to package.json: - @objectstack/plugin-audit - @objectstack/plugin-security - @objectstack/plugin-setup - @objectstack/service-ai - @objectstack/service-analytics - @objectstack/service-automation - @objectstack/service-feed - Updated server/index.ts to register all kernel plugins: - SetupPlugin (loads before other plugins) - SecurityPlugin (RBAC and permissions) - AuditPlugin (audit logging) - FeedServicePlugin (activity feeds) - MetadataPlugin (metadata management) - AIServicePlugin (AI capabilities) - AutomationServicePlugin (flows and workflows) - AnalyticsServicePlugin (analytics) - Added broker shim to bridge HttpDispatcher → ObjectQL engine - Copied create-broker-shim.ts from studio - Integrated broker shim before kernel bootstrap - Added broker validation after bootstrap - Reordered plugin loading to match studio: - Apps load first (provide object schemas) - SetupPlugin loads early (navigation service) - Auth plugin loads after setup - Service plugins load after auth This ensures app-host has the same capabilities as studio when deployed to Vercel. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> Co-authored-by: xuyushun441-sys <255036401+xuyushun441-sys@users.noreply.github.com>
1 parent a5a0d9d commit 692d768

3 files changed

Lines changed: 351 additions & 4 deletions

File tree

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* Broker Shim Factory
5+
*
6+
* Creates an in-process broker shim that bridges HttpDispatcher calls
7+
* to ObjectQL engine operations. Required by both MSW (browser) and
8+
* Hono (server) modes since the simplified kernel setup does not include
9+
* a full message broker.
10+
*
11+
* @module
12+
*/
13+
14+
import { SchemaRegistry } from '@objectstack/objectql';
15+
16+
/**
17+
* Minimal broker interface expected by HttpDispatcher
18+
*/
19+
export interface BrokerShim {
20+
call(action: string, params: any, opts?: any): Promise<any>;
21+
}
22+
23+
/**
24+
* Create a broker shim bound to the given kernel instance.
25+
*
26+
* The shim delegates data/metadata/package actions to the ObjectQL engine
27+
* and SchemaRegistry that were registered on the kernel during bootstrap.
28+
*/
29+
export function createBrokerShim(kernel: any): BrokerShim {
30+
return {
31+
call: async (action: string, params: any, _opts: any) => {
32+
const parts = action.split('.');
33+
const service = parts[0];
34+
const method = parts[1];
35+
36+
// Get Engines
37+
const ql = kernel.context?.getService('objectql');
38+
39+
if (service === 'data') {
40+
// Delegate to protocol service when available for proper expand/populate support
41+
const protocol = kernel.context?.getService('protocol');
42+
// All data responses conform to protocol.zod.ts schemas:
43+
// CreateDataResponse = { object, id, record }
44+
// GetDataResponse = { object, id, record }
45+
// FindDataResponse = { object, records, total?, hasMore? }
46+
// UpdateDataResponse = { object, id, record }
47+
// DeleteDataResponse = { object, id, deleted }
48+
if (method === 'create') {
49+
const res = await ql.insert(params.object, params.data);
50+
const record = { ...params.data, ...res };
51+
return { object: params.object, id: record.id, record };
52+
}
53+
if (method === 'get') {
54+
// Delegate to protocol for proper expand/select support
55+
if (protocol) {
56+
return await protocol.getData({ object: params.object, id: params.id, expand: params.expand, select: params.select });
57+
}
58+
let all = await ql.find(params.object);
59+
if (!all) all = [];
60+
const match = all.find((i: any) => i.id === params.id);
61+
return match ? { object: params.object, id: params.id, record: match } : null;
62+
}
63+
if (method === 'update') {
64+
if (params.id) {
65+
let all = await ql.find(params.object);
66+
67+
if (all && (all as any).value) all = (all as any).value;
68+
if (!all) all = [];
69+
70+
const existing = all.find((i: any) => i.id === params.id);
71+
72+
if (!existing) {
73+
console.warn(`[BrokerShim] Update failed: Record ${params.id} not found.`);
74+
throw new Error('[ObjectStack] Not Found');
75+
}
76+
77+
try {
78+
await ql.update(params.object, params.data, { where: { id: params.id } });
79+
} catch (err: any) {
80+
console.warn(`[BrokerShim] update failed: ${err.message}`);
81+
throw err;
82+
}
83+
84+
return { object: params.object, id: params.id, record: { ...existing, ...params.data } };
85+
}
86+
return null;
87+
}
88+
if (method === 'delete') {
89+
try {
90+
await ql.delete(params.object, { where: { id: params.id } });
91+
return { object: params.object, id: params.id, deleted: true };
92+
} catch (err: any) {
93+
console.warn(`[BrokerShim] delete failed: ${err.message}`);
94+
throw err;
95+
}
96+
}
97+
if (method === 'find' || method === 'query') {
98+
// Delegate to protocol for proper expand/populate support
99+
if (protocol) {
100+
return await protocol.findData({ object: params.object, query: params.query || params.filters });
101+
}
102+
let all = await ql.find(params.object);
103+
104+
// Handle PaginatedResult { value: [...] } vs Array [...]
105+
if (!Array.isArray(all) && all && (all as any).value) {
106+
all = (all as any).value;
107+
}
108+
109+
if (!all) all = [];
110+
111+
const filters = params.query || params.filters;
112+
let queryOptions: any = {};
113+
if (filters && typeof filters === 'object') {
114+
const reserved = ['top', 'skip', 'sort', 'select', 'expand', 'count', 'search'];
115+
reserved.forEach(opt => {
116+
if (filters[opt] !== undefined) {
117+
queryOptions[opt] = filters[opt];
118+
}
119+
});
120+
}
121+
122+
if (filters && typeof filters === 'object' && !Array.isArray(filters)) {
123+
const reserved = ['top', 'skip', 'sort', 'select', 'expand', 'count', 'search'];
124+
const keys = Object.keys(filters).filter(k => !reserved.includes(k));
125+
126+
if (keys.length > 0) {
127+
all = all.filter((item: any) => {
128+
return keys.every(k => {
129+
return String(item[k]) == String(filters[k]);
130+
});
131+
});
132+
}
133+
}
134+
135+
// --- Sort ---
136+
if (queryOptions.sort) {
137+
const sortFields = String(queryOptions.sort).split(',').map(s => s.trim());
138+
all.sort((a: any, b: any) => {
139+
for (const field of sortFields) {
140+
const desc = field.startsWith('-');
141+
const key = desc ? field.substring(1) : field;
142+
if (a[key] < b[key]) return desc ? 1 : -1;
143+
if (a[key] > b[key]) return desc ? -1 : 1;
144+
}
145+
return 0;
146+
});
147+
}
148+
149+
// --- Select ---
150+
if (queryOptions.select) {
151+
const selectFields = Array.isArray(queryOptions.select)
152+
? queryOptions.select
153+
: String(queryOptions.select).split(',').map((s: string) => s.trim());
154+
155+
all = all.map((item: any) => {
156+
const projected: any = { id: item.id }; // Always include ID
157+
selectFields.forEach((f: string) => {
158+
if (item[f] !== undefined) projected[f] = item[f];
159+
});
160+
return projected;
161+
});
162+
}
163+
164+
// --- Skip/Top ---
165+
const totalCount = all.length;
166+
const skip = parseInt(queryOptions.skip) || 0;
167+
const top = parseInt(queryOptions.top);
168+
169+
if (skip > 0) {
170+
all = all.slice(skip);
171+
}
172+
if (!isNaN(top)) {
173+
all = all.slice(0, top);
174+
}
175+
176+
return { object: params.object, records: all, total: totalCount };
177+
}
178+
}
179+
180+
if (service === 'metadata') {
181+
// Get MetadataService for runtime-registered metadata (agents, tools, etc.)
182+
const metadataService = kernel.context?.getService('metadata');
183+
184+
if (method === 'types') {
185+
// Combine types from both SchemaRegistry (static) and MetadataService (runtime)
186+
const schemaTypes = SchemaRegistry.getRegisteredTypes();
187+
188+
// MetadataService exposes types through getRegisteredTypes() method
189+
let runtimeTypes: string[] = [];
190+
if (metadataService && typeof metadataService.getRegisteredTypes === 'function') {
191+
runtimeTypes = await metadataService.getRegisteredTypes();
192+
}
193+
194+
// Merge and deduplicate
195+
const allTypes = Array.from(new Set([...schemaTypes, ...runtimeTypes]));
196+
return { types: allTypes };
197+
}
198+
if (method === 'objects') {
199+
const packageId = params.packageId;
200+
let objs = (ql && typeof ql.getObjects === 'function') ? ql.getObjects() : [];
201+
202+
if (!objs || objs.length === 0) {
203+
objs = SchemaRegistry.getAllObjects(packageId);
204+
} else if (packageId) {
205+
objs = objs.filter((o: any) => o._packageId === packageId);
206+
}
207+
return { type: 'object', items: objs };
208+
}
209+
if (method === 'getObject' || method === 'getItem') {
210+
if (!params.objectName && !params.name) {
211+
return SchemaRegistry.getAllObjects();
212+
}
213+
214+
const name = params.objectName || params.name;
215+
216+
let def = SchemaRegistry.getObject(name);
217+
218+
if (!def && ql && typeof (ql as any).getObject === 'function') {
219+
def = (ql as any).getObject(name);
220+
}
221+
return def || null;
222+
}
223+
// Generic metadata type: metadata.<type> → check both SchemaRegistry and MetadataService
224+
const packageId = params.packageId;
225+
226+
// Try SchemaRegistry first (static metadata from packages)
227+
let items = SchemaRegistry.listItems(method, packageId);
228+
229+
// Also check MetadataService for runtime-registered metadata (agents, tools, etc.)
230+
if (metadataService && typeof metadataService.list === 'function') {
231+
try {
232+
const runtimeItems = await metadataService.list(method);
233+
if (runtimeItems && runtimeItems.length > 0) {
234+
// Merge items, avoiding duplicates by name
235+
const itemMap = new Map();
236+
items.forEach((item: any) => itemMap.set(item.name, item));
237+
runtimeItems.forEach((item: any) => {
238+
if (item && typeof item === 'object' && 'name' in item) {
239+
itemMap.set(item.name, item);
240+
}
241+
});
242+
items = Array.from(itemMap.values());
243+
}
244+
} catch (err) {
245+
// MetadataService.list might fail for unknown types, that's OK
246+
console.debug(`[BrokerShim] MetadataService.list('${method}') failed:`, err);
247+
}
248+
}
249+
250+
if (items && items.length > 0) {
251+
return { type: method, items };
252+
}
253+
return { type: method, items: [] };
254+
}
255+
256+
// Package Management Actions
257+
if (service === 'package') {
258+
if (method === 'list') {
259+
let packages = SchemaRegistry.getAllPackages();
260+
if (params.status) {
261+
packages = packages.filter((p: any) => p.status === params.status);
262+
}
263+
if (params.type) {
264+
packages = packages.filter((p: any) => p.manifest?.type === params.type);
265+
}
266+
if (params.enabled !== undefined) {
267+
packages = packages.filter((p: any) => p.enabled === params.enabled);
268+
}
269+
return { packages, total: packages.length };
270+
}
271+
if (method === 'get') {
272+
const pkg = SchemaRegistry.getPackage(params.id);
273+
if (!pkg) throw new Error(`Package not found: ${params.id}`);
274+
return { package: pkg };
275+
}
276+
if (method === 'install') {
277+
const manifest = params.manifest;
278+
const id = manifest?.id || manifest?.name;
279+
280+
if (ql && typeof (ql as any).registerApp === 'function') {
281+
(ql as any).registerApp(manifest);
282+
} else {
283+
SchemaRegistry.installPackage(manifest, params.settings);
284+
}
285+
286+
const pkg = id ? SchemaRegistry.getPackage(id) : null;
287+
return { package: pkg, message: `Package ${id || 'unknown'} installed successfully` };
288+
}
289+
if (method === 'uninstall') {
290+
const success = SchemaRegistry.uninstallPackage(params.id);
291+
return { id: params.id, success, message: success ? 'Uninstalled' : 'Not found' };
292+
}
293+
if (method === 'enable') {
294+
const pkg = SchemaRegistry.enablePackage(params.id);
295+
if (!pkg) throw new Error(`Package not found: ${params.id}`);
296+
return { package: pkg, message: `Package ${params.id} enabled` };
297+
}
298+
if (method === 'disable') {
299+
const pkg = SchemaRegistry.disablePackage(params.id);
300+
if (!pkg) throw new Error(`Package not found: ${params.id}`);
301+
return { package: pkg, message: `Package ${params.id} disabled` };
302+
}
303+
}
304+
305+
console.warn(`[BrokerShim] Action not implemented: ${action}`);
306+
return null;
307+
}
308+
};
309+
}

examples/app-host/package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,16 @@
2424
"@objectstack/hono": "workspace:*",
2525
"@objectstack/metadata": "workspace:*",
2626
"@objectstack/objectql": "workspace:*",
27+
"@objectstack/plugin-audit": "workspace:*",
2728
"@objectstack/plugin-auth": "workspace:*",
2829
"@objectstack/plugin-hono-server": "workspace:*",
30+
"@objectstack/plugin-security": "workspace:*",
31+
"@objectstack/plugin-setup": "workspace:*",
2932
"@objectstack/runtime": "workspace:*",
33+
"@objectstack/service-ai": "workspace:*",
34+
"@objectstack/service-analytics": "workspace:*",
35+
"@objectstack/service-automation": "workspace:*",
36+
"@objectstack/service-feed": "workspace:*",
3037
"@objectstack/spec": "workspace:*",
3138
"hono": "^4.12.12"
3239
},

examples/app-host/server/index.ts

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,17 @@ import { ObjectQLPlugin } from '@objectstack/objectql';
1515
import { TursoDriver } from '@objectstack/driver-turso';
1616
import { createHonoApp } from '@objectstack/hono';
1717
import { AuthPlugin } from '@objectstack/plugin-auth';
18+
import { SecurityPlugin } from '@objectstack/plugin-security';
19+
import { AuditPlugin } from '@objectstack/plugin-audit';
20+
import { SetupPlugin } from '@objectstack/plugin-setup';
21+
import { FeedServicePlugin } from '@objectstack/service-feed';
22+
import { MetadataPlugin } from '@objectstack/metadata';
23+
import { AIServicePlugin } from '@objectstack/service-ai';
24+
import { AutomationServicePlugin } from '@objectstack/service-automation';
25+
import { AnalyticsServicePlugin } from '@objectstack/service-analytics';
1826
import { getRequestListener } from '@hono/node-server';
1927
import type { Hono } from 'hono';
28+
import { createBrokerShim } from '../lib/create-broker-shim.js';
2029
import CrmApp from '@example/app-crm';
2130
import TodoApp from '@example/app-todo';
2231
import BiPluginManifest from '@example/plugin-bi';
@@ -70,6 +79,15 @@ async function ensureKernel(): Promise<ObjectKernel> {
7079

7180
await kernel.use(new DriverPlugin(tursoDriver));
7281

82+
// Load app manifests (BEFORE plugins that need object schemas)
83+
await kernel.use(new AppPlugin(CrmApp));
84+
await kernel.use(new AppPlugin(TodoApp));
85+
await kernel.use(new AppPlugin(BiPluginManifest));
86+
87+
// SetupPlugin must load BEFORE other plugins that contribute navigation items
88+
// so that the setupNav service is available during their init() phase
89+
await kernel.use(new SetupPlugin());
90+
7391
// Auth plugin — uses environment variables for configuration
7492
// Prefer VERCEL_PROJECT_PRODUCTION_URL (stable across deployments)
7593
// over VERCEL_URL (unique per deployment, causes origin mismatch).
@@ -84,13 +102,26 @@ async function ensureKernel(): Promise<ObjectKernel> {
84102
baseUrl: vercelUrl,
85103
}));
86104

87-
// Load app manifests
88-
await kernel.use(new AppPlugin(CrmApp));
89-
await kernel.use(new AppPlugin(TodoApp));
90-
await kernel.use(new AppPlugin(BiPluginManifest));
105+
// Register all kernel plugins (matching studio configuration)
106+
await kernel.use(new SecurityPlugin());
107+
await kernel.use(new AuditPlugin());
108+
await kernel.use(new FeedServicePlugin());
109+
await kernel.use(new MetadataPlugin({ watch: false }));
110+
await kernel.use(new AIServicePlugin());
111+
await kernel.use(new AutomationServicePlugin());
112+
await kernel.use(new AnalyticsServicePlugin());
113+
114+
// Broker shim — bridges HttpDispatcher → ObjectQL engine
115+
(kernel as any).broker = createBrokerShim(kernel);
91116

92117
await kernel.bootstrap();
93118

119+
// Validate broker attachment
120+
if (!(kernel as any).broker) {
121+
console.warn('[Vercel] Broker shim lost during bootstrap — reattaching.');
122+
(kernel as any).broker = createBrokerShim(kernel);
123+
}
124+
94125
_kernel = kernel;
95126
console.log('[Vercel] Kernel ready.');
96127
return kernel;

0 commit comments

Comments
 (0)