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