@x12i/catalox 3.5.1 → 3.6.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.
Files changed (113) hide show
  1. package/README.md +106 -1
  2. package/dist/src/adapters/api/api-adapter.d.ts.map +1 -1
  3. package/dist/src/adapters/api/api-adapter.js +1 -0
  4. package/dist/src/adapters/api/api-adapter.js.map +1 -1
  5. package/dist/src/adapters/mongo/mongo-adapter.d.ts.map +1 -1
  6. package/dist/src/adapters/mongo/mongo-adapter.js +1 -0
  7. package/dist/src/adapters/mongo/mongo-adapter.js.map +1 -1
  8. package/dist/src/catalox/catalog-discovery.d.ts.map +1 -1
  9. package/dist/src/catalox/catalog-discovery.js +8 -6
  10. package/dist/src/catalox/catalog-discovery.js.map +1 -1
  11. package/dist/src/catalox/catalog-lifecycle.d.ts.map +1 -1
  12. package/dist/src/catalox/catalog-lifecycle.js +26 -19
  13. package/dist/src/catalox/catalog-lifecycle.js.map +1 -1
  14. package/dist/src/catalox/catalox-bound.d.ts +42 -0
  15. package/dist/src/catalox/catalox-bound.d.ts.map +1 -1
  16. package/dist/src/catalox/catalox-bound.js +15 -0
  17. package/dist/src/catalox/catalox-bound.js.map +1 -1
  18. package/dist/src/catalox/catalox.d.ts +84 -1
  19. package/dist/src/catalox/catalox.d.ts.map +1 -1
  20. package/dist/src/catalox/catalox.js +318 -58
  21. package/dist/src/catalox/catalox.js.map +1 -1
  22. package/dist/src/catalox/create-catalox.d.ts +4 -0
  23. package/dist/src/catalox/create-catalox.d.ts.map +1 -1
  24. package/dist/src/catalox/create-catalox.js +15 -0
  25. package/dist/src/catalox/create-catalox.js.map +1 -1
  26. package/dist/src/catalox/native-catalog-merge.d.ts.map +1 -1
  27. package/dist/src/catalox/native-catalog-merge.js +12 -11
  28. package/dist/src/catalox/native-catalog-merge.js.map +1 -1
  29. package/dist/src/contracts/agents.d.ts +19 -0
  30. package/dist/src/contracts/agents.d.ts.map +1 -0
  31. package/dist/src/contracts/agents.js +2 -0
  32. package/dist/src/contracts/agents.js.map +1 -0
  33. package/dist/src/contracts/catalog-types.d.ts +19 -0
  34. package/dist/src/contracts/catalog-types.d.ts.map +1 -0
  35. package/dist/src/contracts/catalog-types.js +2 -0
  36. package/dist/src/contracts/catalog-types.js.map +1 -0
  37. package/dist/src/contracts/catalogs.d.ts +26 -6
  38. package/dist/src/contracts/catalogs.d.ts.map +1 -1
  39. package/dist/src/contracts/catalogs.js.map +1 -1
  40. package/dist/src/contracts/descriptors.d.ts +5 -0
  41. package/dist/src/contracts/descriptors.d.ts.map +1 -1
  42. package/dist/src/contracts/design-objects.d.ts +38 -0
  43. package/dist/src/contracts/design-objects.d.ts.map +1 -0
  44. package/dist/src/contracts/design-objects.js +2 -0
  45. package/dist/src/contracts/design-objects.js.map +1 -0
  46. package/dist/src/contracts/discovery.d.ts +7 -8
  47. package/dist/src/contracts/discovery.d.ts.map +1 -1
  48. package/dist/src/contracts/domains.d.ts +19 -0
  49. package/dist/src/contracts/domains.d.ts.map +1 -0
  50. package/dist/src/contracts/domains.js +2 -0
  51. package/dist/src/contracts/domains.js.map +1 -0
  52. package/dist/src/contracts/ids.d.ts +2 -0
  53. package/dist/src/contracts/ids.d.ts.map +1 -1
  54. package/dist/src/contracts/index.d.ts +8 -2
  55. package/dist/src/contracts/index.d.ts.map +1 -1
  56. package/dist/src/contracts/index.js +1 -0
  57. package/dist/src/contracts/index.js.map +1 -1
  58. package/dist/src/contracts/inputs.d.ts +1 -0
  59. package/dist/src/contracts/inputs.d.ts.map +1 -1
  60. package/dist/src/contracts/items.d.ts +9 -7
  61. package/dist/src/contracts/items.d.ts.map +1 -1
  62. package/dist/src/contracts/presentation-binding.d.ts +116 -0
  63. package/dist/src/contracts/presentation-binding.d.ts.map +1 -0
  64. package/dist/src/contracts/presentation-binding.js +215 -0
  65. package/dist/src/contracts/presentation-binding.js.map +1 -0
  66. package/dist/src/contracts/presentation.d.ts +41 -0
  67. package/dist/src/contracts/presentation.d.ts.map +1 -1
  68. package/dist/src/firebase/agent-store.d.ts +18 -0
  69. package/dist/src/firebase/agent-store.d.ts.map +1 -0
  70. package/dist/src/firebase/agent-store.js +47 -0
  71. package/dist/src/firebase/agent-store.js.map +1 -0
  72. package/dist/src/firebase/catalog-type-store.d.ts +22 -0
  73. package/dist/src/firebase/catalog-type-store.d.ts.map +1 -0
  74. package/dist/src/firebase/catalog-type-store.js +51 -0
  75. package/dist/src/firebase/catalog-type-store.js.map +1 -0
  76. package/dist/src/firebase/definition-store.d.ts +4 -18
  77. package/dist/src/firebase/definition-store.d.ts.map +1 -1
  78. package/dist/src/firebase/definition-store.js.map +1 -1
  79. package/dist/src/firebase/design-object-store.d.ts +15 -0
  80. package/dist/src/firebase/design-object-store.d.ts.map +1 -0
  81. package/dist/src/firebase/design-object-store.js +51 -0
  82. package/dist/src/firebase/design-object-store.js.map +1 -0
  83. package/dist/src/firebase/domain-store.d.ts +18 -0
  84. package/dist/src/firebase/domain-store.d.ts.map +1 -0
  85. package/dist/src/firebase/domain-store.js +47 -0
  86. package/dist/src/firebase/domain-store.js.map +1 -0
  87. package/dist/src/firebase/index.d.ts +2 -0
  88. package/dist/src/firebase/index.d.ts.map +1 -1
  89. package/dist/src/firebase/index.js +2 -0
  90. package/dist/src/firebase/index.js.map +1 -1
  91. package/dist/src/firebase/presentation-profile-store.d.ts +18 -0
  92. package/dist/src/firebase/presentation-profile-store.d.ts.map +1 -0
  93. package/dist/src/firebase/presentation-profile-store.js +44 -0
  94. package/dist/src/firebase/presentation-profile-store.js.map +1 -0
  95. package/dist/src/migrations/backfill-catalog-model.d.ts +29 -0
  96. package/dist/src/migrations/backfill-catalog-model.d.ts.map +1 -0
  97. package/dist/src/migrations/backfill-catalog-model.js +124 -0
  98. package/dist/src/migrations/backfill-catalog-model.js.map +1 -0
  99. package/dist/src/migrations/migrate-native-catalog-layout.js +4 -4
  100. package/dist/src/migrations/migrate-native-catalog-layout.js.map +1 -1
  101. package/dist/src/validation/index.d.ts +2 -2
  102. package/dist/src/validation/index.d.ts.map +1 -1
  103. package/dist/src/validation/index.js +2 -2
  104. package/dist/src/validation/index.js.map +1 -1
  105. package/dist/src/validation/ui-spec-schema.d.ts +5 -0
  106. package/dist/src/validation/ui-spec-schema.d.ts.map +1 -1
  107. package/dist/src/validation/ui-spec-schema.js +114 -0
  108. package/dist/src/validation/ui-spec-schema.js.map +1 -1
  109. package/dist/src/validation/ui-spec-validate.d.ts +3 -0
  110. package/dist/src/validation/ui-spec-validate.d.ts.map +1 -1
  111. package/dist/src/validation/ui-spec-validate.js +13 -1
  112. package/dist/src/validation/ui-spec-validate.js.map +1 -1
  113. package/package.json +2 -2
