Skip to content

Commit 3992a69

Browse files
authored
Merge pull request #619 from objectstack-ai/copilot/upgrade-metadata-datasource
2 parents 01cae69 + 19ba759 commit 3992a69

8 files changed

Lines changed: 430 additions & 90 deletions

File tree

packages/objectql/src/metadata-facade.ts

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,18 @@ import { SchemaRegistry } from './registry.js';
88
* Provides a clean, injectable interface over SchemaRegistry.
99
* Registered as the 'metadata' kernel service to eliminate
1010
* downstream packages needing to manually wrap SchemaRegistry.
11+
*
12+
* Implements the async IMetadataService interface.
13+
* Internally delegates to SchemaRegistry (in-memory) with Promise wrappers.
1114
*/
1215
export class MetadataFacade {
1316
/**
1417
* Register a metadata item
1518
*/
16-
register(type: string, definition: any): void {
19+
async register(type: string, name: string, data: any): Promise<void> {
20+
const definition = typeof data === 'object' && data !== null
21+
? { ...data, name: data.name ?? name }
22+
: data;
1723
if (type === 'object') {
1824
SchemaRegistry.registerItem(type, definition, 'name' as any);
1925
} else {
@@ -24,7 +30,7 @@ export class MetadataFacade {
2430
/**
2531
* Get a metadata item by type and name
2632
*/
27-
get(type: string, name: string): any {
33+
async get(type: string, name: string): Promise<any> {
2834
const item = SchemaRegistry.getItem(type, name) as any;
2935
return item?.content ?? item;
3036
}
@@ -39,36 +45,52 @@ export class MetadataFacade {
3945
/**
4046
* List all items of a type
4147
*/
42-
list(type: string): any[] {
48+
async list(type: string): Promise<any[]> {
4349
const items = SchemaRegistry.listItems(type);
4450
return items.map((item: any) => item?.content ?? item);
4551
}
4652

4753
/**
4854
* Unregister a metadata item
4955
*/
50-
unregister(type: string, name: string): void {
56+
async unregister(type: string, name: string): Promise<void> {
5157
SchemaRegistry.unregisterItem(type, name);
5258
}
5359

60+
/**
61+
* Check if a metadata item exists
62+
*/
63+
async exists(type: string, name: string): Promise<boolean> {
64+
const item = SchemaRegistry.getItem(type, name);
65+
return item !== undefined && item !== null;
66+
}
67+
68+
/**
69+
* List all names of metadata items of a given type
70+
*/
71+
async listNames(type: string): Promise<string[]> {
72+
const items = SchemaRegistry.listItems(type);
73+
return items.map((item: any) => item?.name ?? item?.content?.name ?? '').filter(Boolean);
74+
}
75+
5476
/**
5577
* Unregister all metadata from a package
5678
*/
57-
unregisterPackage(packageName: string): void {
79+
async unregisterPackage(packageName: string): Promise<void> {
5880
SchemaRegistry.unregisterObjectsByPackage(packageName);
5981
}
6082

6183
/**
6284
* Convenience: get object definition
6385
*/
64-
getObject(name: string): any {
86+
async getObject(name: string): Promise<any> {
6587
return SchemaRegistry.getObject(name);
6688
}
6789

6890
/**
6991
* Convenience: list all objects
7092
*/
71-
listObjects(): any[] {
93+
async listObjects(): Promise<any[]> {
7294
return SchemaRegistry.getAllObjects();
7395
}
7496
}

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

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -276,16 +276,18 @@ function createMetadataStub() {
276276
const store = new Map<string, Map<string, unknown>>(); // type → (name → def)
277277
return {
278278
_dev: true, _serviceName: 'metadata',
279-
register(type: string, definition: any) {
279+
async register(type: string, name: string, data: unknown) {
280280
if (!store.has(type)) store.set(type, new Map());
281-
store.get(type)!.set(definition.name ?? '', definition);
281+
store.get(type)!.set(name, data);
282282
},
283-
get(type: string, name: string) { return store.get(type)?.get(name); },
284-
list(type: string) { return [...(store.get(type)?.values() ?? [])]; },
285-
unregister(type: string, name: string) { store.get(type)?.delete(name); },
286-
getObject(name: string) { return store.get('object')?.get(name); },
287-
listObjects() { return [...(store.get('object')?.values() ?? [])]; },
288-
unregisterPackage() {},
283+
async get(type: string, name: string) { return store.get(type)?.get(name); },
284+
async list(type: string) { return [...(store.get(type)?.values() ?? [])]; },
285+
async unregister(type: string, name: string) { store.get(type)?.delete(name); },
286+
async exists(type: string, name: string) { return store.get(type)?.has(name) ?? false; },
287+
async listNames(type: string) { return [...(store.get(type)?.keys() ?? [])]; },
288+
async getObject(name: string) { return store.get('object')?.get(name); },
289+
async listObjects() { return [...(store.get('object')?.values() ?? [])]; },
290+
async unregisterPackage() {},
289291
};
290292
}
291293

packages/spec/src/contracts/metadata-service.test.ts

Lines changed: 55 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -4,85 +4,97 @@ import type { IMetadataService } from './metadata-service';
44
describe('Metadata Service Contract', () => {
55
it('should allow a minimal IMetadataService implementation with required methods', () => {
66
const service: IMetadataService = {
7-
register: (_type, _definition) => {},
8-
get: (_type, _name) => undefined,
9-
list: (_type) => [],
10-
unregister: (_type, _name) => {},
11-
getObject: (_name) => undefined,
12-
listObjects: () => [],
7+
register: async (_type, _name, _data) => {},
8+
get: async (_type, _name) => undefined,
9+
list: async (_type) => [],
10+
unregister: async (_type, _name) => {},
11+
exists: async (_type, _name) => false,
12+
listNames: async (_type) => [],
13+
getObject: async (_name) => undefined,
14+
listObjects: async () => [],
1315
};
1416

1517
expect(typeof service.register).toBe('function');
1618
expect(typeof service.get).toBe('function');
1719
expect(typeof service.list).toBe('function');
1820
expect(typeof service.unregister).toBe('function');
21+
expect(typeof service.exists).toBe('function');
22+
expect(typeof service.listNames).toBe('function');
1923
expect(typeof service.getObject).toBe('function');
2024
expect(typeof service.listObjects).toBe('function');
2125
});
2226

2327
it('should allow a full implementation with optional methods', () => {
2428
const service: IMetadataService = {
25-
register: () => {},
26-
get: () => undefined,
27-
list: () => [],
28-
unregister: () => {},
29-
getObject: () => undefined,
30-
listObjects: () => [],
31-
unregisterPackage: (_packageName) => {},
29+
register: async () => {},
30+
get: async () => undefined,
31+
list: async () => [],
32+
unregister: async () => {},
33+
exists: async () => false,
34+
listNames: async () => [],
35+
getObject: async () => undefined,
36+
listObjects: async () => [],
37+
unregisterPackage: async (_packageName) => {},
3238
};
3339

3440
expect(service.unregisterPackage).toBeDefined();
3541
});
3642

37-
it('should register and retrieve metadata items', () => {
43+
it('should register and retrieve metadata items asynchronously', async () => {
3844
const store = new Map<string, Map<string, unknown>>();
3945

4046
const service: IMetadataService = {
41-
register: (type, definition) => {
47+
register: async (type, name, data) => {
4248
if (!store.has(type)) store.set(type, new Map());
43-
const def = definition as { name: string };
44-
store.get(type)!.set(def.name, definition);
49+
store.get(type)!.set(name, data);
4550
},
46-
get: (type, name) => store.get(type)?.get(name),
47-
list: (type) => Array.from(store.get(type)?.values() ?? []),
48-
unregister: (type, name) => { store.get(type)?.delete(name); },
49-
getObject: (name) => store.get('object')?.get(name),
50-
listObjects: () => Array.from(store.get('object')?.values() ?? []),
51+
get: async (type, name) => store.get(type)?.get(name),
52+
list: async (type) => Array.from(store.get(type)?.values() ?? []),
53+
unregister: async (type, name) => { store.get(type)?.delete(name); },
54+
exists: async (type, name) => store.get(type)?.has(name) ?? false,
55+
listNames: async (type) => Array.from(store.get(type)?.keys() ?? []),
56+
getObject: async (name) => store.get('object')?.get(name),
57+
listObjects: async () => Array.from(store.get('object')?.values() ?? []),
5158
};
5259

5360
const objectDef = { name: 'account', label: 'Account', fields: {} };
54-
service.register('object', objectDef);
61+
await service.register('object', 'account', objectDef);
5562

56-
expect(service.get('object', 'account')).toEqual(objectDef);
57-
expect(service.getObject('account')).toEqual(objectDef);
58-
expect(service.listObjects()).toHaveLength(1);
63+
expect(await service.get('object', 'account')).toEqual(objectDef);
64+
expect(await service.getObject('account')).toEqual(objectDef);
65+
expect(await service.listObjects()).toHaveLength(1);
66+
expect(await service.exists('object', 'account')).toBe(true);
67+
expect(await service.listNames('object')).toEqual(['account']);
5968

60-
service.unregister('object', 'account');
61-
expect(service.get('object', 'account')).toBeUndefined();
69+
await service.unregister('object', 'account');
70+
expect(await service.get('object', 'account')).toBeUndefined();
71+
expect(await service.exists('object', 'account')).toBe(false);
6272
});
6373

64-
it('should list items by type', () => {
74+
it('should list items by type', async () => {
6575
const store = new Map<string, Map<string, unknown>>();
6676

6777
const service: IMetadataService = {
68-
register: (type, definition) => {
78+
register: async (type, name, data) => {
6979
if (!store.has(type)) store.set(type, new Map());
70-
const def = definition as { name: string };
71-
store.get(type)!.set(def.name, definition);
80+
store.get(type)!.set(name, data);
7281
},
73-
get: (type, name) => store.get(type)?.get(name),
74-
list: (type) => Array.from(store.get(type)?.values() ?? []),
75-
unregister: (type, name) => { store.get(type)?.delete(name); },
76-
getObject: (name) => store.get('object')?.get(name),
77-
listObjects: () => Array.from(store.get('object')?.values() ?? []),
82+
get: async (type, name) => store.get(type)?.get(name),
83+
list: async (type) => Array.from(store.get(type)?.values() ?? []),
84+
unregister: async (type, name) => { store.get(type)?.delete(name); },
85+
exists: async (type, name) => store.get(type)?.has(name) ?? false,
86+
listNames: async (type) => Array.from(store.get(type)?.keys() ?? []),
87+
getObject: async (name) => store.get('object')?.get(name),
88+
listObjects: async () => Array.from(store.get('object')?.values() ?? []),
7889
};
7990

80-
service.register('object', { name: 'account', label: 'Account' });
81-
service.register('object', { name: 'contact', label: 'Contact' });
82-
service.register('view', { name: 'account_list', label: 'Account List' });
91+
await service.register('object', 'account', { name: 'account', label: 'Account' });
92+
await service.register('object', 'contact', { name: 'contact', label: 'Contact' });
93+
await service.register('view', 'account_list', { name: 'account_list', label: 'Account List' });
8394

84-
expect(service.list('object')).toHaveLength(2);
85-
expect(service.list('view')).toHaveLength(1);
86-
expect(service.list('flow')).toHaveLength(0);
95+
expect(await service.list('object')).toHaveLength(2);
96+
expect(await service.list('view')).toHaveLength(1);
97+
expect(await service.list('flow')).toHaveLength(0);
98+
expect(await service.listNames('object')).toEqual(['account', 'contact']);
8799
});
88100
});

packages/spec/src/contracts/metadata-service.ts

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
/**
44
* IMetadataService - Metadata Service Contract
55
*
6-
* Defines the interface for managing object/field definitions in ObjectStack.
6+
* Defines the async interface for managing object/field definitions in ObjectStack.
77
* Concrete implementations (SchemaRegistry, Database-backed, etc.)
88
* should implement this interface.
99
*
10+
* All methods are async to support database-backed persistence via datasource.
11+
*
1012
* Follows Dependency Inversion Principle - plugins depend on this interface,
1113
* not on concrete metadata storage implementations.
1214
*
@@ -15,50 +17,66 @@
1517

1618
export interface IMetadataService {
1719
/**
18-
* Register a metadata item by type
20+
* Register/save a metadata item by type
1921
* @param type - Metadata type (e.g. 'object', 'view', 'flow')
20-
* @param definition - The metadata definition to register
22+
* @param name - Item name/identifier (snake_case)
23+
* @param data - The metadata definition to register
2124
*/
22-
register(type: string, definition: unknown): void;
25+
register(type: string, name: string, data: unknown): Promise<void>;
2326

2427
/**
2528
* Get a metadata item by type and name
2629
* @param type - Metadata type
2730
* @param name - Item name/identifier
2831
* @returns The metadata definition, or undefined if not found
2932
*/
30-
get(type: string, name: string): unknown | undefined;
33+
get(type: string, name: string): Promise<unknown | undefined>;
3134

3235
/**
3336
* List all metadata items of a given type
3437
* @param type - Metadata type
3538
* @returns Array of metadata definitions
3639
*/
37-
list(type: string): unknown[];
40+
list(type: string): Promise<unknown[]>;
41+
42+
/**
43+
* Unregister/remove a metadata item by type and name
44+
* @param type - Metadata type
45+
* @param name - Item name/identifier
46+
*/
47+
unregister(type: string, name: string): Promise<void>;
3848

3949
/**
40-
* Unregister a metadata item by type and name
50+
* Check if a metadata item exists
4151
* @param type - Metadata type
4252
* @param name - Item name/identifier
53+
* @returns True if the item exists
54+
*/
55+
exists(type: string, name: string): Promise<boolean>;
56+
57+
/**
58+
* List all names of metadata items of a given type
59+
* @param type - Metadata type
60+
* @returns Array of item names
4361
*/
44-
unregister(type: string, name: string): void;
62+
listNames(type: string): Promise<string[]>;
4563

4664
/**
4765
* Convenience: get an object definition by name
4866
* @param name - Object name (snake_case)
4967
* @returns The object definition, or undefined if not found
5068
*/
51-
getObject(name: string): unknown | undefined;
69+
getObject(name: string): Promise<unknown | undefined>;
5270

5371
/**
5472
* Convenience: list all object definitions
5573
* @returns Array of object definitions
5674
*/
57-
listObjects(): unknown[];
75+
listObjects(): Promise<unknown[]>;
5876

5977
/**
6078
* Unregister all metadata items from a specific package
6179
* @param packageName - The package name whose items should be removed
6280
*/
63-
unregisterPackage?(packageName: string): void;
81+
unregisterPackage?(packageName: string): Promise<void>;
6482
}

0 commit comments

Comments
 (0)