@objectstack/metadata 2.0.7 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
- * Load a single metadata item
248
- * Iterates through registered loaders until found
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 disk
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
- * Check if metadata item exists
851
+ * Register a watch callback for metadata changes
337
852
  */
338
- async exists(type, name) {
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
- * Unwatch metadata changes
860
+ * Remove a watch callback for metadata changes
368
861
  */
369
- unwatch(type, callback) {
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 import_spec = require("@objectstack/spec");
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 metadataTypes = Object.keys(import_spec.ObjectStackDefinitionSchema.shape).filter((key) => key !== "manifest");
1317
+ const sortedTypes = [...import_kernel.DEFAULT_METADATA_TYPE_REGISTRY].sort((a, b) => a.loadOrder - b.loadOrder);
825
1318
  let totalLoaded = 0;
826
- for (const type of metadataTypes) {
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
- ctx.logger.info(`Loaded ${items.length} ${type} from file system`);
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
- note: "ObjectQL will sync these into its registry during its start phase"
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