Skip to content

Commit 259b7ad

Browse files
committed
feat: add packageId support for metadata filtering and registration across components
1 parent 3400380 commit 259b7ad

7 files changed

Lines changed: 53 additions & 27 deletions

File tree

apps/console/src/components/app-sidebar.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,14 +129,17 @@ export function AppSidebar({ client, selectedObject, onSelectObject, packages, s
129129
}
130130
setMetaTypes(types);
131131

132+
// Package scope: filter metadata by selected package
133+
const packageId = selectedPackage?.manifest?.id;
134+
132135
// 2. Load items for each type in parallel
133136
const entries = await Promise.all(
134137
types
135138
.filter(t => !HIDDEN_TYPES.has(t))
136139
.map(async (type) => {
137140
try {
138141
// Spec: GetMetaItemsResponse = { type, items: any[] }
139-
const result = await client.meta.getItems(type);
142+
const result = await client.meta.getItems(type, packageId ? { packageId } : undefined);
140143
let items: any[] = [];
141144
if (Array.isArray(result)) {
142145
items = result as any;
@@ -157,7 +160,7 @@ export function AppSidebar({ client, selectedObject, onSelectObject, packages, s
157160
} finally {
158161
setLoading(false);
159162
}
160-
}, [client]);
163+
}, [client, selectedPackage]);
161164

162165
useEffect(() => { loadMetadata(); }, [loadMetadata]);
163166

apps/console/src/mocks/createKernel.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -204,10 +204,14 @@ export async function createKernel(options: KernelOptions) {
204204
}
205205
if (method === 'objects') {
206206
// Return spec-compliant GetMetaItemsResponse
207+
// Support optional packageId filter
208+
const packageId = params.packageId;
207209
let objs = (ql && typeof ql.getObjects === 'function') ? ql.getObjects() : [];
208210

209211
if (!objs || objs.length === 0) {
210-
objs = SchemaRegistry.getAllObjects();
212+
objs = SchemaRegistry.getAllObjects(packageId);
213+
} else if (packageId) {
214+
objs = objs.filter((o: any) => o._packageId === packageId);
211215
}
212216
return { type: 'object', items: objs };
213217
}
@@ -230,8 +234,9 @@ export async function createKernel(options: KernelOptions) {
230234
}
231235
return def || null;
232236
}
233-
// Generic metadata type: metadata.<type> → SchemaRegistry.listItems(type)
234-
const items = SchemaRegistry.listItems(method);
237+
// Generic metadata type: metadata.<type> → SchemaRegistry.listItems(type, packageId?)
238+
const packageId = params.packageId;
239+
const items = SchemaRegistry.listItems(method, packageId);
235240
if (items && items.length > 0) {
236241
return { type: method, items };
237242
}

packages/client/src/index.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,10 +208,15 @@ export class ObjectStackClient {
208208
/**
209209
* Get all items of a specific metadata type
210210
* @param type - Metadata type name (e.g., 'object', 'plugin')
211+
* @param options - Optional filters (e.g., packageId to scope by package)
211212
*/
212-
getItems: async (type: string): Promise<GetMetaItemsResponse> => {
213+
getItems: async (type: string, options?: { packageId?: string }): Promise<GetMetaItemsResponse> => {
213214
const route = this.getRoute('metadata');
214-
const res = await this.fetch(`${this.baseUrl}${route}/${type}`);
215+
const params = new URLSearchParams();
216+
if (options?.packageId) params.set('package', options.packageId);
217+
const qs = params.toString();
218+
const url = `${this.baseUrl}${route}/${type}${qs ? `?${qs}` : ''}`;
219+
const res = await this.fetch(url);
215220
return this.unwrapResponse<GetMetaItemsResponse>(res);
216221
},
217222

packages/objectql/src/engine.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ describe('ObjectQL Engine', () => {
8888
};
8989

9090
engine.registerApp(manifest);
91-
expect(SchemaRegistry.registerObject).toHaveBeenCalledWith(expect.objectContaining({ name: 'task' }));
91+
expect(SchemaRegistry.registerObject).toHaveBeenCalledWith(expect.objectContaining({ name: 'task' }), 'com.example.app');
9292
});
9393

9494
it('should register kinds from app manifest', () => {

packages/objectql/src/engine.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -161,15 +161,15 @@ export class ObjectQL implements IDataEngine {
161161
if (Array.isArray(manifest.objects)) {
162162
this.logger.debug('Registering objects from manifest (Array)', { id, objectCount: manifest.objects.length });
163163
for (const objDef of manifest.objects) {
164-
SchemaRegistry.registerObject(objDef);
164+
SchemaRegistry.registerObject(objDef, id);
165165
this.logger.debug('Registered Object', { object: objDef.name, from: id });
166166
}
167167
} else {
168168
this.logger.debug('Registering objects from manifest (Map)', { id, objectCount: Object.keys(manifest.objects).length });
169169
for (const [name, objDef] of Object.entries(manifest.objects)) {
170170
// Ensure name in definition matches key
171171
(objDef as any).name = name;
172-
SchemaRegistry.registerObject(objDef as any);
172+
SchemaRegistry.registerObject(objDef as any, id);
173173
this.logger.debug('Registered Object', { object: name, from: id });
174174
}
175175
}
@@ -181,7 +181,7 @@ export class ObjectQL implements IDataEngine {
181181
for (const app of manifest.apps) {
182182
const appName = app.name || app.id;
183183
if (appName) {
184-
SchemaRegistry.registerApp(app);
184+
SchemaRegistry.registerApp(app, id);
185185
this.logger.debug('Registered App', { app: appName, from: id });
186186
}
187187
}
@@ -190,7 +190,7 @@ export class ObjectQL implements IDataEngine {
190190
// 4. If manifest itself looks like an App (has navigation), also register as app
191191
// This handles the case where the manifest IS the app definition (legacy/simple packages)
192192
if (manifest.name && manifest.navigation && !manifest.apps?.length) {
193-
SchemaRegistry.registerApp(manifest);
193+
SchemaRegistry.registerApp(manifest, id);
194194
this.logger.debug('Registered manifest-as-app', { app: manifest.name, from: id });
195195
}
196196

@@ -206,7 +206,7 @@ export class ObjectQL implements IDataEngine {
206206
for (const item of items) {
207207
const itemName = item.name || item.id;
208208
if (itemName) {
209-
SchemaRegistry.registerItem(key, item, 'name' as any);
209+
SchemaRegistry.registerItem(key, item, 'name' as any, id);
210210
}
211211
}
212212
}

packages/objectql/src/registry.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,18 @@ export class SchemaRegistry {
2222
* @param type The category of metadata (e.g., 'object', 'package', 'apps')
2323
* @param item The metadata item itself
2424
* @param keyField The property to use as the unique key (default: 'name')
25+
* @param packageId Optional: the owning package ID for scoped queries
2526
*/
26-
static registerItem<T>(type: string, item: T, keyField: keyof T = 'name' as keyof T) {
27+
static registerItem<T>(type: string, item: T, keyField: keyof T = 'name' as keyof T, packageId?: string) {
2728
if (!this.metadata.has(type)) {
2829
this.metadata.set(type, new Map());
2930
}
3031
const collection = this.metadata.get(type)!;
3132
const key = String(item[keyField]);
33+
// Tag item with owning package for scoped queries
34+
if (packageId) {
35+
(item as any)._packageId = packageId;
36+
}
3237

3338
// Validation Hook
3439
try {
@@ -88,9 +93,15 @@ export class SchemaRegistry {
8893

8994
/**
9095
* Universal List Method
96+
* @param type The metadata type to list
97+
* @param packageId Optional: filter items belonging to a specific package
9198
*/
92-
static listItems<T>(type: string): T[] {
93-
return Array.from(this.metadata.get(type)?.values() || []) as T[];
99+
static listItems<T>(type: string, packageId?: string): T[] {
100+
const items = Array.from(this.metadata.get(type)?.values() || []) as T[];
101+
if (packageId) {
102+
return items.filter((item: any) => item._packageId === packageId);
103+
}
104+
return items;
94105
}
95106

96107
/**
@@ -107,16 +118,16 @@ export class SchemaRegistry {
107118
/**
108119
* Object Helpers
109120
*/
110-
static registerObject(schema: ServiceObject) {
111-
this.registerItem('object', schema, 'name');
121+
static registerObject(schema: ServiceObject, packageId?: string) {
122+
this.registerItem('object', schema, 'name', packageId);
112123
}
113124

114125
static getObject(name: string): ServiceObject | undefined {
115126
return this.getItem<ServiceObject>('object', name);
116127
}
117128

118-
static getAllObjects(): ServiceObject[] {
119-
return this.listItems<ServiceObject>('object');
129+
static getAllObjects(packageId?: string): ServiceObject[] {
130+
return this.listItems<ServiceObject>('object', packageId);
120131
}
121132

122133
/**
@@ -198,8 +209,8 @@ export class SchemaRegistry {
198209
* App Helpers — UI navigation shells extracted from packages
199210
* @deprecated Use registerItem('apps', app, 'name') instead of registerApp for clarity
200211
*/
201-
static registerApp(app: any) {
202-
this.registerItem('apps', app, 'name');
212+
static registerApp(app: any, packageId?: string) {
213+
this.registerItem('apps', app, 'name', packageId);
203214
}
204215

205216
static getApp(name: string): any {

packages/runtime/src/http-dispatcher.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ export class HttpDispatcher {
137137
* Standard: /metadata/:type/:name
138138
* Fallback for backward compat: /metadata (all objects), /metadata/:objectName (get object)
139139
*/
140-
async handleMetadata(path: string, context: HttpProtocolContext, method?: string, body?: any): Promise<HttpDispatcherResult> {
140+
async handleMetadata(path: string, context: HttpProtocolContext, method?: string, body?: any, query?: any): Promise<HttpDispatcherResult> {
141141
const broker = this.ensureBroker();
142142
const parts = path.replace(/^\/+/, '').split('/').filter(Boolean);
143143

@@ -224,12 +224,14 @@ export class HttpDispatcher {
224224
// GET /metadata/:type (List items of type) OR /metadata/:objectName (Legacy)
225225
if (parts.length === 1) {
226226
const typeOrName = parts[0];
227+
// Extract optional package filter from query string
228+
const packageId = query?.package || undefined;
227229

228230
// Try protocol service first for any type
229231
const protocol = this.kernel?.context?.getService ? this.kernel.context.getService('protocol') : null;
230232
if (protocol && typeof protocol.getMetaItems === 'function') {
231233
try {
232-
const data = await protocol.getMetaItems({ type: typeOrName });
234+
const data = await protocol.getMetaItems({ type: typeOrName, packageId });
233235
if (data && ((data.items && data.items.length > 0) || (Array.isArray(data) && data.length > 0))) {
234236
return { handled: true, response: this.success(data) };
235237
}
@@ -241,10 +243,10 @@ export class HttpDispatcher {
241243
// Try broker for the type
242244
try {
243245
if (typeOrName === 'objects') {
244-
const data = await broker.call('metadata.objects', {}, { request: context.request });
246+
const data = await broker.call('metadata.objects', { packageId }, { request: context.request });
245247
return { handled: true, response: this.success(data) };
246248
}
247-
const data = await broker.call(`metadata.${typeOrName}`, {}, { request: context.request });
249+
const data = await broker.call(`metadata.${typeOrName}`, { packageId }, { request: context.request });
248250
if (data !== null && data !== undefined) {
249251
return { handled: true, response: this.success(data) };
250252
}
@@ -683,7 +685,7 @@ export class HttpDispatcher {
683685
}
684686

685687
if (cleanPath.startsWith('/meta')) {
686-
return this.handleMetadata(cleanPath.substring(5), context);
688+
return this.handleMetadata(cleanPath.substring(5), context, method, body, query);
687689
}
688690

689691
if (cleanPath.startsWith('/data')) {

0 commit comments

Comments
 (0)