@objectstack/metadata 2.0.6 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +22 -0
- package/dist/index.d.mts +140 -20
- package/dist/index.d.ts +140 -20
- package/dist/index.js +547 -47
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +547 -47
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +24 -1
- package/src/loaders/filesystem-loader.ts +2 -2
- package/src/loaders/memory-loader.ts +1 -1
- package/src/loaders/remote-loader.ts +1 -1
- package/src/metadata-manager.ts +680 -49
- package/src/metadata-service.test.ts +611 -0
- package/src/metadata.test.ts +26 -27
- package/src/node-metadata-manager.ts +1 -1
- package/src/plugin.ts +23 -14
- package/vitest.config.ts +2 -0
package/src/metadata.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
2
|
|
|
3
3
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
4
|
-
import { MetadataManager
|
|
4
|
+
import { MetadataManager } from './metadata-manager';
|
|
5
5
|
import { MemoryLoader } from './loaders/memory-loader';
|
|
6
6
|
import type { MetadataLoader } from './loaders/loader-interface';
|
|
7
7
|
|
|
@@ -109,7 +109,7 @@ describe('MetadataManager', () => {
|
|
|
109
109
|
|
|
110
110
|
it('should throw when no writable loader is available', async () => {
|
|
111
111
|
const readOnlyLoader: MetadataLoader = {
|
|
112
|
-
contract: { name: 'readonly', protocol: '
|
|
112
|
+
contract: { name: 'readonly', protocol: 'memory:' as const, capabilities: { read: true, write: false, watch: false, list: true } },
|
|
113
113
|
load: vi.fn().mockResolvedValue({ data: null }),
|
|
114
114
|
loadMany: vi.fn().mockResolvedValue([]),
|
|
115
115
|
exists: vi.fn().mockResolvedValue(false),
|
|
@@ -148,14 +148,14 @@ describe('MetadataManager', () => {
|
|
|
148
148
|
|
|
149
149
|
describe('list', () => {
|
|
150
150
|
it('should return empty array for empty type', async () => {
|
|
151
|
-
const result = await manager.
|
|
151
|
+
const result = await manager.listNames('object');
|
|
152
152
|
expect(result).toEqual([]);
|
|
153
153
|
});
|
|
154
154
|
|
|
155
155
|
it('should list all items of a type', async () => {
|
|
156
156
|
await memoryLoader.save('object', 'account', {});
|
|
157
157
|
await memoryLoader.save('object', 'contact', {});
|
|
158
|
-
const result = await manager.
|
|
158
|
+
const result = await manager.listNames('object');
|
|
159
159
|
expect(result).toHaveLength(2);
|
|
160
160
|
expect(result).toContain('account');
|
|
161
161
|
expect(result).toContain('contact');
|
|
@@ -163,7 +163,7 @@ describe('MetadataManager', () => {
|
|
|
163
163
|
|
|
164
164
|
it('should deduplicate across loaders', async () => {
|
|
165
165
|
const loader1: MetadataLoader = {
|
|
166
|
-
contract: { name: 'l1', protocol: '
|
|
166
|
+
contract: { name: 'l1', protocol: 'memory:' as const, capabilities: { read: true, write: false, watch: false, list: true } },
|
|
167
167
|
load: vi.fn().mockResolvedValue({ data: null }),
|
|
168
168
|
loadMany: vi.fn().mockResolvedValue([]),
|
|
169
169
|
exists: vi.fn().mockResolvedValue(false),
|
|
@@ -171,7 +171,7 @@ describe('MetadataManager', () => {
|
|
|
171
171
|
list: vi.fn().mockResolvedValue(['account', 'contact']),
|
|
172
172
|
};
|
|
173
173
|
const loader2: MetadataLoader = {
|
|
174
|
-
contract: { name: 'l2', protocol: '
|
|
174
|
+
contract: { name: 'l2', protocol: 'memory:' as const, capabilities: { read: true, write: false, watch: false, list: true } },
|
|
175
175
|
load: vi.fn().mockResolvedValue({ data: null }),
|
|
176
176
|
loadMany: vi.fn().mockResolvedValue([]),
|
|
177
177
|
exists: vi.fn().mockResolvedValue(false),
|
|
@@ -180,7 +180,7 @@ describe('MetadataManager', () => {
|
|
|
180
180
|
};
|
|
181
181
|
|
|
182
182
|
const m = new MetadataManager({ formats: ['json'], loaders: [loader1, loader2] });
|
|
183
|
-
const result = await m.
|
|
183
|
+
const result = await m.listNames('object');
|
|
184
184
|
expect(result).toHaveLength(3);
|
|
185
185
|
expect(result).toContain('account');
|
|
186
186
|
expect(result).toContain('contact');
|
|
@@ -191,7 +191,7 @@ describe('MetadataManager', () => {
|
|
|
191
191
|
describe('watch / unwatch', () => {
|
|
192
192
|
it('should register and invoke watch callbacks', () => {
|
|
193
193
|
const callback = vi.fn();
|
|
194
|
-
manager.
|
|
194
|
+
(manager as any).addWatchCallback('object', callback);
|
|
195
195
|
|
|
196
196
|
// Trigger via protected method — cast to access it
|
|
197
197
|
(manager as any).notifyWatchers('object', {
|
|
@@ -207,8 +207,8 @@ describe('MetadataManager', () => {
|
|
|
207
207
|
|
|
208
208
|
it('should unwatch callback', () => {
|
|
209
209
|
const callback = vi.fn();
|
|
210
|
-
manager.
|
|
211
|
-
manager.
|
|
210
|
+
(manager as any).addWatchCallback('object', callback);
|
|
211
|
+
(manager as any).removeWatchCallback('object', callback);
|
|
212
212
|
|
|
213
213
|
(manager as any).notifyWatchers('object', {
|
|
214
214
|
type: 'changed',
|
|
@@ -222,7 +222,7 @@ describe('MetadataManager', () => {
|
|
|
222
222
|
});
|
|
223
223
|
|
|
224
224
|
it('should not throw when unwatching non-existent callback', () => {
|
|
225
|
-
expect(() => manager.
|
|
225
|
+
expect(() => (manager as any).removeWatchCallback('object', vi.fn())).not.toThrow();
|
|
226
226
|
});
|
|
227
227
|
});
|
|
228
228
|
|
|
@@ -269,7 +269,7 @@ describe('MemoryLoader', () => {
|
|
|
269
269
|
|
|
270
270
|
it('should have correct contract', () => {
|
|
271
271
|
expect(loader.contract.name).toBe('memory');
|
|
272
|
-
expect(loader.contract.protocol).toBe('memory');
|
|
272
|
+
expect(loader.contract.protocol).toBe('memory:');
|
|
273
273
|
expect(loader.contract.capabilities.read).toBe(true);
|
|
274
274
|
expect(loader.contract.capabilities.write).toBe(true);
|
|
275
275
|
});
|
|
@@ -339,24 +339,22 @@ describe('MetadataPlugin', () => {
|
|
|
339
339
|
loadMany = vi.fn().mockResolvedValue([]);
|
|
340
340
|
registerLoader = vi.fn();
|
|
341
341
|
stopWatching = vi.fn();
|
|
342
|
+
setTypeRegistry = vi.fn();
|
|
343
|
+
register = vi.fn();
|
|
342
344
|
};
|
|
343
345
|
return { NodeMetadataManager: MockNodeMetadataManager };
|
|
344
346
|
});
|
|
345
347
|
|
|
346
|
-
// Mock the spec import
|
|
347
|
-
vi.mock('@objectstack/spec', () => ({
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
apps: {},
|
|
353
|
-
views: {},
|
|
354
|
-
},
|
|
355
|
-
},
|
|
348
|
+
// Mock the spec kernel import
|
|
349
|
+
vi.mock('@objectstack/spec/kernel', () => ({
|
|
350
|
+
DEFAULT_METADATA_TYPE_REGISTRY: [
|
|
351
|
+
{ type: 'object', label: 'Object', filePatterns: ['**/*.object.ts'], supportsOverlay: true, allowRuntimeCreate: false, supportsVersioning: true, loadOrder: 10, domain: 'data' },
|
|
352
|
+
{ type: 'view', label: 'View', filePatterns: ['**/*.view.ts'], supportsOverlay: true, allowRuntimeCreate: true, supportsVersioning: false, loadOrder: 50, domain: 'ui' },
|
|
353
|
+
],
|
|
356
354
|
}));
|
|
357
355
|
|
|
358
356
|
it('should have correct plugin metadata', async () => {
|
|
359
|
-
const { MetadataPlugin } = await import('./plugin');
|
|
357
|
+
const { MetadataPlugin } = await import('./plugin.js');
|
|
360
358
|
const plugin = new MetadataPlugin({ rootDir: '/tmp/test', watch: false });
|
|
361
359
|
expect(plugin.name).toBe('com.objectstack.metadata');
|
|
362
360
|
expect(plugin.version).toBe('1.0.0');
|
|
@@ -364,7 +362,7 @@ describe('MetadataPlugin', () => {
|
|
|
364
362
|
});
|
|
365
363
|
|
|
366
364
|
it('should call init and register metadata service', async () => {
|
|
367
|
-
const { MetadataPlugin } = await import('./plugin');
|
|
365
|
+
const { MetadataPlugin } = await import('./plugin.js');
|
|
368
366
|
const plugin = new MetadataPlugin({ rootDir: '/tmp/test', watch: false });
|
|
369
367
|
|
|
370
368
|
const ctx = createMockPluginContext();
|
|
@@ -374,7 +372,7 @@ describe('MetadataPlugin', () => {
|
|
|
374
372
|
});
|
|
375
373
|
|
|
376
374
|
it('should call start and attempt to load metadata types', async () => {
|
|
377
|
-
const { MetadataPlugin } = await import('./plugin');
|
|
375
|
+
const { MetadataPlugin } = await import('./plugin.js');
|
|
378
376
|
const plugin = new MetadataPlugin({ rootDir: '/tmp/test', watch: false });
|
|
379
377
|
|
|
380
378
|
const ctx = createMockPluginContext();
|
|
@@ -390,7 +388,7 @@ describe('MetadataPlugin', () => {
|
|
|
390
388
|
|
|
391
389
|
function createMockLoader(name: string, data: any, shouldFail = false): MetadataLoader {
|
|
392
390
|
return {
|
|
393
|
-
contract: { name, protocol: '
|
|
391
|
+
contract: { name, protocol: 'memory:' as const, capabilities: { read: true, write: false, watch: false, list: true } },
|
|
394
392
|
load: shouldFail
|
|
395
393
|
? vi.fn().mockRejectedValue(new Error('loader failed'))
|
|
396
394
|
: vi.fn().mockResolvedValue({ data }),
|
|
@@ -403,7 +401,7 @@ function createMockLoader(name: string, data: any, shouldFail = false): Metadata
|
|
|
403
401
|
|
|
404
402
|
function createMockLoaderMany(name: string, items: any[], shouldFail = false): MetadataLoader {
|
|
405
403
|
return {
|
|
406
|
-
contract: { name, protocol: '
|
|
404
|
+
contract: { name, protocol: 'memory:' as const, capabilities: { read: true, write: false, watch: false, list: true } },
|
|
407
405
|
load: vi.fn().mockResolvedValue({ data: null }),
|
|
408
406
|
loadMany: shouldFail
|
|
409
407
|
? vi.fn().mockRejectedValue(new Error('loader failed'))
|
|
@@ -417,6 +415,7 @@ function createMockLoaderMany(name: string, items: any[], shouldFail = false): M
|
|
|
417
415
|
function createMockPluginContext() {
|
|
418
416
|
return {
|
|
419
417
|
registerService: vi.fn(),
|
|
418
|
+
replaceService: vi.fn(),
|
|
420
419
|
getService: vi.fn().mockReturnValue(null),
|
|
421
420
|
getServices: vi.fn().mockReturnValue(new Map()),
|
|
422
421
|
hook: vi.fn(),
|
package/src/plugin.ts
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import { Plugin, PluginContext } from '@objectstack/core';
|
|
4
4
|
import { NodeMetadataManager } from './node-metadata-manager.js';
|
|
5
|
-
import {
|
|
5
|
+
import { DEFAULT_METADATA_TYPE_REGISTRY } from '@objectstack/spec/kernel';
|
|
6
|
+
import type { MetadataPluginConfig } from '@objectstack/spec/kernel';
|
|
6
7
|
|
|
7
8
|
export interface MetadataPluginOptions {
|
|
8
9
|
rootDir?: string;
|
|
9
10
|
watch?: boolean;
|
|
11
|
+
config?: Partial<MetadataPluginConfig>;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
export class MetadataPlugin implements Plugin {
|
|
@@ -30,6 +32,9 @@ export class MetadataPlugin implements Plugin {
|
|
|
30
32
|
watch: this.options.watch ?? true,
|
|
31
33
|
formats: ['yaml', 'json', 'typescript', 'javascript']
|
|
32
34
|
});
|
|
35
|
+
|
|
36
|
+
// Initialize with default type registry
|
|
37
|
+
this.manager.setTypeRegistry(DEFAULT_METADATA_TYPE_REGISTRY);
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
init = async (ctx: PluginContext) => {
|
|
@@ -43,39 +48,43 @@ export class MetadataPlugin implements Plugin {
|
|
|
43
48
|
ctx.registerService('metadata', this.manager);
|
|
44
49
|
ctx.logger.info('MetadataPlugin providing metadata service (primary mode)', {
|
|
45
50
|
mode: 'file-system',
|
|
46
|
-
features: ['watch', 'persistence', 'multi-format']
|
|
51
|
+
features: ['watch', 'persistence', 'multi-format', 'query', 'overlay', 'type-registry']
|
|
47
52
|
});
|
|
48
53
|
}
|
|
49
54
|
|
|
50
55
|
start = async (ctx: PluginContext) => {
|
|
51
56
|
ctx.logger.info('Loading metadata from file system...');
|
|
52
57
|
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
.filter(key => key !== 'manifest'); // Manifest is handled separately
|
|
58
|
+
// Use the type registry to discover metadata types (sorted by loadOrder)
|
|
59
|
+
const sortedTypes = [...DEFAULT_METADATA_TYPE_REGISTRY]
|
|
60
|
+
.sort((a, b) => a.loadOrder - b.loadOrder);
|
|
57
61
|
|
|
58
62
|
let totalLoaded = 0;
|
|
59
|
-
for (const
|
|
63
|
+
for (const entry of sortedTypes) {
|
|
60
64
|
try {
|
|
61
|
-
|
|
62
|
-
const items = await this.manager.loadMany(type, {
|
|
65
|
+
const items = await this.manager.loadMany(entry.type, {
|
|
63
66
|
recursive: true
|
|
64
67
|
});
|
|
65
68
|
|
|
66
69
|
if (items.length > 0) {
|
|
67
|
-
|
|
68
|
-
|
|
70
|
+
// Register loaded items in the in-memory registry
|
|
71
|
+
for (const item of items) {
|
|
72
|
+
const meta = item as any;
|
|
73
|
+
if (meta?.name) {
|
|
74
|
+
await this.manager.register(entry.type, meta.name, item);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
ctx.logger.info(`Loaded ${items.length} ${entry.type} from file system`);
|
|
78
|
+
totalLoaded += items.length;
|
|
69
79
|
}
|
|
70
80
|
} catch (e: any) {
|
|
71
|
-
|
|
72
|
-
ctx.logger.debug(`No ${type} metadata found`, { error: e.message });
|
|
81
|
+
ctx.logger.debug(`No ${entry.type} metadata found`, { error: e.message });
|
|
73
82
|
}
|
|
74
83
|
}
|
|
75
84
|
|
|
76
85
|
ctx.logger.info('Metadata loading complete', {
|
|
77
86
|
totalItems: totalLoaded,
|
|
78
|
-
|
|
87
|
+
registeredTypes: sortedTypes.length,
|
|
79
88
|
});
|
|
80
89
|
}
|
|
81
90
|
}
|
package/vitest.config.ts
CHANGED
|
@@ -7,6 +7,8 @@ export default defineConfig({
|
|
|
7
7
|
resolve: {
|
|
8
8
|
alias: {
|
|
9
9
|
'@objectstack/core': path.resolve(__dirname, '../core/src/index.ts'),
|
|
10
|
+
'@objectstack/spec/contracts': path.resolve(__dirname, '../spec/src/contracts/index.ts'),
|
|
11
|
+
'@objectstack/spec/kernel': path.resolve(__dirname, '../spec/src/kernel/index.ts'),
|
|
10
12
|
'@objectstack/spec/system': path.resolve(__dirname, '../spec/src/system/index.ts'),
|
|
11
13
|
'@objectstack/spec': path.resolve(__dirname, '../spec/src/index.ts'),
|
|
12
14
|
'@objectstack/types': path.resolve(__dirname, '../types/src/index.ts'),
|