@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.
@@ -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, type MetadataManagerOptions } from './metadata-manager';
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: 'test', capabilities: { read: true, write: false, watch: false, list: true } },
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.list('object');
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.list('object');
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: 'test', capabilities: { read: true, write: false, watch: false, list: true } },
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: 'test', capabilities: { read: true, write: false, watch: false, list: true } },
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.list('object');
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.watch('object', callback);
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.watch('object', callback);
211
- manager.unwatch('object', callback);
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.unwatch('object', vi.fn())).not.toThrow();
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
- ObjectStackDefinitionSchema: {
349
- shape: {
350
- manifest: {},
351
- objects: {},
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: 'test', capabilities: { read: true, write: false, watch: false, list: true } },
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: 'test', capabilities: { read: true, write: false, watch: false, list: true } },
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(),
@@ -118,7 +118,7 @@ export class NodeMetadataManager extends MetadataManager {
118
118
  name,
119
119
  path: filePath,
120
120
  data,
121
- timestamp: new Date(),
121
+ timestamp: new Date().toISOString(),
122
122
  };
123
123
 
124
124
  this.notifyWatchers(type, event);
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 { ObjectStackDefinitionSchema } from '@objectstack/spec';
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
- // Define metadata types directly from the Protocol Definition
54
- // This ensures the loader is always in sync with the Spec
55
- const metadataTypes = Object.keys(ObjectStackDefinitionSchema.shape)
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 type of metadataTypes) {
63
+ for (const entry of sortedTypes) {
60
64
  try {
61
- // Try to load metadata of this type
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
- ctx.logger.info(`Loaded ${items.length} ${type} from file system`);
68
- totalLoaded += items.length;
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
- // Ignore missing directories or errors
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
- note: 'ObjectQL will sync these into its registry during its start phase'
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'),