@objectstack/metadata 3.3.0 → 4.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/dist/index.cjs +2197 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.js +42 -82
- package/dist/index.js.map +1 -1
- package/dist/node.cjs +2201 -0
- package/dist/node.cjs.map +1 -0
- package/dist/node.d.cts +65 -0
- package/dist/node.d.ts +65 -0
- package/dist/{index.mjs → node.js} +3 -1
- package/package.json +22 -17
- package/.turbo/turbo-build.log +0 -22
- package/CHANGELOG.md +0 -504
- package/ROADMAP.md +0 -224
- package/src/index.ts +0 -68
- package/src/loaders/database-loader.test.ts +0 -559
- package/src/loaders/database-loader.ts +0 -352
- package/src/loaders/filesystem-loader.ts +0 -420
- package/src/loaders/loader-interface.ts +0 -89
- package/src/loaders/memory-loader.ts +0 -103
- package/src/loaders/remote-loader.ts +0 -140
- package/src/metadata-manager.ts +0 -1168
- package/src/metadata-service.test.ts +0 -965
- package/src/metadata.test.ts +0 -431
- package/src/migration/executor.ts +0 -54
- package/src/migration/index.ts +0 -3
- package/src/node-metadata-manager.ts +0 -126
- package/src/node.ts +0 -11
- package/src/objects/sys-metadata.object.ts +0 -188
- package/src/plugin.ts +0 -102
- package/src/serializers/json-serializer.ts +0 -73
- package/src/serializers/serializer-interface.ts +0 -65
- package/src/serializers/serializers.test.ts +0 -74
- package/src/serializers/typescript-serializer.ts +0 -127
- package/src/serializers/yaml-serializer.ts +0 -49
- package/tsconfig.json +0 -9
- package/vitest.config.ts +0 -23
- /package/dist/{index.d.mts → index.d.cts} +0 -0
- /package/dist/{index.mjs.map → node.js.map} +0 -0
package/src/metadata.test.ts
DELETED
|
@@ -1,431 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
4
|
-
import { MetadataManager } from './metadata-manager';
|
|
5
|
-
import { MemoryLoader } from './loaders/memory-loader';
|
|
6
|
-
import type { MetadataLoader } from './loaders/loader-interface';
|
|
7
|
-
|
|
8
|
-
// Suppress logger output during tests
|
|
9
|
-
vi.mock('@objectstack/core', () => ({
|
|
10
|
-
createLogger: () => ({
|
|
11
|
-
info: vi.fn(),
|
|
12
|
-
warn: vi.fn(),
|
|
13
|
-
error: vi.fn(),
|
|
14
|
-
debug: vi.fn(),
|
|
15
|
-
}),
|
|
16
|
-
}));
|
|
17
|
-
|
|
18
|
-
// ---------- MetadataManager ----------
|
|
19
|
-
|
|
20
|
-
describe('MetadataManager', () => {
|
|
21
|
-
let manager: MetadataManager;
|
|
22
|
-
let memoryLoader: MemoryLoader;
|
|
23
|
-
|
|
24
|
-
beforeEach(() => {
|
|
25
|
-
memoryLoader = new MemoryLoader();
|
|
26
|
-
manager = new MetadataManager({
|
|
27
|
-
formats: ['json'],
|
|
28
|
-
loaders: [memoryLoader],
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
describe('load', () => {
|
|
33
|
-
it('should return null when item does not exist', async () => {
|
|
34
|
-
const result = await manager.load('object', 'nonexistent');
|
|
35
|
-
expect(result).toBeNull();
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('should return data from a loader', async () => {
|
|
39
|
-
await memoryLoader.save('object', 'account', { name: 'account', label: 'Account' });
|
|
40
|
-
const result = await manager.load('object', 'account');
|
|
41
|
-
expect(result).toEqual({ name: 'account', label: 'Account' });
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('should try loaders in order and return first result', async () => {
|
|
45
|
-
const loader1 = createMockLoader('first', { name: 'from_first' });
|
|
46
|
-
const loader2 = createMockLoader('second', { name: 'from_second' });
|
|
47
|
-
|
|
48
|
-
const m = new MetadataManager({ formats: ['json'], loaders: [loader1, loader2] });
|
|
49
|
-
const result = await m.load('object', 'test');
|
|
50
|
-
expect(result).toEqual({ name: 'from_first' });
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('should skip failing loaders and try the next', async () => {
|
|
54
|
-
const failingLoader = createMockLoader('failing', null, true);
|
|
55
|
-
const goodLoader = createMockLoader('good', { name: 'ok' });
|
|
56
|
-
|
|
57
|
-
const m = new MetadataManager({ formats: ['json'], loaders: [failingLoader, goodLoader] });
|
|
58
|
-
const result = await m.load('object', 'test');
|
|
59
|
-
expect(result).toEqual({ name: 'ok' });
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
describe('loadMany', () => {
|
|
64
|
-
it('should return empty array when nothing loaded', async () => {
|
|
65
|
-
const result = await manager.loadMany('object');
|
|
66
|
-
expect(result).toEqual([]);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('should return all items from a loader', async () => {
|
|
70
|
-
await memoryLoader.save('object', 'account', { name: 'account' });
|
|
71
|
-
await memoryLoader.save('object', 'contact', { name: 'contact' });
|
|
72
|
-
|
|
73
|
-
const result = await manager.loadMany('object');
|
|
74
|
-
expect(result).toHaveLength(2);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('should deduplicate items by name across loaders', async () => {
|
|
78
|
-
const loader1 = createMockLoaderMany('first', [
|
|
79
|
-
{ name: 'account', label: 'Account V1' },
|
|
80
|
-
]);
|
|
81
|
-
const loader2 = createMockLoaderMany('second', [
|
|
82
|
-
{ name: 'account', label: 'Account V2' },
|
|
83
|
-
{ name: 'contact', label: 'Contact' },
|
|
84
|
-
]);
|
|
85
|
-
|
|
86
|
-
const m = new MetadataManager({ formats: ['json'], loaders: [loader1, loader2] });
|
|
87
|
-
const result = await m.loadMany<{ name: string; label: string }>('object');
|
|
88
|
-
expect(result).toHaveLength(2);
|
|
89
|
-
// First loader wins
|
|
90
|
-
expect(result.find(r => r.name === 'account')?.label).toBe('Account V1');
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('should skip failing loaders in loadMany', async () => {
|
|
94
|
-
const failingLoader = createMockLoaderMany('failing', [], true);
|
|
95
|
-
const goodLoader = createMockLoaderMany('good', [{ name: 'ok' }]);
|
|
96
|
-
|
|
97
|
-
const m = new MetadataManager({ formats: ['json'], loaders: [failingLoader, goodLoader] });
|
|
98
|
-
const result = await m.loadMany('object');
|
|
99
|
-
expect(result).toHaveLength(1);
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
describe('save', () => {
|
|
104
|
-
it('should save to a writable loader', async () => {
|
|
105
|
-
await manager.save('object', 'account', { name: 'account' });
|
|
106
|
-
const result = await manager.load('object', 'account');
|
|
107
|
-
expect(result).toEqual({ name: 'account' });
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('should throw when no writable loader is available', async () => {
|
|
111
|
-
const readOnlyLoader: MetadataLoader = {
|
|
112
|
-
contract: { name: 'readonly', protocol: 'memory:' as const, capabilities: { read: true, write: false, watch: false, list: true } },
|
|
113
|
-
load: vi.fn().mockResolvedValue({ data: null }),
|
|
114
|
-
loadMany: vi.fn().mockResolvedValue([]),
|
|
115
|
-
exists: vi.fn().mockResolvedValue(false),
|
|
116
|
-
stat: vi.fn().mockResolvedValue(null),
|
|
117
|
-
list: vi.fn().mockResolvedValue([]),
|
|
118
|
-
// No save method
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
const m = new MetadataManager({ formats: ['json'], loaders: [readOnlyLoader] });
|
|
122
|
-
await expect(m.save('object', 'test', {})).rejects.toThrow('No loader available');
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('should save to a specific named loader', async () => {
|
|
126
|
-
await manager.save('object', 'account', { name: 'account' }, { loader: 'memory' } as any);
|
|
127
|
-
const result = await manager.load('object', 'account');
|
|
128
|
-
expect(result).toEqual({ name: 'account' });
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it('should throw when specified loader not found', async () => {
|
|
132
|
-
await expect(
|
|
133
|
-
manager.save('object', 'test', {}, { loader: 'nonexistent' } as any)
|
|
134
|
-
).rejects.toThrow('Loader not found');
|
|
135
|
-
});
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
describe('exists', () => {
|
|
139
|
-
it('should return false for non-existent items', async () => {
|
|
140
|
-
expect(await manager.exists('object', 'nope')).toBe(false);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('should return true for existing items', async () => {
|
|
144
|
-
await memoryLoader.save('object', 'account', { name: 'account' });
|
|
145
|
-
expect(await manager.exists('object', 'account')).toBe(true);
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
describe('list', () => {
|
|
150
|
-
it('should return empty array for empty type', async () => {
|
|
151
|
-
const result = await manager.listNames('object');
|
|
152
|
-
expect(result).toEqual([]);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it('should list all items of a type', async () => {
|
|
156
|
-
await memoryLoader.save('object', 'account', {});
|
|
157
|
-
await memoryLoader.save('object', 'contact', {});
|
|
158
|
-
const result = await manager.listNames('object');
|
|
159
|
-
expect(result).toHaveLength(2);
|
|
160
|
-
expect(result).toContain('account');
|
|
161
|
-
expect(result).toContain('contact');
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it('should deduplicate across loaders', async () => {
|
|
165
|
-
const loader1: MetadataLoader = {
|
|
166
|
-
contract: { name: 'l1', protocol: 'memory:' as const, capabilities: { read: true, write: false, watch: false, list: true } },
|
|
167
|
-
load: vi.fn().mockResolvedValue({ data: null }),
|
|
168
|
-
loadMany: vi.fn().mockResolvedValue([]),
|
|
169
|
-
exists: vi.fn().mockResolvedValue(false),
|
|
170
|
-
stat: vi.fn().mockResolvedValue(null),
|
|
171
|
-
list: vi.fn().mockResolvedValue(['account', 'contact']),
|
|
172
|
-
};
|
|
173
|
-
const loader2: MetadataLoader = {
|
|
174
|
-
contract: { name: 'l2', protocol: 'memory:' as const, capabilities: { read: true, write: false, watch: false, list: true } },
|
|
175
|
-
load: vi.fn().mockResolvedValue({ data: null }),
|
|
176
|
-
loadMany: vi.fn().mockResolvedValue([]),
|
|
177
|
-
exists: vi.fn().mockResolvedValue(false),
|
|
178
|
-
stat: vi.fn().mockResolvedValue(null),
|
|
179
|
-
list: vi.fn().mockResolvedValue(['account', 'lead']),
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
const m = new MetadataManager({ formats: ['json'], loaders: [loader1, loader2] });
|
|
183
|
-
const result = await m.listNames('object');
|
|
184
|
-
expect(result).toHaveLength(3);
|
|
185
|
-
expect(result).toContain('account');
|
|
186
|
-
expect(result).toContain('contact');
|
|
187
|
-
expect(result).toContain('lead');
|
|
188
|
-
});
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
describe('watch / unwatch', () => {
|
|
192
|
-
it('should register and invoke watch callbacks', () => {
|
|
193
|
-
const callback = vi.fn();
|
|
194
|
-
(manager as any).addWatchCallback('object', callback);
|
|
195
|
-
|
|
196
|
-
// Trigger via protected method — cast to access it
|
|
197
|
-
(manager as any).notifyWatchers('object', {
|
|
198
|
-
type: 'changed',
|
|
199
|
-
metadataType: 'object',
|
|
200
|
-
name: 'account',
|
|
201
|
-
path: '/fake',
|
|
202
|
-
timestamp: new Date(),
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
expect(callback).toHaveBeenCalledOnce();
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
it('should unwatch callback', () => {
|
|
209
|
-
const callback = vi.fn();
|
|
210
|
-
(manager as any).addWatchCallback('object', callback);
|
|
211
|
-
(manager as any).removeWatchCallback('object', callback);
|
|
212
|
-
|
|
213
|
-
(manager as any).notifyWatchers('object', {
|
|
214
|
-
type: 'changed',
|
|
215
|
-
metadataType: 'object',
|
|
216
|
-
name: 'account',
|
|
217
|
-
path: '/fake',
|
|
218
|
-
timestamp: new Date(),
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
expect(callback).not.toHaveBeenCalled();
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
it('should not throw when unwatching non-existent callback', () => {
|
|
225
|
-
expect(() => (manager as any).removeWatchCallback('object', vi.fn())).not.toThrow();
|
|
226
|
-
});
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
describe('registerLoader', () => {
|
|
230
|
-
it('should register a new loader', async () => {
|
|
231
|
-
const newLoader = new MemoryLoader();
|
|
232
|
-
await newLoader.save('view', 'dashboard', { name: 'dashboard' });
|
|
233
|
-
|
|
234
|
-
manager.registerLoader(newLoader);
|
|
235
|
-
|
|
236
|
-
const result = await manager.load('view', 'dashboard');
|
|
237
|
-
expect(result).toEqual({ name: 'dashboard' });
|
|
238
|
-
});
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
describe('serializer initialization', () => {
|
|
242
|
-
it('should initialize with default formats', () => {
|
|
243
|
-
const m = new MetadataManager({ loaders: [] });
|
|
244
|
-
// Default formats are typescript, json, yaml
|
|
245
|
-
expect((m as any).serializers.size).toBe(3);
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
it('should initialize with only requested formats', () => {
|
|
249
|
-
const m = new MetadataManager({ formats: ['json'], loaders: [] });
|
|
250
|
-
expect((m as any).serializers.size).toBe(1);
|
|
251
|
-
expect((m as any).serializers.has('json')).toBe(true);
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
it('should support javascript format', () => {
|
|
255
|
-
const m = new MetadataManager({ formats: ['javascript'], loaders: [] });
|
|
256
|
-
expect((m as any).serializers.has('javascript')).toBe(true);
|
|
257
|
-
});
|
|
258
|
-
});
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
// ---------- MemoryLoader ----------
|
|
262
|
-
|
|
263
|
-
describe('MemoryLoader', () => {
|
|
264
|
-
let loader: MemoryLoader;
|
|
265
|
-
|
|
266
|
-
beforeEach(() => {
|
|
267
|
-
loader = new MemoryLoader();
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
it('should have correct contract', () => {
|
|
271
|
-
expect(loader.contract.name).toBe('memory');
|
|
272
|
-
expect(loader.contract.protocol).toBe('memory:');
|
|
273
|
-
expect(loader.contract.capabilities.read).toBe(true);
|
|
274
|
-
expect(loader.contract.capabilities.write).toBe(true);
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
it('should save and load items', async () => {
|
|
278
|
-
await loader.save('object', 'task', { name: 'task', label: 'Task' });
|
|
279
|
-
const result = await loader.load('object', 'task');
|
|
280
|
-
expect(result.data).toEqual({ name: 'task', label: 'Task' });
|
|
281
|
-
expect(result.source).toBe('memory');
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
it('should return null for missing items', async () => {
|
|
285
|
-
const result = await loader.load('object', 'missing');
|
|
286
|
-
expect(result.data).toBeNull();
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
it('should check existence', async () => {
|
|
290
|
-
expect(await loader.exists('object', 'task')).toBe(false);
|
|
291
|
-
await loader.save('object', 'task', {});
|
|
292
|
-
expect(await loader.exists('object', 'task')).toBe(true);
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
it('should list items', async () => {
|
|
296
|
-
await loader.save('object', 'a', {});
|
|
297
|
-
await loader.save('object', 'b', {});
|
|
298
|
-
const items = await loader.list('object');
|
|
299
|
-
expect(items).toEqual(['a', 'b']);
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
it('should return empty list for unknown types', async () => {
|
|
303
|
-
expect(await loader.list('unknown')).toEqual([]);
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
it('should loadMany items', async () => {
|
|
307
|
-
await loader.save('object', 'a', { name: 'a' });
|
|
308
|
-
await loader.save('object', 'b', { name: 'b' });
|
|
309
|
-
const items = await loader.loadMany('object');
|
|
310
|
-
expect(items).toHaveLength(2);
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
it('should return stats for existing items', async () => {
|
|
314
|
-
await loader.save('object', 'task', {});
|
|
315
|
-
const stats = await loader.stat('object', 'task');
|
|
316
|
-
expect(stats).not.toBeNull();
|
|
317
|
-
expect(stats!.format).toBe('json');
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
it('should return null stats for missing items', async () => {
|
|
321
|
-
const stats = await loader.stat('object', 'missing');
|
|
322
|
-
expect(stats).toBeNull();
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
it('should return save result with path', async () => {
|
|
326
|
-
const result = await loader.save('object', 'task', {});
|
|
327
|
-
expect(result.success).toBe(true);
|
|
328
|
-
expect(result.path).toBe('memory://object/task');
|
|
329
|
-
});
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
// ---------- MetadataPlugin ----------
|
|
333
|
-
|
|
334
|
-
describe('MetadataPlugin', () => {
|
|
335
|
-
// Plugin creates NodeMetadataManager which depends on node:path and chokidar.
|
|
336
|
-
// We mock NodeMetadataManager to avoid filesystem side effects.
|
|
337
|
-
vi.mock('./node-metadata-manager', () => {
|
|
338
|
-
const MockNodeMetadataManager = class {
|
|
339
|
-
loadMany = vi.fn().mockResolvedValue([]);
|
|
340
|
-
registerLoader = vi.fn();
|
|
341
|
-
stopWatching = vi.fn();
|
|
342
|
-
setTypeRegistry = vi.fn();
|
|
343
|
-
register = vi.fn();
|
|
344
|
-
};
|
|
345
|
-
return { NodeMetadataManager: MockNodeMetadataManager };
|
|
346
|
-
});
|
|
347
|
-
|
|
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
|
-
],
|
|
354
|
-
}));
|
|
355
|
-
|
|
356
|
-
it('should have correct plugin metadata', async () => {
|
|
357
|
-
const { MetadataPlugin } = await import('./plugin.js');
|
|
358
|
-
const plugin = new MetadataPlugin({ rootDir: '/tmp/test', watch: false });
|
|
359
|
-
expect(plugin.name).toBe('com.objectstack.metadata');
|
|
360
|
-
expect(plugin.version).toBe('1.0.0');
|
|
361
|
-
expect(plugin.type).toBe('standard');
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
it('should call init and register metadata service', async () => {
|
|
365
|
-
const { MetadataPlugin } = await import('./plugin.js');
|
|
366
|
-
const plugin = new MetadataPlugin({ rootDir: '/tmp/test', watch: false });
|
|
367
|
-
|
|
368
|
-
const ctx = createMockPluginContext();
|
|
369
|
-
await plugin.init(ctx);
|
|
370
|
-
|
|
371
|
-
expect(ctx.registerService).toHaveBeenCalledWith('metadata', expect.anything());
|
|
372
|
-
});
|
|
373
|
-
|
|
374
|
-
it('should call start and attempt to load metadata types', async () => {
|
|
375
|
-
const { MetadataPlugin } = await import('./plugin.js');
|
|
376
|
-
const plugin = new MetadataPlugin({ rootDir: '/tmp/test', watch: false });
|
|
377
|
-
|
|
378
|
-
const ctx = createMockPluginContext();
|
|
379
|
-
await plugin.init(ctx);
|
|
380
|
-
await plugin.start(ctx);
|
|
381
|
-
|
|
382
|
-
// start should call logger.info at least once
|
|
383
|
-
expect(ctx.logger.info).toHaveBeenCalled();
|
|
384
|
-
});
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
// ---------- Helpers ----------
|
|
388
|
-
|
|
389
|
-
function createMockLoader(name: string, data: any, shouldFail = false): MetadataLoader {
|
|
390
|
-
return {
|
|
391
|
-
contract: { name, protocol: 'memory:' as const, capabilities: { read: true, write: false, watch: false, list: true } },
|
|
392
|
-
load: shouldFail
|
|
393
|
-
? vi.fn().mockRejectedValue(new Error('loader failed'))
|
|
394
|
-
: vi.fn().mockResolvedValue({ data }),
|
|
395
|
-
loadMany: vi.fn().mockResolvedValue([]),
|
|
396
|
-
exists: vi.fn().mockResolvedValue(false),
|
|
397
|
-
stat: vi.fn().mockResolvedValue(null),
|
|
398
|
-
list: vi.fn().mockResolvedValue([]),
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
function createMockLoaderMany(name: string, items: any[], shouldFail = false): MetadataLoader {
|
|
403
|
-
return {
|
|
404
|
-
contract: { name, protocol: 'memory:' as const, capabilities: { read: true, write: false, watch: false, list: true } },
|
|
405
|
-
load: vi.fn().mockResolvedValue({ data: null }),
|
|
406
|
-
loadMany: shouldFail
|
|
407
|
-
? vi.fn().mockRejectedValue(new Error('loader failed'))
|
|
408
|
-
: vi.fn().mockResolvedValue(items),
|
|
409
|
-
exists: vi.fn().mockResolvedValue(false),
|
|
410
|
-
stat: vi.fn().mockResolvedValue(null),
|
|
411
|
-
list: vi.fn().mockResolvedValue([]),
|
|
412
|
-
};
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
function createMockPluginContext() {
|
|
416
|
-
return {
|
|
417
|
-
registerService: vi.fn(),
|
|
418
|
-
replaceService: vi.fn(),
|
|
419
|
-
getService: vi.fn().mockReturnValue(null),
|
|
420
|
-
getServices: vi.fn().mockReturnValue(new Map()),
|
|
421
|
-
hook: vi.fn(),
|
|
422
|
-
trigger: vi.fn(),
|
|
423
|
-
logger: {
|
|
424
|
-
info: vi.fn(),
|
|
425
|
-
warn: vi.fn(),
|
|
426
|
-
error: vi.fn(),
|
|
427
|
-
debug: vi.fn(),
|
|
428
|
-
},
|
|
429
|
-
getKernel: vi.fn(),
|
|
430
|
-
};
|
|
431
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
import { System } from '@objectstack/spec';
|
|
4
|
-
import { ISchemaDriver } from '@objectstack/spec/contracts';
|
|
5
|
-
|
|
6
|
-
export class MigrationExecutor {
|
|
7
|
-
constructor(private driver: ISchemaDriver) {}
|
|
8
|
-
|
|
9
|
-
async executeChangeSet(changeSet: System.ChangeSet): Promise<void> {
|
|
10
|
-
console.log(`Executing ChangeSet: ${changeSet.name} (${changeSet.id})`);
|
|
11
|
-
|
|
12
|
-
for (const op of changeSet.operations) {
|
|
13
|
-
try {
|
|
14
|
-
await this.executeOperation(op);
|
|
15
|
-
} catch (e) {
|
|
16
|
-
console.error(`Failed to execute operation ${op.type}:`, e);
|
|
17
|
-
throw e;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
private async executeOperation(op: System.MigrationOperation): Promise<void> {
|
|
23
|
-
switch (op.type) {
|
|
24
|
-
case 'create_object':
|
|
25
|
-
console.log(` > Create Object: ${op.object.name}`);
|
|
26
|
-
await this.driver.createCollection(op.object.name, op.object);
|
|
27
|
-
break;
|
|
28
|
-
case 'add_field':
|
|
29
|
-
console.log(` > Add Field: ${op.objectName}.${op.fieldName}`);
|
|
30
|
-
await this.driver.addColumn(op.objectName, op.fieldName, op.field);
|
|
31
|
-
break;
|
|
32
|
-
case 'remove_field':
|
|
33
|
-
console.log(` > Remove Field: ${op.objectName}.${op.fieldName}`);
|
|
34
|
-
await this.driver.dropColumn(op.objectName, op.fieldName);
|
|
35
|
-
break;
|
|
36
|
-
case 'delete_object':
|
|
37
|
-
console.log(` > Delete Object: ${op.objectName}`);
|
|
38
|
-
await this.driver.dropCollection(op.objectName);
|
|
39
|
-
break;
|
|
40
|
-
case 'execute_sql':
|
|
41
|
-
console.log(` > Execute SQL`);
|
|
42
|
-
await this.driver.executeRaw(op.sql);
|
|
43
|
-
break;
|
|
44
|
-
case 'modify_field':
|
|
45
|
-
console.warn(` ! Modify Field: ${op.objectName}.${op.fieldName} (Not fully implemented)`);
|
|
46
|
-
break;
|
|
47
|
-
case 'rename_object':
|
|
48
|
-
console.warn(` ! Rename Object: ${op.oldName} -> ${op.newName} (Not fully implemented)`);
|
|
49
|
-
break;
|
|
50
|
-
default:
|
|
51
|
-
throw new Error(`Unknown operation type`);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
package/src/migration/index.ts
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Node Metadata Manager
|
|
5
|
-
*
|
|
6
|
-
* Extends MetadataManager with Filesystem capabilities (Watching, default loader)
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import * as path from 'node:path';
|
|
10
|
-
import { watch as chokidarWatch, type FSWatcher } from 'chokidar';
|
|
11
|
-
import type {
|
|
12
|
-
MetadataWatchEvent,
|
|
13
|
-
} from '@objectstack/spec/system';
|
|
14
|
-
import { FilesystemLoader } from './loaders/filesystem-loader.js';
|
|
15
|
-
import { MetadataManager, type MetadataManagerOptions } from './metadata-manager.js';
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Node metadata manager class
|
|
19
|
-
*/
|
|
20
|
-
export class NodeMetadataManager extends MetadataManager {
|
|
21
|
-
private watcher?: FSWatcher;
|
|
22
|
-
|
|
23
|
-
constructor(config: MetadataManagerOptions) {
|
|
24
|
-
super(config);
|
|
25
|
-
|
|
26
|
-
// Initialize Default Filesystem Loader if no loaders provided
|
|
27
|
-
// This logic replaces the removed logic from base class
|
|
28
|
-
if (!config.loaders || config.loaders.length === 0) {
|
|
29
|
-
const rootDir = config.rootDir || process.cwd();
|
|
30
|
-
this.registerLoader(new FilesystemLoader(rootDir, this.serializers, this.logger));
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Start watching if enabled
|
|
34
|
-
if (config.watch) {
|
|
35
|
-
this.startWatching();
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Stop all watching
|
|
41
|
-
*/
|
|
42
|
-
async stopWatching(): Promise<void> {
|
|
43
|
-
if (this.watcher) {
|
|
44
|
-
await this.watcher.close();
|
|
45
|
-
this.watcher = undefined;
|
|
46
|
-
}
|
|
47
|
-
// Call base cleanup if any
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Start watching for file changes
|
|
52
|
-
*/
|
|
53
|
-
private startWatching(): void {
|
|
54
|
-
const rootDir = this.config.rootDir || process.cwd();
|
|
55
|
-
const { ignored = ['**/node_modules/**', '**/*.test.*'], persistent = true } =
|
|
56
|
-
this.config.watchOptions || {};
|
|
57
|
-
|
|
58
|
-
this.watcher = chokidarWatch(rootDir, {
|
|
59
|
-
ignored,
|
|
60
|
-
persistent,
|
|
61
|
-
ignoreInitial: true,
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
this.watcher.on('add', async (filePath) => {
|
|
65
|
-
await this.handleFileEvent('added', filePath);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
this.watcher.on('change', async (filePath) => {
|
|
69
|
-
await this.handleFileEvent('changed', filePath);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
this.watcher.on('unlink', async (filePath) => {
|
|
73
|
-
await this.handleFileEvent('deleted', filePath);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
this.logger.info('File watcher started', { rootDir });
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Handle file change events
|
|
81
|
-
*/
|
|
82
|
-
private async handleFileEvent(
|
|
83
|
-
eventType: 'added' | 'changed' | 'deleted',
|
|
84
|
-
filePath: string
|
|
85
|
-
): Promise<void> {
|
|
86
|
-
const rootDir = this.config.rootDir || process.cwd();
|
|
87
|
-
const relativePath = path.relative(rootDir, filePath);
|
|
88
|
-
const parts = relativePath.split(path.sep);
|
|
89
|
-
|
|
90
|
-
if (parts.length < 2) {
|
|
91
|
-
return; // Not a metadata file
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const type = parts[0];
|
|
95
|
-
const fileName = parts[parts.length - 1];
|
|
96
|
-
const name = path.basename(fileName, path.extname(fileName));
|
|
97
|
-
|
|
98
|
-
// We can't access private watchCallbacks from parent.
|
|
99
|
-
// We need a protected method to trigger watch event or access it.
|
|
100
|
-
// OPTION: Add a method `triggerWatchEvent` to MetadataManager
|
|
101
|
-
|
|
102
|
-
let data: any = undefined;
|
|
103
|
-
if (eventType !== 'deleted') {
|
|
104
|
-
try {
|
|
105
|
-
data = await this.load(type, name, { useCache: false });
|
|
106
|
-
} catch (error) {
|
|
107
|
-
this.logger.error('Failed to load changed file', undefined, {
|
|
108
|
-
filePath,
|
|
109
|
-
error: error instanceof Error ? error.message : String(error),
|
|
110
|
-
});
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const event: MetadataWatchEvent = {
|
|
116
|
-
type: eventType,
|
|
117
|
-
metadataType: type,
|
|
118
|
-
name,
|
|
119
|
-
path: filePath,
|
|
120
|
-
data,
|
|
121
|
-
timestamp: new Date().toISOString(),
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
this.notifyWatchers(type, event);
|
|
125
|
-
}
|
|
126
|
-
}
|
package/src/node.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Node.js specific exports for @objectstack/metadata
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export * from './index.js';
|
|
8
|
-
export { NodeMetadataManager } from './node-metadata-manager.js';
|
|
9
|
-
export { FilesystemLoader } from './loaders/filesystem-loader.js';
|
|
10
|
-
export { DatabaseLoader, type DatabaseLoaderOptions } from './loaders/database-loader.js';
|
|
11
|
-
export { MetadataPlugin } from './plugin.js';
|