Skip to content

Commit 1465eb2

Browse files
committed
feat: implement manifest service for plugin registration and update tests for namespace handling
1 parent 89e707b commit 1465eb2

13 files changed

Lines changed: 143 additions & 136 deletions

File tree

.cursorrules.d/objectstack.prompt.md

Lines changed: 0 additions & 87 deletions
This file was deleted.

packages/metadata/src/plugin.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,20 @@ export class MetadataPlugin implements Plugin {
4848
// This takes precedence over ObjectQL's fallback metadata service
4949
ctx.registerService('metadata', this.manager);
5050

51-
// Register metadata system objects so ObjectQLPlugin auto-discovers them
52-
ctx.registerService('app.com.objectstack.metadata', {
53-
id: 'com.objectstack.metadata',
54-
name: 'Metadata',
55-
version: '1.0.0',
56-
type: 'plugin',
57-
namespace: 'sys',
58-
objects: [SysMetadataObject],
59-
});
51+
// Register metadata system objects via the manifest service (if available).
52+
// MetadataPlugin may init before ObjectQLPlugin, so wrap in try/catch.
53+
try {
54+
ctx.getService<{ register(m: any): void }>('manifest').register({
55+
id: 'com.objectstack.metadata',
56+
name: 'Metadata',
57+
version: '1.0.0',
58+
type: 'plugin',
59+
namespace: 'sys',
60+
objects: [SysMetadataObject],
61+
});
62+
} catch {
63+
// ObjectQL not loaded yet — objects will be discovered via legacy fallback
64+
}
6065

6166
ctx.logger.info('MetadataPlugin providing metadata service (primary mode)', {
6267
mode: 'file-system',

packages/objectql/src/plugin.integration.test.ts

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ describe('ObjectQLPlugin - Metadata Service Integration', () => {
6565
});
6666

6767
describe('Service Registration', () => {
68-
it('should register objectql, data, and protocol services', async () => {
68+
it('should register manifest service', async () => {
6969
// Arrange
7070
const plugin = new ObjectQLPlugin();
7171
await kernel.use(plugin);
@@ -77,6 +77,7 @@ describe('ObjectQLPlugin - Metadata Service Integration', () => {
7777
expect(kernel.getService('objectql')).toBeDefined();
7878
expect(kernel.getService('data')).toBeDefined();
7979
expect(kernel.getService('protocol')).toBeDefined();
80+
expect(kernel.getService('manifest')).toBeDefined();
8081
});
8182

8283
it('should respect existing metadata service', async () => {
@@ -146,8 +147,71 @@ describe('ObjectQLPlugin - Metadata Service Integration', () => {
146147
expect(objectql.drivers?.has('mock-driver')).toBe(true);
147148
});
148149

149-
it('should discover and register apps from kernel services', async () => {
150+
it('should register apps via manifest service', async () => {
150151
// Arrange
152+
const plugin = new ObjectQLPlugin();
153+
await kernel.use(plugin);
154+
155+
// Plugin that uses the manifest service directly
156+
await kernel.use({
157+
name: 'mock-app-plugin',
158+
type: 'app',
159+
version: '1.0.0',
160+
dependencies: ['com.objectstack.engine.objectql'],
161+
init: async (ctx) => {
162+
ctx.getService<{ register(m: any): void }>('manifest').register({
163+
id: 'test-app',
164+
name: 'test_app',
165+
version: '1.0.0',
166+
type: 'app',
167+
apps: [{ name: 'Test App' }],
168+
});
169+
}
170+
});
171+
172+
// Act
173+
await kernel.bootstrap();
174+
175+
// Assert
176+
const objectql = kernel.getService('objectql') as any;
177+
expect(objectql.registry).toBeDefined();
178+
const apps = objectql.registry.getAllApps();
179+
expect(apps.some((a: any) => a.name === 'Test App')).toBe(true);
180+
});
181+
182+
it('should register manifests from start() phase via manifest service', async () => {
183+
// Arrange — simulates SetupPlugin's pattern (registers in start, not init)
184+
const plugin = new ObjectQLPlugin();
185+
await kernel.use(plugin);
186+
187+
await kernel.use({
188+
name: 'late-registerer',
189+
type: 'standard',
190+
version: '1.0.0',
191+
dependencies: ['com.objectstack.engine.objectql'],
192+
init: async () => {},
193+
start: async (ctx) => {
194+
ctx.getService<{ register(m: any): void }>('manifest').register({
195+
id: 'late-app',
196+
name: 'late_app',
197+
version: '1.0.0',
198+
type: 'plugin',
199+
apps: [{ name: 'Late App' }],
200+
});
201+
}
202+
});
203+
204+
// Act
205+
await kernel.bootstrap();
206+
207+
// Assert
208+
const objectql = kernel.getService('objectql') as any;
209+
const apps = objectql.registry.getAllApps();
210+
expect(apps.some((a: any) => a.name === 'Late App')).toBe(true);
211+
});
212+
213+
it('should still discover apps registered via legacy app.* convention', async () => {
214+
// Arrange — legacy pattern for backward compatibility
151215
const mockApp = {
152216
manifest: {
153217
id: 'test-app',
@@ -172,9 +236,8 @@ describe('ObjectQLPlugin - Metadata Service Integration', () => {
172236
// Act
173237
await kernel.bootstrap();
174238

175-
// Assert
239+
// Assert — legacy pattern still works
176240
const objectql = kernel.getService('objectql') as any;
177-
// App should be registered (check via registry or apps list)
178241
expect(objectql.registry).toBeDefined();
179242
});
180243
});

packages/objectql/src/plugin.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,22 @@ export class ObjectQLPlugin implements Plugin {
6868
}
6969

7070
ctx.registerService('data', this.ql); // ObjectQL implements IDataEngine
71-
72-
ctx.logger.info('ObjectQL engine registered', {
73-
services: ['objectql', 'data'],
71+
72+
// Register manifest service for direct app/package registration.
73+
// Plugins call ctx.getService('manifest').register(manifestData)
74+
// instead of the legacy ctx.registerService('app.<id>', manifestData) convention.
75+
const ql = this.ql;
76+
ctx.registerService('manifest', {
77+
register: (manifest: any) => {
78+
ql.registerApp(manifest);
79+
ctx.logger.debug('Manifest registered via manifest service', {
80+
id: manifest.id || manifest.name
81+
});
82+
}
83+
});
84+
85+
ctx.logger.info('ObjectQL engine registered', {
86+
services: ['objectql', 'data', 'manifest'],
7487
metadataProvider: metadataProvider
7588
});
7689

@@ -109,9 +122,13 @@ export class ObjectQLPlugin implements Plugin {
109122
ctx.logger.debug('Discovered and registered driver service', { serviceName: name });
110123
}
111124
if (name.startsWith('app.')) {
112-
// Register App
125+
// Legacy fallback: discover app.* services (DEPRECATED)
126+
ctx.logger.warn(
127+
`[DEPRECATED] Service "${name}" uses legacy app.* convention. ` +
128+
`Migrate to ctx.getService('manifest').register(data).`
129+
);
113130
this.ql.registerApp(service); // service is Manifest
114-
ctx.logger.debug('Discovered and registered app service', { serviceName: name });
131+
ctx.logger.debug('Discovered and registered app service (legacy)', { serviceName: name });
115132
}
116133
}
117134
}

packages/objectql/src/registry.test.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,18 +53,29 @@ describe('SchemaRegistry', () => {
5353
}).not.toThrow();
5454
});
5555

56-
it('should throw on namespace conflict', () => {
57-
SchemaRegistry.registerNamespace('crm', 'com.example.crm');
58-
expect(() => {
59-
SchemaRegistry.registerNamespace('crm', 'com.other.crm');
60-
}).toThrow(/already registered/);
56+
it('should allow multiple packages to share a namespace', () => {
57+
SchemaRegistry.registerNamespace('sys', 'com.objectstack.auth');
58+
SchemaRegistry.registerNamespace('sys', 'com.objectstack.security');
59+
// First registered package returned for backwards compat
60+
expect(SchemaRegistry.getNamespaceOwner('sys')).toBe('com.objectstack.auth');
61+
expect(SchemaRegistry.getNamespaceOwners('sys')).toEqual([
62+
'com.objectstack.auth',
63+
'com.objectstack.security',
64+
]);
6165
});
6266

6367
it('should unregister namespace', () => {
6468
SchemaRegistry.registerNamespace('crm', 'com.example.crm');
6569
SchemaRegistry.unregisterNamespace('crm', 'com.example.crm');
6670
expect(SchemaRegistry.getNamespaceOwner('crm')).toBeUndefined();
6771
});
72+
73+
it('should keep namespace when one of multiple packages unregisters', () => {
74+
SchemaRegistry.registerNamespace('sys', 'com.objectstack.auth');
75+
SchemaRegistry.registerNamespace('sys', 'com.objectstack.setup');
76+
SchemaRegistry.unregisterNamespace('sys', 'com.objectstack.setup');
77+
expect(SchemaRegistry.getNamespaceOwner('sys')).toBe('com.objectstack.auth');
78+
});
6879
});
6980

7081
// ==========================================

packages/plugins/plugin-audit/src/audit-plugin.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ export class AuditPlugin implements Plugin {
1313
name = 'com.objectstack.audit';
1414
type = 'standard';
1515
version = '1.0.0';
16+
dependencies = ['com.objectstack.engine.objectql'];
1617

1718
async init(ctx: PluginContext): Promise<void> {
18-
// Register audit system objects so ObjectQLPlugin auto-discovers them
19-
ctx.registerService('app.com.objectstack.audit', {
19+
// Register audit system objects via the manifest service.
20+
ctx.getService<{ register(m: any): void }>('manifest').register({
2021
id: 'com.objectstack.audit',
2122
name: 'Audit',
2223
version: '1.0.0',

packages/plugins/plugin-auth/src/auth-plugin.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export class AuthPlugin implements Plugin {
6060
name = 'com.objectstack.auth';
6161
type = 'standard';
6262
version = '1.0.0';
63-
dependencies: string[] = []; // HTTP server is optional; routes are registered only when available
63+
dependencies: string[] = ['com.objectstack.engine.objectql']; // manifest service required
6464

6565
private options: AuthPluginOptions;
6666
private authManager: AuthManager | null = null;
@@ -96,9 +96,8 @@ export class AuthPlugin implements Plugin {
9696
// Register auth service
9797
ctx.registerService('auth', this.authManager);
9898

99-
// Register system objects as an app service so ObjectQLPlugin
100-
// auto-discovers them via the `app.*` convention.
101-
ctx.registerService('app.com.objectstack.system', {
99+
// Register system objects via the manifest service.
100+
ctx.getService<{ register(m: any): void }>('manifest').register({
102101
id: 'com.objectstack.system',
103102
name: 'System',
104103
version: '1.0.0',

packages/plugins/plugin-security/src/security-plugin.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ export class SecurityPlugin implements Plugin {
3838
ctx.registerService('security.rls', this.rlsCompiler);
3939
ctx.registerService('security.fieldMasker', this.fieldMasker);
4040

41-
// Register security system objects so ObjectQLPlugin auto-discovers them
42-
ctx.registerService('app.com.objectstack.security', {
41+
// Register security system objects via the manifest service.
42+
ctx.getService<{ register(m: any): void }>('manifest').register({
4343
id: 'com.objectstack.security',
4444
name: 'Security',
4545
version: '1.0.0',

packages/plugins/plugin-setup/src/setup-plugin.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export class SetupPlugin implements Plugin {
4040
name = 'com.objectstack.setup';
4141
type = 'standard';
4242
version = '1.0.0';
43+
dependencies = ['com.objectstack.engine.objectql'];
4344

4445
/** Accumulated contributions from other plugins. */
4546
private contributions: SetupNavContribution[] = [];
@@ -75,9 +76,8 @@ export class SetupPlugin implements Plugin {
7576
areas: areas.length > 0 ? areas : undefined,
7677
};
7778

78-
// Register the finalized Setup App as an internal platform app
79-
// following the `app.<id>` service convention used by ObjectQLPlugin.
80-
ctx.registerService('app.com.objectstack.setup', {
79+
// Register the finalized Setup App via the manifest service.
80+
ctx.getService<{ register(m: any): void }>('manifest').register({
8181
id: 'com.objectstack.setup',
8282
name: 'Setup',
8383
version: '1.0.0',

packages/runtime/src/app-plugin.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,13 @@ export class AppPlugin implements Plugin {
4141
version: this.version
4242
});
4343

44-
// Register the app manifest as a service
45-
// ObjectQLPlugin will discover this and call ql.registerApp()
46-
const serviceName = `app.${appId}`;
47-
48-
// Merge manifest with the bundle to ensure objects/apps are accessible at root
49-
// This supports both Legacy Manifests and new Stack Definitions
50-
const servicePayload = this.bundle.manifest
44+
// Register the app manifest directly via the manifest service.
45+
// This immediately decomposes the manifest into SchemaRegistry entries.
46+
const servicePayload = this.bundle.manifest
5147
? { ...this.bundle.manifest, ...this.bundle }
5248
: this.bundle;
5349

54-
ctx.registerService(serviceName, servicePayload);
50+
ctx.getService<{ register(m: any): void }>('manifest').register(servicePayload);
5551
}
5652

5753
start = async (ctx: PluginContext) => {

0 commit comments

Comments
 (0)