@@ -12,11 +12,17 @@ import { parseJson, toJson } from "./json-io.js";
12
12
  import { createHash, randomUUID } from "node:crypto";
13
13
  import { renderInventoryReportMarkdown } from "./reporting/render-inventory-report.js";
14
14
  import { optionsFromCatalogItems, optionsFromStaticSource, resolveFilterBy } from "./field-source-resolution.js";
15
+ import { resolveCatalogPresentationBinding } from "../contracts/presentation-binding.js";
16
+ import { buildRendererSnippetDocId } from "../migrations/renderer-metadata.js";
17
+ import { backfillCatalogModel } from "../migrations/backfill-catalog-model.js";
15
18
  import { AppStore } from "../firebase/app-store.js";
16
19
  import { AdapterStore } from "../firebase/adapter-store.js";
17
20
  import { BindingStore } from "../firebase/binding-store.js";
18
21
  import { CatalogStore } from "../firebase/catalog-store.js";
19
22
  import { CatalogDataIndexStore } from "../firebase/catalog-data-index-store.js";
23
+ import { CatalogTypeStore } from "../firebase/catalog-type-store.js";
24
+ import { DomainStore } from "../firebase/domain-store.js";
25
+ import { AgentStore } from "../firebase/agent-store.js";
20
26
  import { DefinitionStore } from "../firebase/definition-store.js";
21
27
  import { DescriptorStore } from "../firebase/descriptor-store.js";
22
28
  import { FirestoreStore } from "../firebase/firestore-store.js";
@@ -46,6 +52,23 @@ export class Catalox {
46
52
  constructor(deps) {
47
53
  this.deps = deps;
48
54
  }
55
+ async validateCatalogTypeForContext(context, catalogType) {
56
+ const registry = this.deps.catalogTypes
57
+ ? await this.deps.catalogTypes.resolveForContext({
58
+ ...(context.storeId ? { storeId: context.storeId } : {}),
59
+ ...(context.appId ? { appId: context.appId } : {}),
60
+ })
61
+ : null;
62
+ if (!registry)
63
+ return; // registry not configured or no entry exists yet
64
+ const allowed = new Set((registry.types ?? []).map((t) => String(t)));
65
+ if (!allowed.has(String(catalogType))) {
66
+ throw new CatalogAdapterError({
67
+ reason: "catalog_type_not_allowed",
68
+ message: `catalogType "${String(catalogType)}" is not allowed for scope ${JSON.stringify(registry.scope)}.`,
69
+ });
70
+ }
71
+ }
49
72
  catalogLifecycleDeps() {
50
73
  return {
51
74
  firestoreStore: this.deps.firestoreStore,
@@ -245,7 +268,7 @@ export class Catalox {
245
268
  data: (i.data ?? {}),
246
269
  ...(i.title != null ? { title: i.title } : {}),
247
270
  ...(i.subtitle != null ? { subtitle: i.subtitle } : {}),
248
- ...(i.status != null ? { status: i.status } : {}),
271
+ ...(i.metadata?.status != null ? { status: String(i.metadata.status) } : {}),
249
272
  ...(i.metadata != null ? { metadata: i.metadata } : {}),
250
273
  })),
251
274
  ...(list.total != null ? { total: list.total } : {}),
@@ -269,6 +292,119 @@ export class Catalox {
269
292
  actions: {},
270
293
  };
271
294
  }
