@objectstack/metadata 2.0.7 → 3.0.1
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 +542 -42
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +542 -42
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +24 -1
- package/src/metadata-manager.ts +680 -49
- package/src/metadata-service.test.ts +611 -0
- package/src/metadata.test.ts +15 -17
- package/src/plugin.ts +23 -14
- package/vitest.config.ts +2 -0
package/dist/index.mjs
CHANGED
|
@@ -179,6 +179,14 @@ var MetadataManager = class {
|
|
|
179
179
|
constructor(config) {
|
|
180
180
|
this.loaders = /* @__PURE__ */ new Map();
|
|
181
181
|
this.watchCallbacks = /* @__PURE__ */ new Map();
|
|
182
|
+
// In-memory metadata registry: type -> name -> data
|
|
183
|
+
this.registry = /* @__PURE__ */ new Map();
|
|
184
|
+
// Overlay storage: "type:name:scope" -> MetadataOverlay
|
|
185
|
+
this.overlays = /* @__PURE__ */ new Map();
|
|
186
|
+
// Type registry for metadata type info
|
|
187
|
+
this.typeRegistry = [];
|
|
188
|
+
// Dependency tracking: "type:name" -> dependencies
|
|
189
|
+
this.dependencies = /* @__PURE__ */ new Map();
|
|
182
190
|
this.config = config;
|
|
183
191
|
this.logger = createLogger({ level: "info", format: "pretty" });
|
|
184
192
|
this.serializers = /* @__PURE__ */ new Map();
|
|
@@ -199,6 +207,12 @@ var MetadataManager = class {
|
|
|
199
207
|
config.loaders.forEach((loader) => this.registerLoader(loader));
|
|
200
208
|
}
|
|
201
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* Set the type registry for metadata type discovery.
|
|
212
|
+
*/
|
|
213
|
+
setTypeRegistry(entries) {
|
|
214
|
+
this.typeRegistry = entries;
|
|
215
|
+
}
|
|
202
216
|
/**
|
|
203
217
|
* Register a new metadata loader (data source)
|
|
204
218
|
*/
|
|
@@ -206,9 +220,513 @@ var MetadataManager = class {
|
|
|
206
220
|
this.loaders.set(loader.contract.name, loader);
|
|
207
221
|
this.logger.info(`Registered metadata loader: ${loader.contract.name} (${loader.contract.protocol})`);
|
|
208
222
|
}
|
|
223
|
+
// ==========================================
|
|
224
|
+
// IMetadataService — Core CRUD Operations
|
|
225
|
+
// ==========================================
|
|
226
|
+
/**
|
|
227
|
+
* Register/save a metadata item by type
|
|
228
|
+
*/
|
|
229
|
+
async register(type, name, data) {
|
|
230
|
+
if (!this.registry.has(type)) {
|
|
231
|
+
this.registry.set(type, /* @__PURE__ */ new Map());
|
|
232
|
+
}
|
|
233
|
+
this.registry.get(type).set(name, data);
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Get a metadata item by type and name.
|
|
237
|
+
* Checks in-memory registry first, then falls back to loaders.
|
|
238
|
+
*/
|
|
239
|
+
async get(type, name) {
|
|
240
|
+
const typeStore = this.registry.get(type);
|
|
241
|
+
if (typeStore?.has(name)) {
|
|
242
|
+
return typeStore.get(name);
|
|
243
|
+
}
|
|
244
|
+
const result = await this.load(type, name);
|
|
245
|
+
return result ?? void 0;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* List all metadata items of a given type
|
|
249
|
+
*/
|
|
250
|
+
async list(type) {
|
|
251
|
+
const items = /* @__PURE__ */ new Map();
|
|
252
|
+
const typeStore = this.registry.get(type);
|
|
253
|
+
if (typeStore) {
|
|
254
|
+
for (const [name, data] of typeStore) {
|
|
255
|
+
items.set(name, data);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
for (const loader of this.loaders.values()) {
|
|
259
|
+
try {
|
|
260
|
+
const loaderItems = await loader.loadMany(type);
|
|
261
|
+
for (const item of loaderItems) {
|
|
262
|
+
const itemAny = item;
|
|
263
|
+
if (itemAny && typeof itemAny.name === "string" && !items.has(itemAny.name)) {
|
|
264
|
+
items.set(itemAny.name, item);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
} catch (e) {
|
|
268
|
+
this.logger.warn(`Loader ${loader.contract.name} failed to loadMany ${type}`, { error: e });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return Array.from(items.values());
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Unregister/remove a metadata item by type and name
|
|
275
|
+
*/
|
|
276
|
+
async unregister(type, name) {
|
|
277
|
+
const typeStore = this.registry.get(type);
|
|
278
|
+
if (typeStore) {
|
|
279
|
+
typeStore.delete(name);
|
|
280
|
+
if (typeStore.size === 0) {
|
|
281
|
+
this.registry.delete(type);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Check if a metadata item exists
|
|
287
|
+
*/
|
|
288
|
+
async exists(type, name) {
|
|
289
|
+
if (this.registry.get(type)?.has(name)) {
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
for (const loader of this.loaders.values()) {
|
|
293
|
+
if (await loader.exists(type, name)) {
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* List all names of metadata items of a given type
|
|
301
|
+
*/
|
|
302
|
+
async listNames(type) {
|
|
303
|
+
const names = /* @__PURE__ */ new Set();
|
|
304
|
+
const typeStore = this.registry.get(type);
|
|
305
|
+
if (typeStore) {
|
|
306
|
+
for (const name of typeStore.keys()) {
|
|
307
|
+
names.add(name);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
for (const loader of this.loaders.values()) {
|
|
311
|
+
const result = await loader.list(type);
|
|
312
|
+
result.forEach((item) => names.add(item));
|
|
313
|
+
}
|
|
314
|
+
return Array.from(names);
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Convenience: get an object definition by name
|
|
318
|
+
*/
|
|
319
|
+
async getObject(name) {
|
|
320
|
+
return this.get("object", name);
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Convenience: list all object definitions
|
|
324
|
+
*/
|
|
325
|
+
async listObjects() {
|
|
326
|
+
return this.list("object");
|
|
327
|
+
}
|
|
328
|
+
// ==========================================
|
|
329
|
+
// Package Management
|
|
330
|
+
// ==========================================
|
|
331
|
+
/**
|
|
332
|
+
* Unregister all metadata items from a specific package
|
|
333
|
+
*/
|
|
334
|
+
async unregisterPackage(packageName) {
|
|
335
|
+
for (const [type, typeStore] of this.registry) {
|
|
336
|
+
const toDelete = [];
|
|
337
|
+
for (const [name, data] of typeStore) {
|
|
338
|
+
const meta = data;
|
|
339
|
+
if (meta?.packageId === packageName || meta?.package === packageName) {
|
|
340
|
+
toDelete.push(name);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
for (const name of toDelete) {
|
|
344
|
+
typeStore.delete(name);
|
|
345
|
+
}
|
|
346
|
+
if (typeStore.size === 0) {
|
|
347
|
+
this.registry.delete(type);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
// ==========================================
|
|
352
|
+
// Query / Search
|
|
353
|
+
// ==========================================
|
|
354
|
+
/**
|
|
355
|
+
* Query metadata items with filtering, sorting, and pagination
|
|
356
|
+
*/
|
|
357
|
+
async query(query) {
|
|
358
|
+
const { types, search, page = 1, pageSize = 50, sortBy = "name", sortOrder = "asc" } = query;
|
|
359
|
+
const allItems = [];
|
|
360
|
+
const targetTypes = types && types.length > 0 ? types : Array.from(this.registry.keys());
|
|
361
|
+
for (const type of targetTypes) {
|
|
362
|
+
const items = await this.list(type);
|
|
363
|
+
for (const item of items) {
|
|
364
|
+
const meta = item;
|
|
365
|
+
allItems.push({
|
|
366
|
+
type,
|
|
367
|
+
name: meta?.name ?? "",
|
|
368
|
+
namespace: meta?.namespace,
|
|
369
|
+
label: meta?.label,
|
|
370
|
+
scope: meta?.scope,
|
|
371
|
+
state: meta?.state,
|
|
372
|
+
packageId: meta?.packageId,
|
|
373
|
+
updatedAt: meta?.updatedAt
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
let filtered = allItems;
|
|
378
|
+
if (search) {
|
|
379
|
+
const searchLower = search.toLowerCase();
|
|
380
|
+
filtered = filtered.filter(
|
|
381
|
+
(item) => item.name.toLowerCase().includes(searchLower) || item.label && item.label.toLowerCase().includes(searchLower)
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
if (query.scope) {
|
|
385
|
+
filtered = filtered.filter((item) => item.scope === query.scope);
|
|
386
|
+
}
|
|
387
|
+
if (query.state) {
|
|
388
|
+
filtered = filtered.filter((item) => item.state === query.state);
|
|
389
|
+
}
|
|
390
|
+
if (query.namespaces && query.namespaces.length > 0) {
|
|
391
|
+
filtered = filtered.filter((item) => item.namespace && query.namespaces.includes(item.namespace));
|
|
392
|
+
}
|
|
393
|
+
if (query.packageId) {
|
|
394
|
+
filtered = filtered.filter((item) => item.packageId === query.packageId);
|
|
395
|
+
}
|
|
396
|
+
if (query.tags && query.tags.length > 0) {
|
|
397
|
+
filtered = filtered.filter((item) => {
|
|
398
|
+
const meta = item;
|
|
399
|
+
return meta?.tags && query.tags.some((t) => meta.tags.includes(t));
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
filtered.sort((a, b) => {
|
|
403
|
+
const aVal = a[sortBy] ?? "";
|
|
404
|
+
const bVal = b[sortBy] ?? "";
|
|
405
|
+
const cmp = String(aVal).localeCompare(String(bVal));
|
|
406
|
+
return sortOrder === "desc" ? -cmp : cmp;
|
|
407
|
+
});
|
|
408
|
+
const total = filtered.length;
|
|
409
|
+
const start = (page - 1) * pageSize;
|
|
410
|
+
const paged = filtered.slice(start, start + pageSize);
|
|
411
|
+
return {
|
|
412
|
+
items: paged,
|
|
413
|
+
total,
|
|
414
|
+
page,
|
|
415
|
+
pageSize
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
// ==========================================
|
|
419
|
+
// Bulk Operations
|
|
420
|
+
// ==========================================
|
|
421
|
+
/**
|
|
422
|
+
* Register multiple metadata items in a single batch
|
|
423
|
+
*/
|
|
424
|
+
async bulkRegister(items, options) {
|
|
425
|
+
const { continueOnError = false } = options ?? {};
|
|
426
|
+
let succeeded = 0;
|
|
427
|
+
let failed = 0;
|
|
428
|
+
const errors = [];
|
|
429
|
+
for (const item of items) {
|
|
430
|
+
try {
|
|
431
|
+
await this.register(item.type, item.name, item.data);
|
|
432
|
+
succeeded++;
|
|
433
|
+
} catch (e) {
|
|
434
|
+
failed++;
|
|
435
|
+
errors.push({
|
|
436
|
+
type: item.type,
|
|
437
|
+
name: item.name,
|
|
438
|
+
error: e instanceof Error ? e.message : String(e)
|
|
439
|
+
});
|
|
440
|
+
if (!continueOnError) break;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return {
|
|
444
|
+
total: items.length,
|
|
445
|
+
succeeded,
|
|
446
|
+
failed,
|
|
447
|
+
errors: errors.length > 0 ? errors : void 0
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Unregister multiple metadata items in a single batch
|
|
452
|
+
*/
|
|
453
|
+
async bulkUnregister(items) {
|
|
454
|
+
let succeeded = 0;
|
|
455
|
+
let failed = 0;
|
|
456
|
+
const errors = [];
|
|
457
|
+
for (const item of items) {
|
|
458
|
+
try {
|
|
459
|
+
await this.unregister(item.type, item.name);
|
|
460
|
+
succeeded++;
|
|
461
|
+
} catch (e) {
|
|
462
|
+
failed++;
|
|
463
|
+
errors.push({
|
|
464
|
+
type: item.type,
|
|
465
|
+
name: item.name,
|
|
466
|
+
error: e instanceof Error ? e.message : String(e)
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return {
|
|
471
|
+
total: items.length,
|
|
472
|
+
succeeded,
|
|
473
|
+
failed,
|
|
474
|
+
errors: errors.length > 0 ? errors : void 0
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
// ==========================================
|
|
478
|
+
// Overlay / Customization Management
|
|
479
|
+
// ==========================================
|
|
480
|
+
overlayKey(type, name, scope = "platform") {
|
|
481
|
+
return `${encodeURIComponent(type)}:${encodeURIComponent(name)}:${scope}`;
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Get the active overlay for a metadata item
|
|
485
|
+
*/
|
|
486
|
+
async getOverlay(type, name, scope) {
|
|
487
|
+
return this.overlays.get(this.overlayKey(type, name, scope ?? "platform"));
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Save/update an overlay for a metadata item
|
|
491
|
+
*/
|
|
492
|
+
async saveOverlay(overlay) {
|
|
493
|
+
const key = this.overlayKey(overlay.baseType, overlay.baseName, overlay.scope);
|
|
494
|
+
this.overlays.set(key, overlay);
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Remove an overlay, reverting to the base definition
|
|
498
|
+
*/
|
|
499
|
+
async removeOverlay(type, name, scope) {
|
|
500
|
+
this.overlays.delete(this.overlayKey(type, name, scope ?? "platform"));
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Get the effective (merged) metadata after applying all overlays.
|
|
504
|
+
* Resolution order: system ← merge(platform) ← merge(user)
|
|
505
|
+
*/
|
|
506
|
+
async getEffective(type, name) {
|
|
507
|
+
const base = await this.get(type, name);
|
|
508
|
+
if (!base) return void 0;
|
|
509
|
+
let effective = { ...base };
|
|
510
|
+
const platformOverlay = await this.getOverlay(type, name, "platform");
|
|
511
|
+
if (platformOverlay?.active && platformOverlay.patch) {
|
|
512
|
+
effective = { ...effective, ...platformOverlay.patch };
|
|
513
|
+
}
|
|
514
|
+
const userOverlay = await this.getOverlay(type, name, "user");
|
|
515
|
+
if (userOverlay?.active && userOverlay.patch) {
|
|
516
|
+
effective = { ...effective, ...userOverlay.patch };
|
|
517
|
+
}
|
|
518
|
+
return effective;
|
|
519
|
+
}
|
|
520
|
+
// ==========================================
|
|
521
|
+
// Watch / Subscribe (IMetadataService)
|
|
522
|
+
// ==========================================
|
|
209
523
|
/**
|
|
210
|
-
*
|
|
211
|
-
*
|
|
524
|
+
* Watch for metadata changes (IMetadataService contract).
|
|
525
|
+
* Returns a handle for unsubscribing.
|
|
526
|
+
*/
|
|
527
|
+
watchService(type, callback) {
|
|
528
|
+
const wrappedCallback = (event) => {
|
|
529
|
+
const mappedType = event.type === "added" ? "registered" : event.type === "deleted" ? "unregistered" : "updated";
|
|
530
|
+
callback({
|
|
531
|
+
type: mappedType,
|
|
532
|
+
metadataType: event.metadataType ?? type,
|
|
533
|
+
name: event.name ?? "",
|
|
534
|
+
data: event.data
|
|
535
|
+
});
|
|
536
|
+
};
|
|
537
|
+
this.addWatchCallback(type, wrappedCallback);
|
|
538
|
+
return {
|
|
539
|
+
unsubscribe: () => this.removeWatchCallback(type, wrappedCallback)
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
// ==========================================
|
|
543
|
+
// Import / Export
|
|
544
|
+
// ==========================================
|
|
545
|
+
/**
|
|
546
|
+
* Export metadata as a portable bundle
|
|
547
|
+
*/
|
|
548
|
+
async exportMetadata(options) {
|
|
549
|
+
const bundle = {};
|
|
550
|
+
const targetTypes = options?.types ?? Array.from(this.registry.keys());
|
|
551
|
+
for (const type of targetTypes) {
|
|
552
|
+
const items = await this.list(type);
|
|
553
|
+
if (items.length > 0) {
|
|
554
|
+
bundle[type] = items;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return bundle;
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Import metadata from a portable bundle
|
|
561
|
+
*/
|
|
562
|
+
async importMetadata(data, options) {
|
|
563
|
+
const {
|
|
564
|
+
conflictResolution = "skip",
|
|
565
|
+
validate: _validate = true,
|
|
566
|
+
dryRun = false
|
|
567
|
+
} = options ?? {};
|
|
568
|
+
const bundle = data;
|
|
569
|
+
let total = 0;
|
|
570
|
+
let imported = 0;
|
|
571
|
+
let skipped = 0;
|
|
572
|
+
let failed = 0;
|
|
573
|
+
const errors = [];
|
|
574
|
+
for (const [type, items] of Object.entries(bundle)) {
|
|
575
|
+
if (!Array.isArray(items)) continue;
|
|
576
|
+
for (const item of items) {
|
|
577
|
+
total++;
|
|
578
|
+
const meta = item;
|
|
579
|
+
const name = meta?.name;
|
|
580
|
+
if (!name) {
|
|
581
|
+
failed++;
|
|
582
|
+
errors.push({ type, name: "(unknown)", error: "Item missing name field" });
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
try {
|
|
586
|
+
const itemExists = await this.exists(type, name);
|
|
587
|
+
if (itemExists && conflictResolution === "skip") {
|
|
588
|
+
skipped++;
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
if (!dryRun) {
|
|
592
|
+
if (itemExists && conflictResolution === "merge") {
|
|
593
|
+
const existing = await this.get(type, name);
|
|
594
|
+
const merged = { ...existing, ...item };
|
|
595
|
+
await this.register(type, name, merged);
|
|
596
|
+
} else {
|
|
597
|
+
await this.register(type, name, item);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
imported++;
|
|
601
|
+
} catch (e) {
|
|
602
|
+
failed++;
|
|
603
|
+
errors.push({
|
|
604
|
+
type,
|
|
605
|
+
name,
|
|
606
|
+
error: e instanceof Error ? e.message : String(e)
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return {
|
|
612
|
+
total,
|
|
613
|
+
imported,
|
|
614
|
+
skipped,
|
|
615
|
+
failed,
|
|
616
|
+
errors: errors.length > 0 ? errors : void 0
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
// ==========================================
|
|
620
|
+
// Validation
|
|
621
|
+
// ==========================================
|
|
622
|
+
/**
|
|
623
|
+
* Validate a metadata item against its type schema.
|
|
624
|
+
* Returns validation result with errors and warnings.
|
|
625
|
+
*/
|
|
626
|
+
async validate(_type, data) {
|
|
627
|
+
if (data === null || data === void 0) {
|
|
628
|
+
return {
|
|
629
|
+
valid: false,
|
|
630
|
+
errors: [{ path: "", message: "Metadata data cannot be null or undefined" }]
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
if (typeof data !== "object") {
|
|
634
|
+
return {
|
|
635
|
+
valid: false,
|
|
636
|
+
errors: [{ path: "", message: "Metadata data must be an object" }]
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
const meta = data;
|
|
640
|
+
const warnings = [];
|
|
641
|
+
if (!meta.name) {
|
|
642
|
+
return {
|
|
643
|
+
valid: false,
|
|
644
|
+
errors: [{ path: "name", message: "Metadata item must have a name field" }]
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
if (!meta.label) {
|
|
648
|
+
warnings.push({ path: "label", message: "Missing label field (recommended)" });
|
|
649
|
+
}
|
|
650
|
+
return { valid: true, warnings: warnings.length > 0 ? warnings : void 0 };
|
|
651
|
+
}
|
|
652
|
+
// ==========================================
|
|
653
|
+
// Type Registry
|
|
654
|
+
// ==========================================
|
|
655
|
+
/**
|
|
656
|
+
* Get all registered metadata types
|
|
657
|
+
*/
|
|
658
|
+
async getRegisteredTypes() {
|
|
659
|
+
const types = /* @__PURE__ */ new Set();
|
|
660
|
+
for (const entry of this.typeRegistry) {
|
|
661
|
+
types.add(entry.type);
|
|
662
|
+
}
|
|
663
|
+
for (const type of this.registry.keys()) {
|
|
664
|
+
types.add(type);
|
|
665
|
+
}
|
|
666
|
+
return Array.from(types);
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Get detailed information about a metadata type
|
|
670
|
+
*/
|
|
671
|
+
async getTypeInfo(type) {
|
|
672
|
+
const entry = this.typeRegistry.find((e) => e.type === type);
|
|
673
|
+
if (!entry) return void 0;
|
|
674
|
+
return {
|
|
675
|
+
type: entry.type,
|
|
676
|
+
label: entry.label,
|
|
677
|
+
description: entry.description,
|
|
678
|
+
filePatterns: entry.filePatterns,
|
|
679
|
+
supportsOverlay: entry.supportsOverlay,
|
|
680
|
+
domain: entry.domain
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
// ==========================================
|
|
684
|
+
// Dependency Tracking
|
|
685
|
+
// ==========================================
|
|
686
|
+
/**
|
|
687
|
+
* Get metadata items that this item depends on
|
|
688
|
+
*/
|
|
689
|
+
async getDependencies(type, name) {
|
|
690
|
+
return this.dependencies.get(`${encodeURIComponent(type)}:${encodeURIComponent(name)}`) ?? [];
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Get metadata items that depend on this item
|
|
694
|
+
*/
|
|
695
|
+
async getDependents(type, name) {
|
|
696
|
+
const dependents = [];
|
|
697
|
+
for (const deps of this.dependencies.values()) {
|
|
698
|
+
for (const dep of deps) {
|
|
699
|
+
if (dep.targetType === type && dep.targetName === name) {
|
|
700
|
+
dependents.push(dep);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return dependents;
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Register a dependency between two metadata items.
|
|
708
|
+
* Used internally to track cross-references.
|
|
709
|
+
* Duplicate dependencies (same source, target, and kind) are ignored.
|
|
710
|
+
*/
|
|
711
|
+
addDependency(dep) {
|
|
712
|
+
const key = `${encodeURIComponent(dep.sourceType)}:${encodeURIComponent(dep.sourceName)}`;
|
|
713
|
+
if (!this.dependencies.has(key)) {
|
|
714
|
+
this.dependencies.set(key, []);
|
|
715
|
+
}
|
|
716
|
+
const existing = this.dependencies.get(key);
|
|
717
|
+
const isDuplicate = existing.some(
|
|
718
|
+
(d) => d.targetType === dep.targetType && d.targetName === dep.targetName && d.kind === dep.kind
|
|
719
|
+
);
|
|
720
|
+
if (!isDuplicate) {
|
|
721
|
+
existing.push(dep);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
// ==========================================
|
|
725
|
+
// Legacy Loader API (backward compatible)
|
|
726
|
+
// ==========================================
|
|
727
|
+
/**
|
|
728
|
+
* Load a single metadata item from loaders.
|
|
729
|
+
* Iterates through registered loaders until found.
|
|
212
730
|
*/
|
|
213
731
|
async load(type, name, options) {
|
|
214
732
|
for (const loader of this.loaders.values()) {
|
|
@@ -224,8 +742,8 @@ var MetadataManager = class {
|
|
|
224
742
|
return null;
|
|
225
743
|
}
|
|
226
744
|
/**
|
|
227
|
-
* Load multiple metadata items
|
|
228
|
-
* Aggregates results from all loaders
|
|
745
|
+
* Load multiple metadata items from loaders.
|
|
746
|
+
* Aggregates results from all loaders.
|
|
229
747
|
*/
|
|
230
748
|
async loadMany(type, options) {
|
|
231
749
|
const results = [];
|
|
@@ -247,10 +765,7 @@ var MetadataManager = class {
|
|
|
247
765
|
return results;
|
|
248
766
|
}
|
|
249
767
|
/**
|
|
250
|
-
* Save metadata to
|
|
251
|
-
*/
|
|
252
|
-
/**
|
|
253
|
-
* Save metadata item
|
|
768
|
+
* Save metadata item to a loader
|
|
254
769
|
*/
|
|
255
770
|
async save(type, name, data, options) {
|
|
256
771
|
const targetLoader = options?.loader;
|
|
@@ -296,40 +811,18 @@ var MetadataManager = class {
|
|
|
296
811
|
return loader.save(type, name, data, options);
|
|
297
812
|
}
|
|
298
813
|
/**
|
|
299
|
-
*
|
|
814
|
+
* Register a watch callback for metadata changes
|
|
300
815
|
*/
|
|
301
|
-
|
|
302
|
-
for (const loader of this.loaders.values()) {
|
|
303
|
-
if (await loader.exists(type, name)) {
|
|
304
|
-
return true;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
return false;
|
|
308
|
-
}
|
|
309
|
-
/**
|
|
310
|
-
* List all items of a type
|
|
311
|
-
*/
|
|
312
|
-
async list(type) {
|
|
313
|
-
const items = /* @__PURE__ */ new Set();
|
|
314
|
-
for (const loader of this.loaders.values()) {
|
|
315
|
-
const result = await loader.list(type);
|
|
316
|
-
result.forEach((item) => items.add(item));
|
|
317
|
-
}
|
|
318
|
-
return Array.from(items);
|
|
319
|
-
}
|
|
320
|
-
/**
|
|
321
|
-
* Watch for metadata changes
|
|
322
|
-
*/
|
|
323
|
-
watch(type, callback) {
|
|
816
|
+
addWatchCallback(type, callback) {
|
|
324
817
|
if (!this.watchCallbacks.has(type)) {
|
|
325
818
|
this.watchCallbacks.set(type, /* @__PURE__ */ new Set());
|
|
326
819
|
}
|
|
327
820
|
this.watchCallbacks.get(type).add(callback);
|
|
328
821
|
}
|
|
329
822
|
/**
|
|
330
|
-
*
|
|
823
|
+
* Remove a watch callback for metadata changes
|
|
331
824
|
*/
|
|
332
|
-
|
|
825
|
+
removeWatchCallback(type, callback) {
|
|
333
826
|
const callbacks = this.watchCallbacks.get(type);
|
|
334
827
|
if (callbacks) {
|
|
335
828
|
callbacks.delete(callback);
|
|
@@ -765,7 +1258,7 @@ var NodeMetadataManager = class extends MetadataManager {
|
|
|
765
1258
|
};
|
|
766
1259
|
|
|
767
1260
|
// src/plugin.ts
|
|
768
|
-
import {
|
|
1261
|
+
import { DEFAULT_METADATA_TYPE_REGISTRY } from "@objectstack/spec/kernel";
|
|
769
1262
|
var MetadataPlugin = class {
|
|
770
1263
|
constructor(options = {}) {
|
|
771
1264
|
this.name = "com.objectstack.metadata";
|
|
@@ -779,29 +1272,35 @@ var MetadataPlugin = class {
|
|
|
779
1272
|
ctx.registerService("metadata", this.manager);
|
|
780
1273
|
ctx.logger.info("MetadataPlugin providing metadata service (primary mode)", {
|
|
781
1274
|
mode: "file-system",
|
|
782
|
-
features: ["watch", "persistence", "multi-format"]
|
|
1275
|
+
features: ["watch", "persistence", "multi-format", "query", "overlay", "type-registry"]
|
|
783
1276
|
});
|
|
784
1277
|
};
|
|
785
1278
|
this.start = async (ctx) => {
|
|
786
1279
|
ctx.logger.info("Loading metadata from file system...");
|
|
787
|
-
const
|
|
1280
|
+
const sortedTypes = [...DEFAULT_METADATA_TYPE_REGISTRY].sort((a, b) => a.loadOrder - b.loadOrder);
|
|
788
1281
|
let totalLoaded = 0;
|
|
789
|
-
for (const
|
|
1282
|
+
for (const entry of sortedTypes) {
|
|
790
1283
|
try {
|
|
791
|
-
const items = await this.manager.loadMany(type, {
|
|
1284
|
+
const items = await this.manager.loadMany(entry.type, {
|
|
792
1285
|
recursive: true
|
|
793
1286
|
});
|
|
794
1287
|
if (items.length > 0) {
|
|
795
|
-
|
|
1288
|
+
for (const item of items) {
|
|
1289
|
+
const meta = item;
|
|
1290
|
+
if (meta?.name) {
|
|
1291
|
+
await this.manager.register(entry.type, meta.name, item);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
ctx.logger.info(`Loaded ${items.length} ${entry.type} from file system`);
|
|
796
1295
|
totalLoaded += items.length;
|
|
797
1296
|
}
|
|
798
1297
|
} catch (e) {
|
|
799
|
-
ctx.logger.debug(`No ${type} metadata found`, { error: e.message });
|
|
1298
|
+
ctx.logger.debug(`No ${entry.type} metadata found`, { error: e.message });
|
|
800
1299
|
}
|
|
801
1300
|
}
|
|
802
1301
|
ctx.logger.info("Metadata loading complete", {
|
|
803
1302
|
totalItems: totalLoaded,
|
|
804
|
-
|
|
1303
|
+
registeredTypes: sortedTypes.length
|
|
805
1304
|
});
|
|
806
1305
|
};
|
|
807
1306
|
this.options = {
|
|
@@ -814,6 +1313,7 @@ var MetadataPlugin = class {
|
|
|
814
1313
|
watch: this.options.watch ?? true,
|
|
815
1314
|
formats: ["yaml", "json", "typescript", "javascript"]
|
|
816
1315
|
});
|
|
1316
|
+
this.manager.setTypeRegistry(DEFAULT_METADATA_TYPE_REGISTRY);
|
|
817
1317
|
}
|
|
818
1318
|
};
|
|
819
1319
|
|