@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-manager.ts
DELETED
|
@@ -1,1168 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Metadata Manager
|
|
5
|
-
*
|
|
6
|
-
* Main orchestrator for metadata loading, saving, and persistence.
|
|
7
|
-
* Implements the IMetadataService contract from @objectstack/spec.
|
|
8
|
-
* Browser-compatible (Pure).
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import type {
|
|
12
|
-
MetadataManagerConfig,
|
|
13
|
-
MetadataLoadOptions,
|
|
14
|
-
MetadataSaveOptions,
|
|
15
|
-
MetadataSaveResult,
|
|
16
|
-
MetadataWatchEvent,
|
|
17
|
-
MetadataFormat,
|
|
18
|
-
PackagePublishResult,
|
|
19
|
-
} from '@objectstack/spec/system';
|
|
20
|
-
import type {
|
|
21
|
-
IMetadataService,
|
|
22
|
-
MetadataWatchCallback,
|
|
23
|
-
MetadataWatchHandle,
|
|
24
|
-
MetadataExportOptions,
|
|
25
|
-
MetadataImportOptions,
|
|
26
|
-
MetadataImportResult,
|
|
27
|
-
MetadataTypeInfo,
|
|
28
|
-
} from '@objectstack/spec/contracts';
|
|
29
|
-
import type {
|
|
30
|
-
MetadataQuery,
|
|
31
|
-
MetadataQueryResult,
|
|
32
|
-
MetadataValidationResult,
|
|
33
|
-
MetadataBulkResult,
|
|
34
|
-
MetadataDependency,
|
|
35
|
-
MetadataTypeRegistryEntry,
|
|
36
|
-
} from '@objectstack/spec/kernel';
|
|
37
|
-
import type { MetadataOverlay } from '@objectstack/spec/kernel';
|
|
38
|
-
import { createLogger, type Logger } from '@objectstack/core';
|
|
39
|
-
import { JSONSerializer } from './serializers/json-serializer.js';
|
|
40
|
-
import { YAMLSerializer } from './serializers/yaml-serializer.js';
|
|
41
|
-
import { TypeScriptSerializer } from './serializers/typescript-serializer.js';
|
|
42
|
-
import type { MetadataSerializer } from './serializers/serializer-interface.js';
|
|
43
|
-
import type { IDataDriver } from '@objectstack/spec/contracts';
|
|
44
|
-
import type { MetadataLoader } from './loaders/loader-interface.js';
|
|
45
|
-
import { DatabaseLoader } from './loaders/database-loader.js';
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Watch callback function (legacy)
|
|
49
|
-
*/
|
|
50
|
-
export type WatchCallback = (event: MetadataWatchEvent) => void | Promise<void>;
|
|
51
|
-
|
|
52
|
-
export interface MetadataManagerOptions extends MetadataManagerConfig {
|
|
53
|
-
loaders?: MetadataLoader[];
|
|
54
|
-
/** Optional IDataDriver instance. When provided alongside config.datasource, auto-configures DatabaseLoader. */
|
|
55
|
-
driver?: IDataDriver;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Main metadata manager class.
|
|
60
|
-
* Implements IMetadataService contract for unified metadata management.
|
|
61
|
-
*/
|
|
62
|
-
export class MetadataManager implements IMetadataService {
|
|
63
|
-
private loaders: Map<string, MetadataLoader> = new Map();
|
|
64
|
-
// Protected so subclasses can access serializers if needed
|
|
65
|
-
protected serializers: Map<MetadataFormat, MetadataSerializer>;
|
|
66
|
-
protected logger: Logger;
|
|
67
|
-
protected watchCallbacks = new Map<string, Set<WatchCallback>>();
|
|
68
|
-
protected config: MetadataManagerOptions;
|
|
69
|
-
|
|
70
|
-
// In-memory metadata registry: type -> name -> data
|
|
71
|
-
private registry = new Map<string, Map<string, unknown>>();
|
|
72
|
-
|
|
73
|
-
// Overlay storage: "type:name:scope" -> MetadataOverlay
|
|
74
|
-
private overlays = new Map<string, MetadataOverlay>();
|
|
75
|
-
|
|
76
|
-
// Type registry for metadata type info
|
|
77
|
-
private typeRegistry: MetadataTypeRegistryEntry[] = [];
|
|
78
|
-
|
|
79
|
-
// Dependency tracking: "type:name" -> dependencies
|
|
80
|
-
private dependencies = new Map<string, MetadataDependency[]>();
|
|
81
|
-
|
|
82
|
-
constructor(config: MetadataManagerOptions) {
|
|
83
|
-
this.config = config;
|
|
84
|
-
this.logger = createLogger({ level: 'info', format: 'pretty' });
|
|
85
|
-
|
|
86
|
-
// Initialize serializers
|
|
87
|
-
this.serializers = new Map();
|
|
88
|
-
const formats = config.formats || ['typescript', 'json', 'yaml'];
|
|
89
|
-
|
|
90
|
-
if (formats.includes('json')) {
|
|
91
|
-
this.serializers.set('json', new JSONSerializer());
|
|
92
|
-
}
|
|
93
|
-
if (formats.includes('yaml')) {
|
|
94
|
-
this.serializers.set('yaml', new YAMLSerializer());
|
|
95
|
-
}
|
|
96
|
-
if (formats.includes('typescript')) {
|
|
97
|
-
this.serializers.set('typescript', new TypeScriptSerializer('typescript'));
|
|
98
|
-
}
|
|
99
|
-
if (formats.includes('javascript')) {
|
|
100
|
-
this.serializers.set('javascript', new TypeScriptSerializer('javascript'));
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Initialize Loaders
|
|
104
|
-
if (config.loaders && config.loaders.length > 0) {
|
|
105
|
-
config.loaders.forEach(loader => this.registerLoader(loader));
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Auto-configure DatabaseLoader when datasource + driver are provided
|
|
109
|
-
if (config.datasource && config.driver) {
|
|
110
|
-
this.setDatabaseDriver(config.driver);
|
|
111
|
-
}
|
|
112
|
-
// Note: No default loader in base class. Subclasses (NodeMetadataManager) or caller must provide one.
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Set the type registry for metadata type discovery.
|
|
117
|
-
*/
|
|
118
|
-
setTypeRegistry(entries: MetadataTypeRegistryEntry[]): void {
|
|
119
|
-
this.typeRegistry = entries;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Configure and register a DatabaseLoader for database-backed metadata persistence.
|
|
124
|
-
* Can be called at any time to enable database storage (e.g. after kernel resolves the driver).
|
|
125
|
-
*
|
|
126
|
-
* @param driver - An IDataDriver instance for database operations
|
|
127
|
-
*/
|
|
128
|
-
setDatabaseDriver(driver: IDataDriver): void {
|
|
129
|
-
const tableName = this.config.tableName ?? 'sys_metadata';
|
|
130
|
-
const dbLoader = new DatabaseLoader({
|
|
131
|
-
driver,
|
|
132
|
-
tableName,
|
|
133
|
-
});
|
|
134
|
-
this.registerLoader(dbLoader);
|
|
135
|
-
this.logger.info('DatabaseLoader configured', { datasource: this.config.datasource, tableName });
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Register a new metadata loader (data source)
|
|
140
|
-
*/
|
|
141
|
-
registerLoader(loader: MetadataLoader) {
|
|
142
|
-
this.loaders.set(loader.contract.name, loader);
|
|
143
|
-
this.logger.info(`Registered metadata loader: ${loader.contract.name} (${loader.contract.protocol})`);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// ==========================================
|
|
147
|
-
// IMetadataService — Core CRUD Operations
|
|
148
|
-
// ==========================================
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Register/save a metadata item by type
|
|
152
|
-
*/
|
|
153
|
-
async register(type: string, name: string, data: unknown): Promise<void> {
|
|
154
|
-
if (!this.registry.has(type)) {
|
|
155
|
-
this.registry.set(type, new Map());
|
|
156
|
-
}
|
|
157
|
-
this.registry.get(type)!.set(name, data);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Get a metadata item by type and name.
|
|
162
|
-
* Checks in-memory registry first, then falls back to loaders.
|
|
163
|
-
*/
|
|
164
|
-
async get(type: string, name: string): Promise<unknown | undefined> {
|
|
165
|
-
// Check in-memory registry first
|
|
166
|
-
const typeStore = this.registry.get(type);
|
|
167
|
-
if (typeStore?.has(name)) {
|
|
168
|
-
return typeStore.get(name);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Fallback to loaders
|
|
172
|
-
const result = await this.load(type, name);
|
|
173
|
-
return result ?? undefined;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* List all metadata items of a given type
|
|
178
|
-
*/
|
|
179
|
-
async list(type: string): Promise<unknown[]> {
|
|
180
|
-
const items = new Map<string, unknown>();
|
|
181
|
-
|
|
182
|
-
// From in-memory registry
|
|
183
|
-
const typeStore = this.registry.get(type);
|
|
184
|
-
if (typeStore) {
|
|
185
|
-
for (const [name, data] of typeStore) {
|
|
186
|
-
items.set(name, data);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// From loaders (deduplicate)
|
|
191
|
-
for (const loader of this.loaders.values()) {
|
|
192
|
-
try {
|
|
193
|
-
const loaderItems = await loader.loadMany(type);
|
|
194
|
-
for (const item of loaderItems) {
|
|
195
|
-
const itemAny = item as any;
|
|
196
|
-
if (itemAny && typeof itemAny.name === 'string' && !items.has(itemAny.name)) {
|
|
197
|
-
items.set(itemAny.name, item);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
} catch (e) {
|
|
201
|
-
this.logger.warn(`Loader ${loader.contract.name} failed to loadMany ${type}`, { error: e });
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return Array.from(items.values());
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Unregister/remove a metadata item by type and name
|
|
210
|
-
*/
|
|
211
|
-
async unregister(type: string, name: string): Promise<void> {
|
|
212
|
-
const typeStore = this.registry.get(type);
|
|
213
|
-
if (typeStore) {
|
|
214
|
-
typeStore.delete(name);
|
|
215
|
-
if (typeStore.size === 0) {
|
|
216
|
-
this.registry.delete(type);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Check if a metadata item exists
|
|
223
|
-
*/
|
|
224
|
-
async exists(type: string, name: string): Promise<boolean> {
|
|
225
|
-
// Check in-memory registry
|
|
226
|
-
if (this.registry.get(type)?.has(name)) {
|
|
227
|
-
return true;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// Check loaders
|
|
231
|
-
for (const loader of this.loaders.values()) {
|
|
232
|
-
if (await loader.exists(type, name)) {
|
|
233
|
-
return true;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
return false;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* List all names of metadata items of a given type
|
|
241
|
-
*/
|
|
242
|
-
async listNames(type: string): Promise<string[]> {
|
|
243
|
-
const names = new Set<string>();
|
|
244
|
-
|
|
245
|
-
// From in-memory registry
|
|
246
|
-
const typeStore = this.registry.get(type);
|
|
247
|
-
if (typeStore) {
|
|
248
|
-
for (const name of typeStore.keys()) {
|
|
249
|
-
names.add(name);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// From loaders
|
|
254
|
-
for (const loader of this.loaders.values()) {
|
|
255
|
-
const result = await loader.list(type);
|
|
256
|
-
result.forEach(item => names.add(item));
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
return Array.from(names);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Convenience: get an object definition by name
|
|
264
|
-
*/
|
|
265
|
-
async getObject(name: string): Promise<unknown | undefined> {
|
|
266
|
-
return this.get('object', name);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Convenience: list all object definitions
|
|
271
|
-
*/
|
|
272
|
-
async listObjects(): Promise<unknown[]> {
|
|
273
|
-
return this.list('object');
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// ==========================================
|
|
277
|
-
// Convenience: UI Metadata
|
|
278
|
-
// ==========================================
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Convenience: get a view definition by name
|
|
282
|
-
*/
|
|
283
|
-
async getView(name: string): Promise<unknown | undefined> {
|
|
284
|
-
return this.get('view', name);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Convenience: list view definitions, optionally filtered by object
|
|
289
|
-
*/
|
|
290
|
-
async listViews(object?: string): Promise<unknown[]> {
|
|
291
|
-
const views = await this.list('view');
|
|
292
|
-
if (object) {
|
|
293
|
-
return views.filter((v: any) => v?.object === object);
|
|
294
|
-
}
|
|
295
|
-
return views;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* Convenience: get a dashboard definition by name
|
|
300
|
-
*/
|
|
301
|
-
async getDashboard(name: string): Promise<unknown | undefined> {
|
|
302
|
-
return this.get('dashboard', name);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* Convenience: list all dashboard definitions
|
|
307
|
-
*/
|
|
308
|
-
async listDashboards(): Promise<unknown[]> {
|
|
309
|
-
return this.list('dashboard');
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// ==========================================
|
|
313
|
-
// Package Management
|
|
314
|
-
// ==========================================
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* Unregister all metadata items from a specific package
|
|
318
|
-
*/
|
|
319
|
-
async unregisterPackage(packageName: string): Promise<void> {
|
|
320
|
-
for (const [type, typeStore] of this.registry) {
|
|
321
|
-
const toDelete: string[] = [];
|
|
322
|
-
for (const [name, data] of typeStore) {
|
|
323
|
-
const meta = data as any;
|
|
324
|
-
if (meta?.packageId === packageName || meta?.package === packageName) {
|
|
325
|
-
toDelete.push(name);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
for (const name of toDelete) {
|
|
329
|
-
typeStore.delete(name);
|
|
330
|
-
}
|
|
331
|
-
if (typeStore.size === 0) {
|
|
332
|
-
this.registry.delete(type);
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* Publish an entire package:
|
|
339
|
-
* 1. Validate all draft items
|
|
340
|
-
* 2. Snapshot all items in the package (publishedDefinition = clone(metadata))
|
|
341
|
-
* 3. Increment version
|
|
342
|
-
* 4. Set all items state → active
|
|
343
|
-
*/
|
|
344
|
-
async publishPackage(packageId: string, options?: {
|
|
345
|
-
changeNote?: string;
|
|
346
|
-
publishedBy?: string;
|
|
347
|
-
validate?: boolean;
|
|
348
|
-
}): Promise<PackagePublishResult> {
|
|
349
|
-
const now = new Date().toISOString();
|
|
350
|
-
const shouldValidate = options?.validate !== false;
|
|
351
|
-
const publishedBy = options?.publishedBy;
|
|
352
|
-
|
|
353
|
-
// Collect all items belonging to this package
|
|
354
|
-
const packageItems: Array<{ type: string; name: string; data: any }> = [];
|
|
355
|
-
for (const [type, typeStore] of this.registry) {
|
|
356
|
-
for (const [name, data] of typeStore) {
|
|
357
|
-
const meta = data as any;
|
|
358
|
-
if (meta?.packageId === packageId || meta?.package === packageId) {
|
|
359
|
-
packageItems.push({ type, name, data: meta });
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
if (packageItems.length === 0) {
|
|
365
|
-
return {
|
|
366
|
-
success: false,
|
|
367
|
-
packageId,
|
|
368
|
-
version: 0,
|
|
369
|
-
publishedAt: now,
|
|
370
|
-
itemsPublished: 0,
|
|
371
|
-
validationErrors: [{ type: '', name: '', message: `No metadata items found for package '${packageId}'` }],
|
|
372
|
-
};
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// Validation pass
|
|
376
|
-
if (shouldValidate) {
|
|
377
|
-
const validationErrors: Array<{ type: string; name: string; message: string }> = [];
|
|
378
|
-
|
|
379
|
-
// Schema validation
|
|
380
|
-
for (const item of packageItems) {
|
|
381
|
-
const result = await this.validate(item.type, item.data);
|
|
382
|
-
if (!result.valid && result.errors) {
|
|
383
|
-
for (const err of result.errors) {
|
|
384
|
-
validationErrors.push({
|
|
385
|
-
type: item.type,
|
|
386
|
-
name: item.name,
|
|
387
|
-
message: err.message,
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
// Dependency validation: referenced items must be in the same package or already published
|
|
394
|
-
const packageItemKeys = new Set(packageItems.map(i => `${i.type}:${i.name}`));
|
|
395
|
-
for (const item of packageItems) {
|
|
396
|
-
const deps = await this.getDependencies(item.type, item.name);
|
|
397
|
-
for (const dep of deps) {
|
|
398
|
-
const depKey = `${dep.targetType}:${dep.targetName}`;
|
|
399
|
-
// Skip if the dependency is within this package
|
|
400
|
-
if (packageItemKeys.has(depKey)) continue;
|
|
401
|
-
// Check if the dependency exists and has been published
|
|
402
|
-
const depItem = await this.get(dep.targetType, dep.targetName);
|
|
403
|
-
if (!depItem) {
|
|
404
|
-
validationErrors.push({
|
|
405
|
-
type: item.type,
|
|
406
|
-
name: item.name,
|
|
407
|
-
message: `Dependency '${dep.targetType}:${dep.targetName}' not found`,
|
|
408
|
-
});
|
|
409
|
-
} else {
|
|
410
|
-
const depMeta = depItem as any;
|
|
411
|
-
if (depMeta.publishedDefinition === undefined && depMeta.state !== 'active') {
|
|
412
|
-
validationErrors.push({
|
|
413
|
-
type: item.type,
|
|
414
|
-
name: item.name,
|
|
415
|
-
message: `Dependency '${dep.targetType}:${dep.targetName}' is not published`,
|
|
416
|
-
});
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
if (validationErrors.length > 0) {
|
|
423
|
-
return {
|
|
424
|
-
success: false,
|
|
425
|
-
packageId,
|
|
426
|
-
version: 0,
|
|
427
|
-
publishedAt: now,
|
|
428
|
-
itemsPublished: 0,
|
|
429
|
-
validationErrors,
|
|
430
|
-
};
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// Determine the next version by finding the max current version across items
|
|
435
|
-
let maxVersion = 0;
|
|
436
|
-
for (const item of packageItems) {
|
|
437
|
-
const v = typeof item.data.version === 'number' ? item.data.version : 0;
|
|
438
|
-
if (v > maxVersion) maxVersion = v;
|
|
439
|
-
}
|
|
440
|
-
const newVersion = maxVersion + 1;
|
|
441
|
-
|
|
442
|
-
// Snapshot and update all items
|
|
443
|
-
for (const item of packageItems) {
|
|
444
|
-
const updated = {
|
|
445
|
-
...item.data,
|
|
446
|
-
publishedDefinition: structuredClone(item.data.metadata ?? item.data),
|
|
447
|
-
publishedAt: now,
|
|
448
|
-
publishedBy: publishedBy ?? item.data.publishedBy,
|
|
449
|
-
version: newVersion,
|
|
450
|
-
state: 'active',
|
|
451
|
-
};
|
|
452
|
-
await this.register(item.type, item.name, updated);
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
return {
|
|
456
|
-
success: true,
|
|
457
|
-
packageId,
|
|
458
|
-
version: newVersion,
|
|
459
|
-
publishedAt: now,
|
|
460
|
-
itemsPublished: packageItems.length,
|
|
461
|
-
};
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
/**
|
|
465
|
-
* Revert entire package to last published state.
|
|
466
|
-
* Restores all metadata definitions from their published snapshots.
|
|
467
|
-
*/
|
|
468
|
-
async revertPackage(packageId: string): Promise<void> {
|
|
469
|
-
const packageItems: Array<{ type: string; name: string; data: any }> = [];
|
|
470
|
-
for (const [type, typeStore] of this.registry) {
|
|
471
|
-
for (const [name, data] of typeStore) {
|
|
472
|
-
const meta = data as any;
|
|
473
|
-
if (meta?.packageId === packageId || meta?.package === packageId) {
|
|
474
|
-
packageItems.push({ type, name, data: meta });
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
if (packageItems.length === 0) {
|
|
480
|
-
throw new Error(`No metadata items found for package '${packageId}'`);
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// Check that at least one item has a published snapshot
|
|
484
|
-
const hasPublished = packageItems.some(item => item.data.publishedDefinition !== undefined);
|
|
485
|
-
if (!hasPublished) {
|
|
486
|
-
throw new Error(`Package '${packageId}' has never been published`);
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
for (const item of packageItems) {
|
|
490
|
-
if (item.data.publishedDefinition !== undefined) {
|
|
491
|
-
const reverted = {
|
|
492
|
-
...item.data,
|
|
493
|
-
metadata: structuredClone(item.data.publishedDefinition),
|
|
494
|
-
state: 'active',
|
|
495
|
-
};
|
|
496
|
-
await this.register(item.type, item.name, reverted);
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
/**
|
|
502
|
-
* Get the published version of any metadata item (for runtime serving).
|
|
503
|
-
* Returns publishedDefinition if exists, else current definition.
|
|
504
|
-
*/
|
|
505
|
-
async getPublished(type: string, name: string): Promise<unknown | undefined> {
|
|
506
|
-
const item = await this.get(type, name);
|
|
507
|
-
if (!item) return undefined;
|
|
508
|
-
|
|
509
|
-
const meta = item as any;
|
|
510
|
-
if (meta.publishedDefinition !== undefined) {
|
|
511
|
-
return meta.publishedDefinition;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
// Fall back to current definition (metadata field or the item itself)
|
|
515
|
-
return meta.metadata ?? item;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// ==========================================
|
|
519
|
-
// Query / Search
|
|
520
|
-
// ==========================================
|
|
521
|
-
|
|
522
|
-
/**
|
|
523
|
-
* Query metadata items with filtering, sorting, and pagination
|
|
524
|
-
*/
|
|
525
|
-
async query(query: MetadataQuery): Promise<MetadataQueryResult> {
|
|
526
|
-
const { types, search, page = 1, pageSize = 50, sortBy = 'name', sortOrder = 'asc' } = query;
|
|
527
|
-
|
|
528
|
-
// Collect all items
|
|
529
|
-
const allItems: Array<{
|
|
530
|
-
type: string;
|
|
531
|
-
name: string;
|
|
532
|
-
namespace?: string;
|
|
533
|
-
label?: string;
|
|
534
|
-
scope?: 'system' | 'platform' | 'user';
|
|
535
|
-
state?: 'draft' | 'active' | 'archived' | 'deprecated';
|
|
536
|
-
packageId?: string;
|
|
537
|
-
updatedAt?: string;
|
|
538
|
-
}> = [];
|
|
539
|
-
|
|
540
|
-
// Determine which types to scan
|
|
541
|
-
const targetTypes = types && types.length > 0
|
|
542
|
-
? types
|
|
543
|
-
: Array.from(this.registry.keys());
|
|
544
|
-
|
|
545
|
-
for (const type of targetTypes) {
|
|
546
|
-
const items = await this.list(type);
|
|
547
|
-
for (const item of items) {
|
|
548
|
-
const meta = item as any;
|
|
549
|
-
allItems.push({
|
|
550
|
-
type,
|
|
551
|
-
name: meta?.name ?? '',
|
|
552
|
-
namespace: meta?.namespace,
|
|
553
|
-
label: meta?.label,
|
|
554
|
-
scope: meta?.scope,
|
|
555
|
-
state: meta?.state,
|
|
556
|
-
packageId: meta?.packageId,
|
|
557
|
-
updatedAt: meta?.updatedAt,
|
|
558
|
-
});
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
// Apply search filter
|
|
563
|
-
let filtered = allItems;
|
|
564
|
-
if (search) {
|
|
565
|
-
const searchLower = search.toLowerCase();
|
|
566
|
-
filtered = filtered.filter(item =>
|
|
567
|
-
item.name.toLowerCase().includes(searchLower) ||
|
|
568
|
-
(item.label && item.label.toLowerCase().includes(searchLower))
|
|
569
|
-
);
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
// Apply scope filter
|
|
573
|
-
if (query.scope) {
|
|
574
|
-
filtered = filtered.filter(item => item.scope === query.scope);
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
// Apply state filter
|
|
578
|
-
if (query.state) {
|
|
579
|
-
filtered = filtered.filter(item => item.state === query.state);
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
// Apply namespace filter
|
|
583
|
-
if (query.namespaces && query.namespaces.length > 0) {
|
|
584
|
-
filtered = filtered.filter(item => item.namespace && query.namespaces!.includes(item.namespace));
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
// Apply packageId filter
|
|
588
|
-
if (query.packageId) {
|
|
589
|
-
filtered = filtered.filter(item => item.packageId === query.packageId);
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
// Apply tags filter
|
|
593
|
-
if (query.tags && query.tags.length > 0) {
|
|
594
|
-
filtered = filtered.filter(item => {
|
|
595
|
-
const meta = item as any;
|
|
596
|
-
return meta?.tags && query.tags!.some((t: string) => meta.tags.includes(t));
|
|
597
|
-
});
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
// Sort
|
|
601
|
-
filtered.sort((a, b) => {
|
|
602
|
-
const aVal = (a as any)[sortBy] ?? '';
|
|
603
|
-
const bVal = (b as any)[sortBy] ?? '';
|
|
604
|
-
const cmp = String(aVal).localeCompare(String(bVal));
|
|
605
|
-
return sortOrder === 'desc' ? -cmp : cmp;
|
|
606
|
-
});
|
|
607
|
-
|
|
608
|
-
// Paginate
|
|
609
|
-
const total = filtered.length;
|
|
610
|
-
const start = (page - 1) * pageSize;
|
|
611
|
-
const paged = filtered.slice(start, start + pageSize);
|
|
612
|
-
|
|
613
|
-
return {
|
|
614
|
-
items: paged,
|
|
615
|
-
total,
|
|
616
|
-
page,
|
|
617
|
-
pageSize,
|
|
618
|
-
};
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
// ==========================================
|
|
622
|
-
// Bulk Operations
|
|
623
|
-
// ==========================================
|
|
624
|
-
|
|
625
|
-
/**
|
|
626
|
-
* Register multiple metadata items in a single batch
|
|
627
|
-
*/
|
|
628
|
-
async bulkRegister(
|
|
629
|
-
items: Array<{ type: string; name: string; data: unknown }>,
|
|
630
|
-
options?: { continueOnError?: boolean; validate?: boolean }
|
|
631
|
-
): Promise<MetadataBulkResult> {
|
|
632
|
-
const { continueOnError = false } = options ?? {};
|
|
633
|
-
let succeeded = 0;
|
|
634
|
-
let failed = 0;
|
|
635
|
-
const errors: Array<{ type: string; name: string; error: string }> = [];
|
|
636
|
-
|
|
637
|
-
for (const item of items) {
|
|
638
|
-
try {
|
|
639
|
-
await this.register(item.type, item.name, item.data);
|
|
640
|
-
succeeded++;
|
|
641
|
-
} catch (e) {
|
|
642
|
-
failed++;
|
|
643
|
-
errors.push({
|
|
644
|
-
type: item.type,
|
|
645
|
-
name: item.name,
|
|
646
|
-
error: e instanceof Error ? e.message : String(e),
|
|
647
|
-
});
|
|
648
|
-
if (!continueOnError) break;
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
return {
|
|
653
|
-
total: items.length,
|
|
654
|
-
succeeded,
|
|
655
|
-
failed,
|
|
656
|
-
errors: errors.length > 0 ? errors : undefined,
|
|
657
|
-
};
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
/**
|
|
661
|
-
* Unregister multiple metadata items in a single batch
|
|
662
|
-
*/
|
|
663
|
-
async bulkUnregister(items: Array<{ type: string; name: string }>): Promise<MetadataBulkResult> {
|
|
664
|
-
let succeeded = 0;
|
|
665
|
-
let failed = 0;
|
|
666
|
-
const errors: Array<{ type: string; name: string; error: string }> = [];
|
|
667
|
-
|
|
668
|
-
for (const item of items) {
|
|
669
|
-
try {
|
|
670
|
-
await this.unregister(item.type, item.name);
|
|
671
|
-
succeeded++;
|
|
672
|
-
} catch (e) {
|
|
673
|
-
failed++;
|
|
674
|
-
errors.push({
|
|
675
|
-
type: item.type,
|
|
676
|
-
name: item.name,
|
|
677
|
-
error: e instanceof Error ? e.message : String(e),
|
|
678
|
-
});
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
return {
|
|
683
|
-
total: items.length,
|
|
684
|
-
succeeded,
|
|
685
|
-
failed,
|
|
686
|
-
errors: errors.length > 0 ? errors : undefined,
|
|
687
|
-
};
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
// ==========================================
|
|
691
|
-
// Overlay / Customization Management
|
|
692
|
-
// ==========================================
|
|
693
|
-
|
|
694
|
-
private overlayKey(type: string, name: string, scope: string = 'platform'): string {
|
|
695
|
-
return `${encodeURIComponent(type)}:${encodeURIComponent(name)}:${scope}`;
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
/**
|
|
699
|
-
* Get the active overlay for a metadata item
|
|
700
|
-
*/
|
|
701
|
-
async getOverlay(type: string, name: string, scope?: 'platform' | 'user'): Promise<MetadataOverlay | undefined> {
|
|
702
|
-
return this.overlays.get(this.overlayKey(type, name, scope ?? 'platform'));
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
/**
|
|
706
|
-
* Save/update an overlay for a metadata item
|
|
707
|
-
*/
|
|
708
|
-
async saveOverlay(overlay: MetadataOverlay): Promise<void> {
|
|
709
|
-
const key = this.overlayKey(overlay.baseType, overlay.baseName, overlay.scope);
|
|
710
|
-
this.overlays.set(key, overlay);
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
/**
|
|
714
|
-
* Remove an overlay, reverting to the base definition
|
|
715
|
-
*/
|
|
716
|
-
async removeOverlay(type: string, name: string, scope?: 'platform' | 'user'): Promise<void> {
|
|
717
|
-
this.overlays.delete(this.overlayKey(type, name, scope ?? 'platform'));
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
/**
|
|
721
|
-
* Get the effective (merged) metadata after applying all overlays.
|
|
722
|
-
* Resolution order: system ← merge(platform) ← merge(user)
|
|
723
|
-
*/
|
|
724
|
-
async getEffective(type: string, name: string, context?: {
|
|
725
|
-
userId?: string;
|
|
726
|
-
tenantId?: string;
|
|
727
|
-
roles?: string[];
|
|
728
|
-
permissions?: string[];
|
|
729
|
-
}): Promise<unknown | undefined> {
|
|
730
|
-
const base = await this.get(type, name);
|
|
731
|
-
if (!base) return undefined;
|
|
732
|
-
|
|
733
|
-
let effective = { ...(base as Record<string, unknown>) };
|
|
734
|
-
|
|
735
|
-
// Apply platform overlay
|
|
736
|
-
const platformOverlay = await this.getOverlay(type, name, 'platform');
|
|
737
|
-
if (platformOverlay?.active && platformOverlay.patch) {
|
|
738
|
-
effective = { ...effective, ...platformOverlay.patch };
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
// Apply user overlay (scoped to specific user if context provided)
|
|
742
|
-
if (context?.userId) {
|
|
743
|
-
// Try user-specific key first, then fall back to generic user overlay.
|
|
744
|
-
// The owner check below ensures we never apply another user's overlay.
|
|
745
|
-
const userOverlayKey = this.overlayKey(type, name, 'user') + `:${context.userId}`;
|
|
746
|
-
const userOverlay = this.overlays.get(userOverlayKey)
|
|
747
|
-
?? await this.getOverlay(type, name, 'user');
|
|
748
|
-
if (userOverlay?.active && userOverlay.patch) {
|
|
749
|
-
// Apply if: overlay has no owner (generic user-level), or owner matches current user
|
|
750
|
-
if (!userOverlay.owner || userOverlay.owner === context.userId) {
|
|
751
|
-
effective = { ...effective, ...userOverlay.patch };
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
} else {
|
|
755
|
-
// No user context — only apply user overlays without an owner restriction
|
|
756
|
-
// (owner-scoped overlays require a userId to resolve)
|
|
757
|
-
const userOverlay = await this.getOverlay(type, name, 'user');
|
|
758
|
-
if (userOverlay?.active && userOverlay.patch && !userOverlay.owner) {
|
|
759
|
-
effective = { ...effective, ...userOverlay.patch };
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
return effective;
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
// ==========================================
|
|
767
|
-
// Watch / Subscribe (IMetadataService)
|
|
768
|
-
// ==========================================
|
|
769
|
-
|
|
770
|
-
/**
|
|
771
|
-
* Watch for metadata changes (IMetadataService contract).
|
|
772
|
-
* Returns a handle for unsubscribing.
|
|
773
|
-
*/
|
|
774
|
-
watchService(type: string, callback: MetadataWatchCallback): MetadataWatchHandle {
|
|
775
|
-
const wrappedCallback: WatchCallback = (event) => {
|
|
776
|
-
const mappedType = event.type === 'added' ? 'registered'
|
|
777
|
-
: event.type === 'deleted' ? 'unregistered'
|
|
778
|
-
: 'updated';
|
|
779
|
-
callback({
|
|
780
|
-
type: mappedType,
|
|
781
|
-
metadataType: event.metadataType ?? type,
|
|
782
|
-
name: event.name ?? '',
|
|
783
|
-
data: event.data,
|
|
784
|
-
});
|
|
785
|
-
};
|
|
786
|
-
this.addWatchCallback(type, wrappedCallback);
|
|
787
|
-
return {
|
|
788
|
-
unsubscribe: () => this.removeWatchCallback(type, wrappedCallback),
|
|
789
|
-
};
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
// ==========================================
|
|
793
|
-
// Import / Export
|
|
794
|
-
// ==========================================
|
|
795
|
-
|
|
796
|
-
/**
|
|
797
|
-
* Export metadata as a portable bundle
|
|
798
|
-
*/
|
|
799
|
-
async exportMetadata(options?: MetadataExportOptions): Promise<unknown> {
|
|
800
|
-
const bundle: Record<string, unknown[]> = {};
|
|
801
|
-
const targetTypes = options?.types ?? Array.from(this.registry.keys());
|
|
802
|
-
|
|
803
|
-
for (const type of targetTypes) {
|
|
804
|
-
const items = await this.list(type);
|
|
805
|
-
if (items.length > 0) {
|
|
806
|
-
bundle[type] = items;
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
return bundle;
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
/**
|
|
814
|
-
* Import metadata from a portable bundle
|
|
815
|
-
*/
|
|
816
|
-
async importMetadata(data: unknown, options?: MetadataImportOptions): Promise<MetadataImportResult> {
|
|
817
|
-
const {
|
|
818
|
-
conflictResolution = 'skip',
|
|
819
|
-
validate: _validate = true,
|
|
820
|
-
dryRun = false,
|
|
821
|
-
} = options ?? {};
|
|
822
|
-
|
|
823
|
-
const bundle = data as Record<string, unknown[]>;
|
|
824
|
-
let total = 0;
|
|
825
|
-
let imported = 0;
|
|
826
|
-
let skipped = 0;
|
|
827
|
-
let failed = 0;
|
|
828
|
-
const errors: Array<{ type: string; name: string; error: string }> = [];
|
|
829
|
-
|
|
830
|
-
for (const [type, items] of Object.entries(bundle)) {
|
|
831
|
-
if (!Array.isArray(items)) continue;
|
|
832
|
-
|
|
833
|
-
for (const item of items) {
|
|
834
|
-
total++;
|
|
835
|
-
const meta = item as any;
|
|
836
|
-
const name = meta?.name;
|
|
837
|
-
|
|
838
|
-
if (!name) {
|
|
839
|
-
failed++;
|
|
840
|
-
errors.push({ type, name: '(unknown)', error: 'Item missing name field' });
|
|
841
|
-
continue;
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
try {
|
|
845
|
-
const itemExists = await this.exists(type, name);
|
|
846
|
-
|
|
847
|
-
if (itemExists && conflictResolution === 'skip') {
|
|
848
|
-
skipped++;
|
|
849
|
-
continue;
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
if (!dryRun) {
|
|
853
|
-
if (itemExists && conflictResolution === 'merge') {
|
|
854
|
-
const existing = await this.get(type, name);
|
|
855
|
-
const merged = { ...(existing as any), ...(item as any) };
|
|
856
|
-
await this.register(type, name, merged);
|
|
857
|
-
} else {
|
|
858
|
-
await this.register(type, name, item);
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
imported++;
|
|
862
|
-
} catch (e) {
|
|
863
|
-
failed++;
|
|
864
|
-
errors.push({
|
|
865
|
-
type,
|
|
866
|
-
name,
|
|
867
|
-
error: e instanceof Error ? e.message : String(e),
|
|
868
|
-
});
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
return {
|
|
874
|
-
total,
|
|
875
|
-
imported,
|
|
876
|
-
skipped,
|
|
877
|
-
failed,
|
|
878
|
-
errors: errors.length > 0 ? errors : undefined,
|
|
879
|
-
};
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
// ==========================================
|
|
883
|
-
// Validation
|
|
884
|
-
// ==========================================
|
|
885
|
-
|
|
886
|
-
/**
|
|
887
|
-
* Validate a metadata item against its type schema.
|
|
888
|
-
* Returns validation result with errors and warnings.
|
|
889
|
-
*/
|
|
890
|
-
async validate(_type: string, data: unknown): Promise<MetadataValidationResult> {
|
|
891
|
-
// Basic structural validation
|
|
892
|
-
if (data === null || data === undefined) {
|
|
893
|
-
return {
|
|
894
|
-
valid: false,
|
|
895
|
-
errors: [{ path: '', message: 'Metadata data cannot be null or undefined' }],
|
|
896
|
-
};
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
if (typeof data !== 'object') {
|
|
900
|
-
return {
|
|
901
|
-
valid: false,
|
|
902
|
-
errors: [{ path: '', message: 'Metadata data must be an object' }],
|
|
903
|
-
};
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
const meta = data as any;
|
|
907
|
-
const warnings: Array<{ path: string; message: string }> = [];
|
|
908
|
-
|
|
909
|
-
if (!meta.name) {
|
|
910
|
-
return {
|
|
911
|
-
valid: false,
|
|
912
|
-
errors: [{ path: 'name', message: 'Metadata item must have a name field' }],
|
|
913
|
-
};
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
if (!meta.label) {
|
|
917
|
-
warnings.push({ path: 'label', message: 'Missing label field (recommended)' });
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
return { valid: true, warnings: warnings.length > 0 ? warnings : undefined };
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
// ==========================================
|
|
924
|
-
// Type Registry
|
|
925
|
-
// ==========================================
|
|
926
|
-
|
|
927
|
-
/**
|
|
928
|
-
* Get all registered metadata types
|
|
929
|
-
*/
|
|
930
|
-
async getRegisteredTypes(): Promise<string[]> {
|
|
931
|
-
const types = new Set<string>();
|
|
932
|
-
|
|
933
|
-
// From type registry
|
|
934
|
-
for (const entry of this.typeRegistry) {
|
|
935
|
-
types.add(entry.type);
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
// From in-memory registry (custom types)
|
|
939
|
-
for (const type of this.registry.keys()) {
|
|
940
|
-
types.add(type);
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
return Array.from(types);
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
/**
|
|
947
|
-
* Get detailed information about a metadata type
|
|
948
|
-
*/
|
|
949
|
-
async getTypeInfo(type: string): Promise<MetadataTypeInfo | undefined> {
|
|
950
|
-
const entry = this.typeRegistry.find(e => e.type === type);
|
|
951
|
-
if (!entry) return undefined;
|
|
952
|
-
|
|
953
|
-
return {
|
|
954
|
-
type: entry.type,
|
|
955
|
-
label: entry.label,
|
|
956
|
-
description: entry.description,
|
|
957
|
-
filePatterns: entry.filePatterns,
|
|
958
|
-
supportsOverlay: entry.supportsOverlay,
|
|
959
|
-
domain: entry.domain,
|
|
960
|
-
};
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
// ==========================================
|
|
964
|
-
// Dependency Tracking
|
|
965
|
-
// ==========================================
|
|
966
|
-
|
|
967
|
-
/**
|
|
968
|
-
* Get metadata items that this item depends on
|
|
969
|
-
*/
|
|
970
|
-
async getDependencies(type: string, name: string): Promise<MetadataDependency[]> {
|
|
971
|
-
return this.dependencies.get(`${encodeURIComponent(type)}:${encodeURIComponent(name)}`) ?? [];
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
/**
|
|
975
|
-
* Get metadata items that depend on this item
|
|
976
|
-
*/
|
|
977
|
-
async getDependents(type: string, name: string): Promise<MetadataDependency[]> {
|
|
978
|
-
const dependents: MetadataDependency[] = [];
|
|
979
|
-
for (const deps of this.dependencies.values()) {
|
|
980
|
-
for (const dep of deps) {
|
|
981
|
-
if (dep.targetType === type && dep.targetName === name) {
|
|
982
|
-
dependents.push(dep);
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
return dependents;
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
/**
|
|
990
|
-
* Register a dependency between two metadata items.
|
|
991
|
-
* Used internally to track cross-references.
|
|
992
|
-
* Duplicate dependencies (same source, target, and kind) are ignored.
|
|
993
|
-
*/
|
|
994
|
-
addDependency(dep: MetadataDependency): void {
|
|
995
|
-
const key = `${encodeURIComponent(dep.sourceType)}:${encodeURIComponent(dep.sourceName)}`;
|
|
996
|
-
if (!this.dependencies.has(key)) {
|
|
997
|
-
this.dependencies.set(key, []);
|
|
998
|
-
}
|
|
999
|
-
const existing = this.dependencies.get(key)!;
|
|
1000
|
-
const isDuplicate = existing.some(
|
|
1001
|
-
d => d.targetType === dep.targetType && d.targetName === dep.targetName && d.kind === dep.kind
|
|
1002
|
-
);
|
|
1003
|
-
if (!isDuplicate) {
|
|
1004
|
-
existing.push(dep);
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
// ==========================================
|
|
1009
|
-
// Legacy Loader API (backward compatible)
|
|
1010
|
-
// ==========================================
|
|
1011
|
-
|
|
1012
|
-
/**
|
|
1013
|
-
* Load a single metadata item from loaders.
|
|
1014
|
-
* Iterates through registered loaders until found.
|
|
1015
|
-
*/
|
|
1016
|
-
async load<T = any>(
|
|
1017
|
-
type: string,
|
|
1018
|
-
name: string,
|
|
1019
|
-
options?: MetadataLoadOptions
|
|
1020
|
-
): Promise<T | null> {
|
|
1021
|
-
for (const loader of this.loaders.values()) {
|
|
1022
|
-
try {
|
|
1023
|
-
const result = await loader.load(type, name, options);
|
|
1024
|
-
if (result.data) {
|
|
1025
|
-
return result.data as T;
|
|
1026
|
-
}
|
|
1027
|
-
} catch (e) {
|
|
1028
|
-
this.logger.warn(`Loader ${loader.contract.name} failed to load ${type}:${name}`, { error: e });
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
return null;
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
/**
|
|
1035
|
-
* Load multiple metadata items from loaders.
|
|
1036
|
-
* Aggregates results from all loaders.
|
|
1037
|
-
*/
|
|
1038
|
-
async loadMany<T = any>(
|
|
1039
|
-
type: string,
|
|
1040
|
-
options?: MetadataLoadOptions
|
|
1041
|
-
): Promise<T[]> {
|
|
1042
|
-
const results: T[] = [];
|
|
1043
|
-
|
|
1044
|
-
for (const loader of this.loaders.values()) {
|
|
1045
|
-
try {
|
|
1046
|
-
const items = await loader.loadMany<T>(type, options);
|
|
1047
|
-
for (const item of items) {
|
|
1048
|
-
const itemAny = item as any;
|
|
1049
|
-
if (itemAny && typeof itemAny.name === 'string') {
|
|
1050
|
-
const exists = results.some((r: any) => r && r.name === itemAny.name);
|
|
1051
|
-
if (exists) continue;
|
|
1052
|
-
}
|
|
1053
|
-
results.push(item);
|
|
1054
|
-
}
|
|
1055
|
-
} catch (e) {
|
|
1056
|
-
this.logger.warn(`Loader ${loader.contract.name} failed to loadMany ${type}`, { error: e });
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1059
|
-
return results;
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
/**
|
|
1063
|
-
* Save metadata item to a loader
|
|
1064
|
-
*/
|
|
1065
|
-
async save<T = any>(
|
|
1066
|
-
type: string,
|
|
1067
|
-
name: string,
|
|
1068
|
-
data: T,
|
|
1069
|
-
options?: MetadataSaveOptions
|
|
1070
|
-
): Promise<MetadataSaveResult> {
|
|
1071
|
-
const targetLoader = (options as any)?.loader;
|
|
1072
|
-
|
|
1073
|
-
let loader: MetadataLoader | undefined;
|
|
1074
|
-
|
|
1075
|
-
if (targetLoader) {
|
|
1076
|
-
loader = this.loaders.get(targetLoader);
|
|
1077
|
-
if (!loader) {
|
|
1078
|
-
throw new Error(`Loader not found: ${targetLoader}`);
|
|
1079
|
-
}
|
|
1080
|
-
} else {
|
|
1081
|
-
for (const l of this.loaders.values()) {
|
|
1082
|
-
if (!l.save) continue;
|
|
1083
|
-
try {
|
|
1084
|
-
if (await l.exists(type, name)) {
|
|
1085
|
-
loader = l;
|
|
1086
|
-
this.logger.info(`Updating existing metadata in loader: ${l.contract.name}`);
|
|
1087
|
-
break;
|
|
1088
|
-
}
|
|
1089
|
-
} catch (e) {
|
|
1090
|
-
// Ignore existence check errors
|
|
1091
|
-
}
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
|
-
if (!loader) {
|
|
1095
|
-
const fsLoader = this.loaders.get('filesystem');
|
|
1096
|
-
if (fsLoader && fsLoader.save) {
|
|
1097
|
-
loader = fsLoader;
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
if (!loader) {
|
|
1102
|
-
for (const l of this.loaders.values()) {
|
|
1103
|
-
if (l.save) {
|
|
1104
|
-
loader = l;
|
|
1105
|
-
break;
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
if (!loader) {
|
|
1112
|
-
throw new Error(`No loader available for saving type: ${type}`);
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
if (!loader.save) {
|
|
1116
|
-
throw new Error(`Loader '${loader.contract?.name}' does not support saving`);
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
return loader.save(type, name, data, options);
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
/**
|
|
1123
|
-
* Register a watch callback for metadata changes
|
|
1124
|
-
*/
|
|
1125
|
-
protected addWatchCallback(type: string, callback: WatchCallback): void {
|
|
1126
|
-
if (!this.watchCallbacks.has(type)) {
|
|
1127
|
-
this.watchCallbacks.set(type, new Set());
|
|
1128
|
-
}
|
|
1129
|
-
this.watchCallbacks.get(type)!.add(callback);
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
/**
|
|
1133
|
-
* Remove a watch callback for metadata changes
|
|
1134
|
-
*/
|
|
1135
|
-
protected removeWatchCallback(type: string, callback: WatchCallback): void {
|
|
1136
|
-
const callbacks = this.watchCallbacks.get(type);
|
|
1137
|
-
if (callbacks) {
|
|
1138
|
-
callbacks.delete(callback);
|
|
1139
|
-
if (callbacks.size === 0) {
|
|
1140
|
-
this.watchCallbacks.delete(type);
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
/**
|
|
1146
|
-
* Stop all watching
|
|
1147
|
-
*/
|
|
1148
|
-
async stopWatching(): Promise<void> {
|
|
1149
|
-
// Override in subclass
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
protected notifyWatchers(type: string, event: MetadataWatchEvent) {
|
|
1153
|
-
const callbacks = this.watchCallbacks.get(type);
|
|
1154
|
-
if (!callbacks) return;
|
|
1155
|
-
|
|
1156
|
-
for (const callback of callbacks) {
|
|
1157
|
-
try {
|
|
1158
|
-
void callback(event);
|
|
1159
|
-
} catch (error) {
|
|
1160
|
-
this.logger.error('Watch callback error', undefined, {
|
|
1161
|
-
type,
|
|
1162
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1163
|
-
});
|
|
1164
|
-
}
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
|