295
+ async getDesignObject(context, designId) {
296
+ void context;
297
+ if (!this.deps.designObjects)
298
+ throw new Error("designObjects dependency is not configured");
299
+ return this.deps.designObjects.get(String(designId));
300
+ }
301
+ async listDesignObjects(context, scope) {
302
+ void context;
303
+ if (!this.deps.designObjects)
304
+ throw new Error("designObjects dependency is not configured");
305
+ return this.deps.designObjects.listByScope(scope);
306
+ }
307
+ async getCatalogPresentationBinding(context, catalogId, role, options) {
308
+ const descriptor = await this.getCatalogDescriptor(context, catalogId);
309
+ const profiles = options?.profiles
310
+ ? options.profiles
311
+ : this.deps.presentationProfiles
312
+ ? await this.deps.presentationProfiles.listForCatalog?.({
313
+ catalogId: String(catalogId),
314
+ ...(descriptor?.catalogType ? { catalogType: String(descriptor.catalogType) } : {}),
315
+ })
316
+ : [];
317
+ const bindingBase = resolveCatalogPresentationBinding({
318
+ descriptor,
319
+ profiles,
320
+ role: role,
321
+ ...(options?.mode ? { mode: options.mode } : {}),
322
+ ...(options?.displayContext ? { displayContext: options.displayContext } : {}),
323
+ ...(options?.designScope ? { designScope: options.designScope } : {}),
324
+ ...(options?.designObjects ? { designObjects: options.designObjects } : {}),
325
+ });
326
+ // Optionally fetch snippet metadata when a snippetRef is involved.
327
+ if (options?.includeSnippet && bindingBase.customRenderer?.entry?.snippetRef) {
328
+ const entry = bindingBase.customRenderer.entry;
329
+ const snippetId = entry.snippetRef?.snippetId;
330
+ const catalogScoped = entry.snippetRef?.catalogScoped !== false;
331
+ if (!this.deps.rendererSnippets)
332
+ throw new Error("rendererSnippets dependency is not configured");
333
+ // Support canonical catalog-scoped ids; for anything else, require host-side snippet resolution.
334
+ if (!snippetId) {
335
+ const rec = await this.getCatalogRendererSnippet(context, catalogId, role, options?.mode);
336
+ return { ...bindingBase, customRenderer: { ...bindingBase.customRenderer, snippet: rec } };
337
+ }
338
+ if (catalogScoped) {
339
+ const expected1 = buildRendererSnippetDocId({ catalogId: String(catalogId), role: role, ...(options?.mode ? { mode: options.mode } : {}) });
340
+ if (String(snippetId) !== expected1) {
341
+ throw new Error(`snippetRef.snippetId is not a canonical catalog-scoped id: ${String(snippetId)}`);
342
+ }
343
+ const rec = await this.getCatalogRendererSnippet(context, catalogId, role, options?.mode);
344
+ return { ...bindingBase, customRenderer: { ...bindingBase.customRenderer, snippet: rec } };
345
+ }
346
+ throw new Error("Global (non-catalog-scoped) snippetRef resolution is host-defined.");
347
+ }
348
+ // Optionally include design merge when requested and available.
349
+ if (options?.includeDesignObjects && !options?.designObjects) {
350
+ if (!this.deps.designObjects)
351
+ throw new Error("designObjects dependency is not configured");
352
+ const designObjects = await this.deps.designObjects.listByScope?.({
353
+ ...(options?.designScope?.storeId ? { storeId: options.designScope.storeId } : {}),
354
+ ...(options?.designScope?.appId ? { appId: options.designScope.appId } : {}),
355
+ ...(options?.designScope?.accountId ? { accountId: options.designScope.accountId } : {}),
356
+ ...(options?.designScope?.agentId ? { agentId: options.designScope.agentId } : {}),
357
+ });
358
+ return resolveCatalogPresentationBinding({
359
+ descriptor,
360
+ profiles,
361
+ role: role,
362
+ ...(options?.mode ? { mode: options.mode } : {}),
363
+ ...(options?.displayContext ? { displayContext: options.displayContext } : {}),
364
+ ...(options?.designScope ? { designScope: options.designScope } : {}),
365
+ designObjects,
366
+ // Preserve snippet result if caller already asked for it separately above.
367
+ ...(bindingBase.customRenderer?.snippet !== undefined ? { snippet: bindingBase.customRenderer.snippet } : {}),
368
+ });
369
+ }
370
+ return bindingBase;
371
+ }
372
+ async buildCatalogListPresentationSurface(context, catalogId, options) {
373
+ const map = await this.buildCatalogListRenderMap(context, catalogId, {
374
+ ...(options?.limit != null ? { limit: options.limit } : {}),
375
+ ...(options?.resolveSources != null ? { resolveSources: options.resolveSources } : {}),
376
+ ...(options?.maxSourceOptions != null ? { maxSourceOptions: options.maxSourceOptions } : {}),
377
+ ...(options?.displayContext != null ? { displayContext: options.displayContext } : {}),
378
+ });
379
+ const binding = await this.getCatalogPresentationBinding(context, catalogId, (options?.displayContext ?? "grid") === "grid" ? "grid" : "list", {
380
+ ...(options?.mode ? { mode: options.mode } : {}),
381
+ displayContext: options?.displayContext ?? "grid",
382
+ ...(options?.includeSnippet ? { includeSnippet: true } : {}),
383
+ ...(options?.includeDesignObjects ? { includeDesignObjects: true } : {}),
384
+ ...(options?.designScope ? { designScope: options.designScope } : {}),
385
+ ...(options?.profiles ? { profiles: options.profiles } : {}),
386
+ ...(options?.designObjects ? { designObjects: options.designObjects } : {}),
387
+ });
388
+ return { map, binding };
389
+ }
390
+ async buildCatalogItemPresentationSurface(context, catalogId, itemId, options) {
391
+ const map = await this.buildCatalogItemRenderMap(context, catalogId, itemId, {
392
+ ...(options?.includeReferences != null ? { includeReferences: options.includeReferences } : {}),
393
+ ...(options?.resolveSources != null ? { resolveSources: options.resolveSources } : {}),
394
+ ...(options?.maxSourceOptions != null ? { maxSourceOptions: options.maxSourceOptions } : {}),
395
+ ...(options?.displayContext != null ? { displayContext: options.displayContext } : {}),
396
+ });
397
+ const binding = await this.getCatalogPresentationBinding(context, catalogId, "item", {
398
+ ...(options?.mode ? { mode: options.mode } : {}),
399
+ displayContext: "form",
400
+ ...(options?.includeSnippet ? { includeSnippet: true } : {}),
401
+ ...(options?.includeDesignObjects ? { includeDesignObjects: true } : {}),
402
+ ...(options?.designScope ? { designScope: options.designScope } : {}),
403
+ ...(options?.profiles ? { profiles: options.profiles } : {}),
404
+ ...(options?.designObjects ? { designObjects: options.designObjects } : {}),
405
+ });
406
+ return { map, binding };
407
+ }
272
408
  async buildCatalogItemRenderMap(context, catalogId, itemId, options) {
273
409
  const descriptor = await this.getCatalogDescriptor(context, catalogId);
274
410
  const got = await this.getCatalogItem(context, catalogId, itemId);
@@ -302,10 +438,10 @@ export class Catalox {
302
438
  data: (item.data ?? {}),
303
439
  ...(item.title != null ? { title: item.title } : {}),
304
440
  ...(item.subtitle != null ? { subtitle: item.subtitle } : {}),
305
- ...(item.status != null ? { status: item.status } : {}),
441
+ ...(item.metadata?.status != null ? { status: String(item.metadata.status) } : {}),
306
442
  ...(item.metadata != null ? { metadata: item.metadata } : {}),
307
- ...(item.createdAt != null ? { createdAt: item.createdAt } : {}),
308
- ...(item.updatedAt != null ? { updatedAt: item.updatedAt } : {}),
443
+ ...(item.metadata?.createdAt != null ? { createdAt: String(item.metadata.createdAt) } : {}),
444
+ ...(item.metadata?.lastUpdate != null ? { updatedAt: String(item.metadata.lastUpdate) } : {}),
309
445
  },
310
446
  resolvedSources,
311
447
  ...(refs.length
@@ -453,8 +589,13 @@ export class Catalox {
453
589
  ...item,
454
590
  ...(title != null ? { title: String(title) } : {}),
455
591
  ...(subtitle != null ? { subtitle: String(subtitle) } : {}),
456
- ...(status != null ? { status: String(status) } : {}),
457
- ...(updatedAt != null && item.updatedAt == null ? { updatedAt: String(updatedAt) } : {}),
592
+ metadata: {
593
+ ...(item.metadata ?? {}),
594
+ ...(item.metadata?.domainIds == null ? { domainIds: [] } : {}),
595
+ ...(item.metadata?.agentIds == null ? { agentIds: [] } : {}),
596
+ ...(status != null ? { status: String(status) } : {}),
597
+ ...(updatedAt != null && item.metadata?.lastUpdate == null ? { lastUpdate: String(updatedAt) } : {}),
598
+ },
458
599
  };
459
600
  }
460
601
  async listAppCatalogs(context, input) {
@@ -470,26 +611,26 @@ export class Catalox {
470
611
  const cat = byId.get(b.catalogId);
471
612
  if (!cat)
472
613
  continue;
473
- if (!input?.includeDisabled && cat.status !== "active")
614
+ if (!input?.includeDisabled && cat.metadata.status !== "active")
474
615
  continue;
475
616
  const descriptor = await this.deps.descriptors.get(cat.catalogId);
476
617
  const description = descriptor?.descriptor.description ?? cat.description;
477
618
  const itemLabel = descriptor?.descriptor.itemLabel ?? cat.itemLabel;
478
619
  const visibility = descriptor?.descriptor.visibility;
479
620
  const descriptorVersion = descriptor?.descriptorVersion;
480
- const metadata = descriptor?.descriptor.metadata ?? cat.metadata;
621
+ const metadata = { ...cat.metadata, ...(descriptor?.descriptor.metadata ?? {}) };
481
622
  out.push({
482
623
  catalogId: cat.catalogId,
483
624
  label: descriptor?.descriptor.label ?? cat.name,
484
625
  ...(description != null ? { description } : {}),
485
626
  ...(itemLabel != null ? { itemLabel } : {}),
627
+ catalogType: cat.catalogType,
486
628
  sourceMode: cat.sourceMode,
487
629
  ...(cat.mappedSourceType != null ? { mappedSourceType: cat.mappedSourceType } : {}),
488
- status: cat.status,
489
630
  ...(visibility != null ? { visibility } : {}),
490
631
  access: b.access,
491
632
  ...(descriptorVersion != null ? { descriptorVersion } : {}),
492
- ...(metadata != null ? { metadata } : {}),
633
+ metadata,
493
634
  });
494
635
  }
495
636
  return out.filter((e) => (input?.includeHidden ? true : e.visibility !== "hidden"));
@@ -536,26 +677,26 @@ export class Catalox {
536
677
  const cat = byId.get(b.catalogId);
537
678
  if (!cat)
538
679
  continue;
539
- if (!input.includeDisabled && cat.status !== "active")
680
+ if (!input.includeDisabled && cat.metadata.status !== "active")
540
681
  continue;
541
682
  const descriptor = await this.deps.descriptors.get(cat.catalogId);
542
683
  const description = descriptor?.descriptor.description ?? cat.description;
543
684
  const itemLabel = descriptor?.descriptor.itemLabel ?? cat.itemLabel;
544
685
  const visibility = descriptor?.descriptor.visibility;
545
686
  const descriptorVersion = descriptor?.descriptorVersion;
546
- const metadata = descriptor?.descriptor.metadata ?? cat.metadata;
687
+ const metadata = { ...cat.metadata, ...(descriptor?.descriptor.metadata ?? {}) };
547
688
  out.push({
548
689
  catalogId: cat.catalogId,
549
690
  label: descriptor?.descriptor.label ?? cat.name,
550
691
  ...(description != null ? { description } : {}),
551
692
  ...(itemLabel != null ? { itemLabel } : {}),
693
+ catalogType: cat.catalogType,
552
694
  sourceMode: cat.sourceMode,
553
695
  ...(cat.mappedSourceType != null ? { mappedSourceType: cat.mappedSourceType } : {}),
554
- status: cat.status,
555
696
  ...(visibility != null ? { visibility } : {}),
556
697
  access: b.access,
557
698
  ...(descriptorVersion != null ? { descriptorVersion } : {}),
558
- ...(metadata != null ? { metadata } : {}),
699
+ metadata,
559
700
  });
560
701
  }
561
702
  return out.filter((e) => (input.includeHidden ? true : e.visibility !== "hidden"));
@@ -679,9 +820,19 @@ export class Catalox {
679
820
  };
680
821
  }
681
822
  const def = await this.deps.definitions.get(catalogId);
682
- if (!def || def.type !== "mapped")
823
+ if (!def)
824
+ throw new CatalogAdapterError({ catalogId, reason: "missing_definition" });
825
+ if (def.catalogItems.providerType !== "external") {
683
826
  throw new CatalogAdapterError({ catalogId, reason: "missing_definition" });
684
- const mapping = await this.deps.mappings.get(def.mappingId);
827
+ }
828
+ const map = def.catalogItems.map;
829
+ const mappingId = map?.mappingId;
830
+ const adapterId = map?.adapterId;
831
+ if (!mappingId)
832
+ throw new CatalogAdapterError({ catalogId, reason: "missing_mapping" });
833
+ if (!adapterId)
834
+ throw new CatalogAdapterError({ catalogId, reason: "missing_adapter" });
835
+ const mapping = await this.deps.mappings.get(mappingId);
685
836
  if (!mapping)
686
837
  throw new CatalogAdapterError({ catalogId, reason: "missing_mapping" });
687
838
  const mappingIssues = validateMappingSpec(mapping.mapping);
@@ -694,10 +845,10 @@ export class Catalox {
694
845
  return rest;
695
846
  })()
696
847
  : undefined;
697
- if (def.adapterType === "mongo") {
848
+ if (def.catalogItems.provider === "mongo") {
698
849
  if (!this.deps.mongoAdapter)
699
850
  throw new CatalogAdapterError({ catalogId, reason: "mongo_adapter_unconfigured" });
700
- const adapterConfig = await this.deps.adapters.get(def.adapterId);
851
+ const adapterConfig = await this.deps.adapters.get(adapterId);
701
852
  if (!adapterConfig)
702
853
  throw new CatalogAdapterError({ catalogId, reason: "missing_adapter" });
703
854
  const result = await this.deps.mongoAdapter.listItems(appCtx, catalogId, adapterConfig, { mapping: mapping.mapping, ...(mapping.options ? { options: mapping.options } : {}) }, mappedListQueryOptions);
@@ -705,10 +856,10 @@ export class Catalox {
705
856
  ? { listOutcome: "ok", items: result.items, issues: result.issues }
706
857
  : { listOutcome: "ok", items: result.items };
707
858
  }
708
- if (def.adapterType === "api") {
859
+ if (def.catalogItems.provider === "api") {
709
860
  if (!this.deps.apiAdapter)
710
861
  throw new CatalogAdapterError({ catalogId, reason: "api_adapter_unconfigured" });
711
- const adapterConfig = await this.deps.adapters.get(def.adapterId);
862
+ const adapterConfig = await this.deps.adapters.get(adapterId);
712
863
  if (!adapterConfig)
713
864
  throw new CatalogAdapterError({ catalogId, reason: "missing_adapter" });
714
865
  const result = await this.deps.apiAdapter.listItems(appCtx, catalogId, adapterConfig, { responseMapping: mapping.mapping, ...(mapping.options ? { options: mapping.options } : {}) }, mappedListQueryOptions);
@@ -767,8 +918,6 @@ export class Catalox {
767
918
  sourceType: "firebase",
768
919
  data: rec.data,
769
920
  metadata: meta,
770
- createdAt: rec.createdAt,
771
- updatedAt: rec.updatedAt,
772
921
  };
773
922
  return { outcome: "found", item: await this.decorateItem(catalogId, base) };
774
923
  }
@@ -854,18 +1003,35 @@ export class Catalox {
854
1003
  async createCatalog(_context, _input) {
855
1004
  const now = new Date().toISOString();
856
1005
  const catalogId = (_input.catalogId ?? randomUUID());
1006
+ await this.validateCatalogTypeForContext(_context, _input.catalogType);
857
1007
  await this.deps.catalogs.upsert({
858
1008
  catalogId,
859
1009
  name: _input.name,
860
1010
  ...(_input.description != null ? { description: _input.description } : {}),
1011
+ catalogType: _input.catalogType,
861
1012
  sourceMode: _input.sourceMode,
862
1013
  ...(_input.sourceMode === "mapped" && _input.mapped?.sourceType
863
1014
  ? { mappedSourceType: _input.mapped.sourceType }
864
1015
  : {}),
865
- status: "active",
866
- ...(_input.metadata != null ? { metadata: _input.metadata } : {}),
867
- createdAt: now,
868
- updatedAt: now,
1016
+ catalogItems: _input.sourceMode === "native"
1017
+ ? {
1018
+ providerType: "internal",
1019
+ ...(_input.native?.itemSchema != null ? { itemSchema: _input.native.itemSchema } : {}),
1020
+ metadata: {},
1021
+ }
1022
+ : {
1023
+ providerType: "external",
1024
+ provider: _input.mapped?.sourceType ?? "api",
1025
+ // map references existing adapter + mapping documents (see createCatalog mapped branch below)
1026
+ map: {},
1027
+ metadata: {},
1028
+ },
1029
+ metadata: {
1030
+ status: "active",
1031
+ createdAt: now,
1032
+ lastUpdate: now,
1033
+ ...(_input.metadata ?? {}),
1034
+ },
869
1035
  });
870
1036
  if (_input.sourceMode === "native") {
871
1037
  if (!_input.native)
@@ -873,12 +1039,12 @@ export class Catalox {
873
1039
  const collectionPath = _input.native.firestoreCollectionPath ?? nativeItemsCollectionId(catalogId);
874
1040
  await this.deps.definitions.upsert({
875
1041
  catalogId,
876
- type: "native",
877
- storage: {
878
- provider: "firestore",
879
- collectionPath,
1042
+ sourceMode: "native",
1043
+ catalogItems: {
1044
+ providerType: "internal",
1045
+ ...(_input.native.itemSchema != null ? { itemSchema: _input.native.itemSchema } : {}),
1046
+ metadata: { firestoreCollectionPath: collectionPath },
880
1047
  },
881
- ...(_input.native.itemSchema != null ? { itemSchema: _input.native.itemSchema } : {}),
882
1048
  createdAt: now,
883
1049
  updatedAt: now,
884
1050
  });
@@ -902,16 +1068,31 @@ export class Catalox {
902
1068
  });
903
1069
  await this.deps.definitions.upsert({
904
1070
  catalogId,
905
- type: "mapped",
906
- adapterType: _input.mapped.sourceType,
907
- adapterId,
908
- mappingId,
909
- ...(_input.mapped.syncMode
910
- ? { sync: { mode: _input.mapped.syncMode, syncStatus: "idle" } }
911
- : {}),
1071
+ sourceMode: "mapped",
1072
+ catalogItems: {
1073
+ providerType: "external",
1074
+ provider: _input.mapped.sourceType,
1075
+ map: { adapterId, mappingId },
1076
+ metadata: {},
1077
+ },
1078
+ ...(_input.mapped.syncMode ? { sync: { mode: _input.mapped.syncMode, syncStatus: "idle" } } : {}),
912
1079
  createdAt: now,
913
1080
  updatedAt: now,
914
1081
  });
1082
+ // Update catalog record to point at the external map references.
1083
+ await this.deps.catalogs.upsert({
1084
+ ...(await this.deps.catalogs.get(catalogId)),
1085
+ catalogItems: {
1086
+ providerType: "external",
1087
+ provider: _input.mapped.sourceType,
1088
+ map: { adapterId, mappingId },
1089
+ metadata: {},
1090
+ },
1091
+ metadata: {
1092
+ ...(await this.deps.catalogs.get(catalogId)).metadata,
1093
+ lastUpdate: now,
1094
+ },
1095
+ });
915
1096
  }
916
1097
  // Seed a minimal descriptor if none exists.
917
1098
  const existingDescriptor = await this.deps.descriptors.get(catalogId);
@@ -959,12 +1140,19 @@ export class Catalox {
959
1140
  if (!existing)
960
1141
  throw new CatalogNotFoundError({ catalogId: _catalogId });
961
1142
  const now = new Date().toISOString();
1143
+ if (_patch.catalogType != null) {
1144
+ await this.validateCatalogTypeForContext(_context, String(_patch.catalogType));
1145
+ }
962
1146
  await this.deps.catalogs.upsert({
963
1147
  ...existing,
964
1148
  ...(_patch.name != null ? { name: _patch.name } : {}),
965
1149
  ...(_patch.description != null ? { description: _patch.description } : {}),
966
- ...(_patch.metadata != null ? { metadata: _patch.metadata } : {}),
967
- updatedAt: now,
1150
+ ...(_patch.catalogType != null ? { catalogType: String(_patch.catalogType) } : {}),
1151
+ metadata: {
1152
+ ...existing.metadata,
1153
+ ...(_patch.metadata ?? {}),
1154
+ lastUpdate: now,
1155
+ },
968
1156
  });
969
1157
  return (await this.deps.catalogs.get(_catalogId));
970
1158
  }
@@ -1264,12 +1452,67 @@ export class Catalox {
1264
1452
  await this.deps.catalogs.upsert({
1265
1453
  catalogId: catalog.catalogId,
1266
1454
  name: catalog.name,
1455
+ catalogType: "generic",
1267
1456
  sourceMode: "native",
1268
- status: catalog.status ?? "active",
1269
- createdAt: now,
1270
- updatedAt: now,
1457
+ catalogItems: { providerType: "internal", metadata: {} },
1458
+ metadata: {
1459
+ status: catalog.status ?? "active",
1460
+ createdAt: now,
1461
+ lastUpdate: now,
1462
+ },
1271
1463
  });
1272
1464
  }
1465
+ async resolveAllowedCatalogTypes(context) {
1466
+ if (!this.deps.catalogTypes)
1467
+ return null;
1468
+ return this.deps.catalogTypes.resolveForContext({
1469
+ ...(context.storeId ? { storeId: context.storeId } : {}),
1470
+ ...(context.appId ? { appId: context.appId } : {}),
1471
+ });
1472
+ }
1473
+ async upsertCatalogTypeRegistry(context, scope, input) {
1474
+ if (!context.superAdmin)
1475
+ throw new CatalogAccessDeniedError({ reason: "super_admin_required" });
1476
+ if (!this.deps.catalogTypes)
1477
+ throw new Error("catalogTypes dependency is not configured");
1478
+ await this.deps.catalogTypes.upsert(scope, input);
1479
+ }
1480
+ async resolveAllowedDomains(context) {
1481
+ if (!this.deps.domains)
1482
+ return null;
1483
+ return this.deps.domains.resolveForContext({
1484
+ ...(context.storeId ? { storeId: context.storeId } : {}),
1485
+ ...(context.appId ? { appId: context.appId } : {}),
1486
+ });
1487
+ }
1488
+ async upsertDomainRegistry(context, scope, input) {
1489
+ if (!context.superAdmin)
1490
+ throw new CatalogAccessDeniedError({ reason: "super_admin_required" });
1491
+ if (!this.deps.domains)
1492
+ throw new Error("domains dependency is not configured");
1493
+ await this.deps.domains.upsert(scope, input);
1494
+ }
1495
+ async resolveAllowedAgents(context) {
1496
+ if (!this.deps.agents)
1497
+ return null;
1498
+ return this.deps.agents.resolveForContext({
1499
+ ...(context.storeId ? { storeId: context.storeId } : {}),
1500
+ ...(context.appId ? { appId: context.appId } : {}),
1501
+ });
1502
+ }
1503
+ async upsertAgentRegistry(context, scope, input) {
1504
+ if (!context.superAdmin)
1505
+ throw new CatalogAccessDeniedError({ reason: "super_admin_required" });
1506
+ if (!this.deps.agents)
1507
+ throw new Error("agents dependency is not configured");
1508
+ await this.deps.agents.upsert(scope, input);
1509
+ }
1510
+ async backfillCatalogModel(context, input = {}) {
1511
+ if (!context.superAdmin)
1512
+ throw new CatalogAccessDeniedError({ reason: "super_admin_required" });
1513
+ const fs = this.deps.firestoreStore.firestore;
1514
+ return backfillCatalogModel(fs, input);
1515
+ }
1273
1516
  async ensureBinding(context, input) {
1274
1517
  // only super-admin apps can provision cross-app bindings.
1275
1518
  if (!context.superAdmin && (!context.appId || input.appId !== context.appId)) {
@@ -1334,7 +1577,13 @@ export class Catalox {
1334
1577
  data: mergedData,
1335
1578
  indexed: idx,
1336
1579
  ...(nextScope.kind !== "global" ? { scope: nextScope } : {}),
1337
- updatedAt,
1580
+ metadata: {
1581
+ ...(existing.metadata ?? {}),
1582
+ createdAt: String(existing.metadata?.createdAt ?? updatedAt),
1583
+ lastUpdate: updatedAt,
1584
+ domainIds: Array.isArray(existing.metadata?.domainIds) ? existing.metadata.domainIds : [],
1585
+ agentIds: Array.isArray(existing.metadata?.agentIds) ? existing.metadata.agentIds : [],
1586
+ },
1338
1587
  version: (existing.version ?? 0) + 1,
1339
1588
  ...(this.resolveActorId(_context) ? { updatedBy: this.resolveActorId(_context) } : {}),
1340
1589
  };
@@ -1358,8 +1607,12 @@ export class Catalox {
1358
1607
  sourceMode: "native",
1359
1608
  sourceType: "firebase",
1360
1609
  data: mergedData,
1361
- createdAt: existing.createdAt,
1362
- updatedAt,
1610
+ metadata: {
1611
+ createdAt: String(existing.metadata?.createdAt ?? updatedAt),
1612
+ lastUpdate: updatedAt,
1613
+ domainIds: Array.isArray(existing.metadata?.domainIds) ? existing.metadata.domainIds : [],
1614
+ agentIds: Array.isArray(existing.metadata?.agentIds) ? existing.metadata.agentIds : [],
1615
+ },
1363
1616
  };
1364
1617
  return this.decorateItem(_catalogId, out);
1365
1618
  }
@@ -1449,9 +1702,13 @@ export class Catalox {
1449
1702
  appScopedOwnerId: appId,
1450
1703
  ...(indexed != null ? { indexed } : {}),
1451
1704
  data,
1705
+ metadata: {
1706
+ createdAt: String(existing?.metadata?.createdAt ?? now),
1707
+ lastUpdate: now,
1708
+ domainIds: Array.isArray(existing?.metadata?.domainIds) ? (existing?.metadata).domainIds : [],
1709
+ agentIds: Array.isArray(existing?.metadata?.agentIds) ? (existing?.metadata).agentIds : [],
1710
+ },
1452
1711
  version: (existing?.version ?? 0) + 1,
1453
- ...(existing?.createdAt ? { createdAt: existing.createdAt } : { createdAt: now }),
1454
- updatedAt: now,
1455
1712
  ...(existing?.createdBy ? { createdBy: existing.createdBy } : actorId ? { createdBy: actorId } : {}),
1456
1713
  ...(actorId ? { updatedBy: actorId } : {}),
1457
1714
  });
@@ -1471,8 +1728,12 @@ export class Catalox {
1471
1728
  sourceMode: "native",
1472
1729
  sourceType: "firebase",
1473
1730
  data,
1474
- createdAt: existing?.createdAt ?? now,
1475
- updatedAt: now,
1731
+ metadata: {
1732
+ createdAt: String(existing?.metadata?.createdAt ?? now),
1733
+ lastUpdate: now,
1734
+ domainIds: Array.isArray(existing?.metadata?.domainIds) ? (existing?.metadata).domainIds : [],
1735
+ agentIds: Array.isArray(existing?.metadata?.agentIds) ? (existing?.metadata).agentIds : [],
1736
+ },
1476
1737
  };
1477
1738
  }
1478
1739
  async batchUpsertNativeCatalogItems(context, catalogId, items) {
@@ -1501,8 +1762,7 @@ export class Catalox {
1501
1762
  appScopedOwnerId: appId,
1502
1763
  ...(indexed != null ? { indexed } : {}),
1503
1764
  data: stripped.data,
1504
- createdAt: now,
1505
- updatedAt: now,
1765
+ metadata: { createdAt: now, lastUpdate: now, domainIds: [], agentIds: [] },
1506
1766
  ...(actorId ? { updatedBy: actorId } : {}),
1507
1767
  };
1508
1768
  });
@@ -1622,13 +1882,13 @@ export class Catalox {
1622
1882
  itemId: it.itemId,
1623
1883
  ...(it.title ? { title: it.title } : {}),
1624
1884
  ...(it.subtitle ? { subtitle: it.subtitle } : {}),
1625
- ...(it.updatedAt ? { updatedAt: it.updatedAt } : {}),
1885
+ ...(it.metadata?.lastUpdate ? { updatedAt: String(it.metadata.lastUpdate) } : {}),
1626
1886
  }));
1627
1887
  catalogStats.push({
1628
1888
  catalogId,
1629
1889
  label: c.label,
1630
1890
  sourceMode: c.sourceMode,
1631
- status: c.status,
1891
+ status: c.metadata.status,
1632
1892
  ...(list.issues ? { mappingIssueCount: list.issues.length } : {}),
1633
1893
  topExamples: examples,
1634
1894
  });
@@ -1705,8 +1965,9 @@ export class Catalox {
1705
1965
  if (!catalog)
1706
1966
  throw new CatalogNotFoundError({ catalogId: _catalogId });
1707
1967
  const def = await this.deps.definitions.get(_catalogId);
1708
- if (!def || def.type !== "mapped")
1968
+ if (!def || def.catalogItems.providerType !== "external") {
1709
1969
  throw new CatalogAdapterError({ catalogId: _catalogId, reason: "not_mapped" });
1970
+ }
1710
1971
  const mode = def.sync?.mode;
1711
1972
  if (mode !== "snapshot")
1712
1973
  return { syncStatus: "idle" };
@@ -1726,7 +1987,7 @@ export class Catalox {
1726
1987
  itemId: item.itemId,
1727
1988
  catalogId: _catalogId,
1728
1989
  sourceFingerprint: this.fingerprint(item.data),
1729
- ...(item.updatedAt ? { sourceUpdatedAt: item.updatedAt } : {}),
1990
+ ...(item.metadata?.lastUpdate ? { sourceUpdatedAt: String(item.metadata.lastUpdate) } : {}),
1730
1991
  data: item.data,
1731
1992
  sync: { lastSyncedAt: now, syncStatus: "success" },
1732
1993
  ...(actorId ? { metadata: { syncedBy: actorId } } : {}),
@@ -1920,8 +2181,7 @@ export class Catalox {
1920
2181
  sourceMode: "native",
1921
2182
  sourceType: "firebase",
1922
2183
  data: liveAfter.data,
1923
- createdAt: liveAfter.createdAt,
1924
- updatedAt: liveAfter.updatedAt,
2184
+ metadata: liveAfter.metadata ?? {},
1925
2185
  });
1926
2186
  }
1927
2187
  async replayCatalogToPointInTime(context, catalogId, input) {