@x12i/catalox 3.5.1 → 3.7.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/README.md +155 -10
- package/dist/src/adapters/api/api-adapter.d.ts.map +1 -1
- package/dist/src/adapters/api/api-adapter.js +1 -0
- package/dist/src/adapters/api/api-adapter.js.map +1 -1
- package/dist/src/adapters/mongo/mongo-adapter.d.ts.map +1 -1
- package/dist/src/adapters/mongo/mongo-adapter.js +1 -0
- package/dist/src/adapters/mongo/mongo-adapter.js.map +1 -1
- package/dist/src/catalox/catalog-discovery.d.ts.map +1 -1
- package/dist/src/catalox/catalog-discovery.js +8 -6
- package/dist/src/catalox/catalog-discovery.js.map +1 -1
- package/dist/src/catalox/catalog-lifecycle.d.ts.map +1 -1
- package/dist/src/catalox/catalog-lifecycle.js +26 -19
- package/dist/src/catalox/catalog-lifecycle.js.map +1 -1
- package/dist/src/catalox/catalox-bound.d.ts +48 -1
- package/dist/src/catalox/catalox-bound.d.ts.map +1 -1
- package/dist/src/catalox/catalox-bound.js +24 -0
- package/dist/src/catalox/catalox-bound.js.map +1 -1
- package/dist/src/catalox/catalox.d.ts +94 -2
- package/dist/src/catalox/catalox.d.ts.map +1 -1
- package/dist/src/catalox/catalox.js +596 -75
- package/dist/src/catalox/catalox.js.map +1 -1
- package/dist/src/catalox/create-catalox.d.ts +4 -0
- package/dist/src/catalox/create-catalox.d.ts.map +1 -1
- package/dist/src/catalox/create-catalox.js +15 -0
- package/dist/src/catalox/create-catalox.js.map +1 -1
- package/dist/src/catalox/native-catalog-merge.d.ts.map +1 -1
- package/dist/src/catalox/native-catalog-merge.js +12 -11
- package/dist/src/catalox/native-catalog-merge.js.map +1 -1
- package/dist/src/contracts/agents.d.ts +19 -0
- package/dist/src/contracts/agents.d.ts.map +1 -0
- package/dist/src/contracts/agents.js +2 -0
- package/dist/src/contracts/agents.js.map +1 -0
- package/dist/src/contracts/catalog-types.d.ts +19 -0
- package/dist/src/contracts/catalog-types.d.ts.map +1 -0
- package/dist/src/contracts/catalog-types.js +2 -0
- package/dist/src/contracts/catalog-types.js.map +1 -0
- package/dist/src/contracts/catalogs.d.ts +26 -6
- package/dist/src/contracts/catalogs.d.ts.map +1 -1
- package/dist/src/contracts/catalogs.js.map +1 -1
- package/dist/src/contracts/descriptors.d.ts +11 -0
- package/dist/src/contracts/descriptors.d.ts.map +1 -1
- package/dist/src/contracts/design-objects.d.ts +38 -0
- package/dist/src/contracts/design-objects.d.ts.map +1 -0
- package/dist/src/contracts/design-objects.js +2 -0
- package/dist/src/contracts/design-objects.js.map +1 -0
- package/dist/src/contracts/discovery.d.ts +7 -8
- package/dist/src/contracts/discovery.d.ts.map +1 -1
- package/dist/src/contracts/domains.d.ts +19 -0
- package/dist/src/contracts/domains.d.ts.map +1 -0
- package/dist/src/contracts/domains.js +2 -0
- package/dist/src/contracts/domains.js.map +1 -0
- package/dist/src/contracts/ids.d.ts +2 -0
- package/dist/src/contracts/ids.d.ts.map +1 -1
- package/dist/src/contracts/index.d.ts +11 -5
- package/dist/src/contracts/index.d.ts.map +1 -1
- package/dist/src/contracts/index.js +1 -0
- package/dist/src/contracts/index.js.map +1 -1
- package/dist/src/contracts/inputs.d.ts +19 -0
- package/dist/src/contracts/inputs.d.ts.map +1 -1
- package/dist/src/contracts/items.d.ts +9 -7
- package/dist/src/contracts/items.d.ts.map +1 -1
- package/dist/src/contracts/presentation-binding.d.ts +116 -0
- package/dist/src/contracts/presentation-binding.d.ts.map +1 -0
- package/dist/src/contracts/presentation-binding.js +215 -0
- package/dist/src/contracts/presentation-binding.js.map +1 -0
- package/dist/src/contracts/presentation.d.ts +76 -0
- package/dist/src/contracts/presentation.d.ts.map +1 -1
- package/dist/src/contracts/references.d.ts +40 -0
- package/dist/src/contracts/references.d.ts.map +1 -1
- package/dist/src/contracts/render-map.d.ts +12 -0
- package/dist/src/contracts/render-map.d.ts.map +1 -1
- package/dist/src/firebase/agent-store.d.ts +18 -0
- package/dist/src/firebase/agent-store.d.ts.map +1 -0
- package/dist/src/firebase/agent-store.js +47 -0
- package/dist/src/firebase/agent-store.js.map +1 -0
- package/dist/src/firebase/catalog-type-store.d.ts +22 -0
- package/dist/src/firebase/catalog-type-store.d.ts.map +1 -0
- package/dist/src/firebase/catalog-type-store.js +51 -0
- package/dist/src/firebase/catalog-type-store.js.map +1 -0
- package/dist/src/firebase/definition-store.d.ts +4 -18
- package/dist/src/firebase/definition-store.d.ts.map +1 -1
- package/dist/src/firebase/definition-store.js.map +1 -1
- package/dist/src/firebase/design-object-store.d.ts +15 -0
- package/dist/src/firebase/design-object-store.d.ts.map +1 -0
- package/dist/src/firebase/design-object-store.js +51 -0
- package/dist/src/firebase/design-object-store.js.map +1 -0
- package/dist/src/firebase/domain-store.d.ts +18 -0
- package/dist/src/firebase/domain-store.d.ts.map +1 -0
- package/dist/src/firebase/domain-store.js +47 -0
- package/dist/src/firebase/domain-store.js.map +1 -0
- package/dist/src/firebase/index.d.ts +2 -0
- package/dist/src/firebase/index.d.ts.map +1 -1
- package/dist/src/firebase/index.js +2 -0
- package/dist/src/firebase/index.js.map +1 -1
- package/dist/src/firebase/presentation-profile-store.d.ts +18 -0
- package/dist/src/firebase/presentation-profile-store.d.ts.map +1 -0
- package/dist/src/firebase/presentation-profile-store.js +44 -0
- package/dist/src/firebase/presentation-profile-store.js.map +1 -0
- package/dist/src/firebase/reference-store.d.ts +1 -0
- package/dist/src/firebase/reference-store.d.ts.map +1 -1
- package/dist/src/firebase/reference-store.js +7 -0
- package/dist/src/firebase/reference-store.js.map +1 -1
- package/dist/src/migrations/backfill-catalog-model.d.ts +29 -0
- package/dist/src/migrations/backfill-catalog-model.d.ts.map +1 -0
- package/dist/src/migrations/backfill-catalog-model.js +124 -0
- package/dist/src/migrations/backfill-catalog-model.js.map +1 -0
- package/dist/src/migrations/migrate-native-catalog-layout.js +4 -4
- package/dist/src/migrations/migrate-native-catalog-layout.js.map +1 -1
- package/dist/src/validation/index.d.ts +2 -2
- package/dist/src/validation/index.d.ts.map +1 -1
- package/dist/src/validation/index.js +2 -2
- package/dist/src/validation/index.js.map +1 -1
- package/dist/src/validation/ui-spec-schema.d.ts +5 -0
- package/dist/src/validation/ui-spec-schema.d.ts.map +1 -1
- package/dist/src/validation/ui-spec-schema.js +139 -0
- package/dist/src/validation/ui-spec-schema.js.map +1 -1
- package/dist/src/validation/ui-spec-validate.d.ts +3 -0
- package/dist/src/validation/ui-spec-validate.d.ts.map +1 -1
- package/dist/src/validation/ui-spec-validate.js +13 -1
- package/dist/src/validation/ui-spec-validate.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { compactCatalogFilter } from "../contracts/catalogs.js";
|
|
2
|
-
import { CatalogAccessDeniedError, CatalogAdapterError, CatalogBindingError, CatalogNotFoundError, } from "../contracts/errors.js";
|
|
2
|
+
import { CatalogAccessDeniedError, CatalogAdapterError, CatalogBindingError, CatalogNotFoundError, CatalogValidationError, } from "../contracts/errors.js";
|
|
3
3
|
import { validateMappingSpec } from "../mapping/validate-mapping.js";
|
|
4
4
|
import { executeMapping } from "../mapping/execute-mapping.js";
|
|
5
5
|
import { ApiCatalogAdapter } from "../adapters/api/api-adapter.js";
|
|
@@ -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,141 @@ export class Catalox {
|
|
|
46
52
|
constructor(deps) {
|
|
47
53
|
this.deps = deps;
|
|
48
54
|
}
|
|
55
|
+
encodeRefSegment(value) {
|
|
56
|
+
// Keep IDs stable and URL-safe for use as Firestore doc ids.
|
|
57
|
+
return encodeURIComponent(String(value));
|
|
58
|
+
}
|
|
59
|
+
buildReferenceId(input) {
|
|
60
|
+
const parts = [
|
|
61
|
+
"ref",
|
|
62
|
+
this.encodeRefSegment(input.fromCatalogId),
|
|
63
|
+
this.encodeRefSegment(input.fromItemId),
|
|
64
|
+
this.encodeRefSegment(input.relationType),
|
|
65
|
+
this.encodeRefSegment(input.toCatalogId),
|
|
66
|
+
this.encodeRefSegment(input.toItemId),
|
|
67
|
+
];
|
|
68
|
+
return parts.join("|");
|
|
69
|
+
}
|
|
70
|
+
normalizeRelationWrites(value) {
|
|
71
|
+
if (!Array.isArray(value))
|
|
72
|
+
return [];
|
|
73
|
+
const out = [];
|
|
74
|
+
for (const v of value) {
|
|
75
|
+
if (!v || typeof v !== "object")
|
|
76
|
+
continue;
|
|
77
|
+
const r = v;
|
|
78
|
+
if (r.toCatalogId == null || r.toItemId == null || r.relationType == null)
|
|
79
|
+
continue;
|
|
80
|
+
out.push({
|
|
81
|
+
toCatalogId: String(r.toCatalogId),
|
|
82
|
+
toItemId: String(r.toItemId),
|
|
83
|
+
relationType: String(r.relationType),
|
|
84
|
+
...(r.label != null ? { label: String(r.label) } : {}),
|
|
85
|
+
...(r.metadata != null && typeof r.metadata === "object" ? { metadata: r.metadata } : {}),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
async validateRelationsAgainstDescriptor(context, params) {
|
|
91
|
+
const rules = params.descriptor?.relationRules;
|
|
92
|
+
if (!rules?.length)
|
|
93
|
+
return { isValid: true, issues: [] };
|
|
94
|
+
const ruleByType = new Map();
|
|
95
|
+
for (const r of rules) {
|
|
96
|
+
if (r && typeof r === "object" && r.relationType != null) {
|
|
97
|
+
ruleByType.set(String(r.relationType), r);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const issues = [];
|
|
101
|
+
const counts = new Map();
|
|
102
|
+
for (const ref of params.refs) {
|
|
103
|
+
const rt = String(ref.relationType);
|
|
104
|
+
counts.set(rt, (counts.get(rt) ?? 0) + 1);
|
|
105
|
+
const rule = ruleByType.get(rt);
|
|
106
|
+
if (!rule) {
|
|
107
|
+
issues.push({
|
|
108
|
+
code: "relation.disallowed_type",
|
|
109
|
+
severity: "error",
|
|
110
|
+
message: `relationType "${rt}" is not allowed by descriptor for catalog "${String(params.catalogId)}".`,
|
|
111
|
+
path: "relations",
|
|
112
|
+
});
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (Array.isArray(rule.targetCatalogIds) && rule.targetCatalogIds.length) {
|
|
116
|
+
const allowed = new Set(rule.targetCatalogIds.map((x) => String(x)));
|
|
117
|
+
if (!allowed.has(String(ref.toCatalogId))) {
|
|
118
|
+
issues.push({
|
|
119
|
+
code: "relation.disallowed_target_catalog",
|
|
120
|
+
severity: "error",
|
|
121
|
+
message: `relationType "${rt}" cannot target catalog "${String(ref.toCatalogId)}".`,
|
|
122
|
+
path: "relations",
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (Array.isArray(rule.targetCatalogTypes) && rule.targetCatalogTypes.length) {
|
|
127
|
+
try {
|
|
128
|
+
const targetDescriptor = await this.getCatalogDescriptor(context, ref.toCatalogId);
|
|
129
|
+
const targetType = targetDescriptor?.catalogType ? String(targetDescriptor.catalogType) : undefined;
|
|
130
|
+
const allowedTypes = new Set(rule.targetCatalogTypes.map((x) => String(x)));
|
|
131
|
+
if (!targetType || !allowedTypes.has(targetType)) {
|
|
132
|
+
issues.push({
|
|
133
|
+
code: "relation.disallowed_target_type",
|
|
134
|
+
severity: "error",
|
|
135
|
+
message: `relationType "${rt}" cannot target catalogType "${String(targetType ?? "unknown")}".`,
|
|
136
|
+
path: "relations",
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
issues.push({
|
|
142
|
+
code: "relation.target_type_unverifiable",
|
|
143
|
+
severity: "warning",
|
|
144
|
+
message: `relationType "${rt}" target catalogType could not be verified for "${String(ref.toCatalogId)}".`,
|
|
145
|
+
path: "relations",
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
for (const [rt, rule] of ruleByType.entries()) {
|
|
151
|
+
const required = Boolean(rule.required);
|
|
152
|
+
const multiple = rule.multiple !== false;
|
|
153
|
+
const count = counts.get(rt) ?? 0;
|
|
154
|
+
if (required && count === 0) {
|
|
155
|
+
issues.push({
|
|
156
|
+
code: "relation.missing_required",
|
|
157
|
+
severity: "error",
|
|
158
|
+
message: `Missing required relationType "${rt}".`,
|
|
159
|
+
path: "relations",
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
if (!multiple && count > 1) {
|
|
163
|
+
issues.push({
|
|
164
|
+
code: "relation.too_many",
|
|
165
|
+
severity: "error",
|
|
166
|
+
message: `relationType "${rt}" allows only one relation, but found ${count}.`,
|
|
167
|
+
path: "relations",
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return { isValid: issues.every((i) => i.severity !== "error"), issues };
|
|
172
|
+
}
|
|
173
|
+
async validateCatalogTypeForContext(context, catalogType) {
|
|
174
|
+
const registry = this.deps.catalogTypes
|
|
175
|
+
? await this.deps.catalogTypes.resolveForContext({
|
|
176
|
+
...(context.storeId ? { storeId: context.storeId } : {}),
|
|
177
|
+
...(context.appId ? { appId: context.appId } : {}),
|
|
178
|
+
})
|
|
179
|
+
: null;
|
|
180
|
+
if (!registry)
|
|
181
|
+
return; // registry not configured or no entry exists yet
|
|
182
|
+
const allowed = new Set((registry.types ?? []).map((t) => String(t)));
|
|
183
|
+
if (!allowed.has(String(catalogType))) {
|
|
184
|
+
throw new CatalogAdapterError({
|
|
185
|
+
reason: "catalog_type_not_allowed",
|
|
186
|
+
message: `catalogType "${String(catalogType)}" is not allowed for scope ${JSON.stringify(registry.scope)}.`,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
49
190
|
catalogLifecycleDeps() {
|
|
50
191
|
return {
|
|
51
192
|
firestoreStore: this.deps.firestoreStore,
|
|
@@ -245,7 +386,7 @@ export class Catalox {
|
|
|
245
386
|
data: (i.data ?? {}),
|
|
246
387
|
...(i.title != null ? { title: i.title } : {}),
|
|
247
388
|
...(i.subtitle != null ? { subtitle: i.subtitle } : {}),
|
|
248
|
-
...(i.status != null ? { status: i.status } : {}),
|
|
389
|
+
...(i.metadata?.status != null ? { status: String(i.metadata.status) } : {}),
|
|
249
390
|
...(i.metadata != null ? { metadata: i.metadata } : {}),
|
|
250
391
|
})),
|
|
251
392
|
...(list.total != null ? { total: list.total } : {}),
|
|
@@ -269,6 +410,119 @@ export class Catalox {
|
|
|
269
410
|
actions: {},
|
|
270
411
|
};
|
|
271
412
|
}
|
|
413
|
+
async getDesignObject(context, designId) {
|
|
414
|
+
void context;
|
|
415
|
+
if (!this.deps.designObjects)
|
|
416
|
+
throw new Error("designObjects dependency is not configured");
|
|
417
|
+
return this.deps.designObjects.get(String(designId));
|
|
418
|
+
}
|
|
419
|
+
async listDesignObjects(context, scope) {
|
|
420
|
+
void context;
|
|
421
|
+
if (!this.deps.designObjects)
|
|
422
|
+
throw new Error("designObjects dependency is not configured");
|
|
423
|
+
return this.deps.designObjects.listByScope(scope);
|
|
424
|
+
}
|
|
425
|
+
async getCatalogPresentationBinding(context, catalogId, role, options) {
|
|
426
|
+
const descriptor = await this.getCatalogDescriptor(context, catalogId);
|
|
427
|
+
const profiles = options?.profiles
|
|
428
|
+
? options.profiles
|
|
429
|
+
: this.deps.presentationProfiles
|
|
430
|
+
? await this.deps.presentationProfiles.listForCatalog?.({
|
|
431
|
+
catalogId: String(catalogId),
|
|
432
|
+
...(descriptor?.catalogType ? { catalogType: String(descriptor.catalogType) } : {}),
|
|
433
|
+
})
|
|
434
|
+
: [];
|
|
435
|
+
const bindingBase = resolveCatalogPresentationBinding({
|
|
436
|
+
descriptor,
|
|
437
|
+
profiles,
|
|
438
|
+
role: role,
|
|
439
|
+
...(options?.mode ? { mode: options.mode } : {}),
|
|
440
|
+
...(options?.displayContext ? { displayContext: options.displayContext } : {}),
|
|
441
|
+
...(options?.designScope ? { designScope: options.designScope } : {}),
|
|
442
|
+
...(options?.designObjects ? { designObjects: options.designObjects } : {}),
|
|
443
|
+
});
|
|
444
|
+
// Optionally fetch snippet metadata when a snippetRef is involved.
|
|
445
|
+
if (options?.includeSnippet && bindingBase.customRenderer?.entry?.snippetRef) {
|
|
446
|
+
const entry = bindingBase.customRenderer.entry;
|
|
447
|
+
const snippetId = entry.snippetRef?.snippetId;
|
|
448
|
+
const catalogScoped = entry.snippetRef?.catalogScoped !== false;
|
|
449
|
+
if (!this.deps.rendererSnippets)
|
|
450
|
+
throw new Error("rendererSnippets dependency is not configured");
|
|
451
|
+
// Support canonical catalog-scoped ids; for anything else, require host-side snippet resolution.
|
|
452
|
+
if (!snippetId) {
|
|
453
|
+
const rec = await this.getCatalogRendererSnippet(context, catalogId, role, options?.mode);
|
|
454
|
+
return { ...bindingBase, customRenderer: { ...bindingBase.customRenderer, snippet: rec } };
|
|
455
|
+
}
|
|
456
|
+
if (catalogScoped) {
|
|
457
|
+
const expected1 = buildRendererSnippetDocId({ catalogId: String(catalogId), role: role, ...(options?.mode ? { mode: options.mode } : {}) });
|
|
458
|
+
if (String(snippetId) !== expected1) {
|
|
459
|
+
throw new Error(`snippetRef.snippetId is not a canonical catalog-scoped id: ${String(snippetId)}`);
|
|
460
|
+
}
|
|
461
|
+
const rec = await this.getCatalogRendererSnippet(context, catalogId, role, options?.mode);
|
|
462
|
+
return { ...bindingBase, customRenderer: { ...bindingBase.customRenderer, snippet: rec } };
|
|
463
|
+
}
|
|
464
|
+
throw new Error("Global (non-catalog-scoped) snippetRef resolution is host-defined.");
|
|
465
|
+
}
|
|
466
|
+
// Optionally include design merge when requested and available.
|
|
467
|
+
if (options?.includeDesignObjects && !options?.designObjects) {
|
|
468
|
+
if (!this.deps.designObjects)
|
|
469
|
+
throw new Error("designObjects dependency is not configured");
|
|
470
|
+
const designObjects = await this.deps.designObjects.listByScope?.({
|
|
471
|
+
...(options?.designScope?.storeId ? { storeId: options.designScope.storeId } : {}),
|
|
472
|
+
...(options?.designScope?.appId ? { appId: options.designScope.appId } : {}),
|
|
473
|
+
...(options?.designScope?.accountId ? { accountId: options.designScope.accountId } : {}),
|
|
474
|
+
...(options?.designScope?.agentId ? { agentId: options.designScope.agentId } : {}),
|
|
475
|
+
});
|
|
476
|
+
return resolveCatalogPresentationBinding({
|
|
477
|
+
descriptor,
|
|
478
|
+
profiles,
|
|
479
|
+
role: role,
|
|
480
|
+
...(options?.mode ? { mode: options.mode } : {}),
|
|
481
|
+
...(options?.displayContext ? { displayContext: options.displayContext } : {}),
|
|
482
|
+
...(options?.designScope ? { designScope: options.designScope } : {}),
|
|
483
|
+
designObjects,
|
|
484
|
+
// Preserve snippet result if caller already asked for it separately above.
|
|
485
|
+
...(bindingBase.customRenderer?.snippet !== undefined ? { snippet: bindingBase.customRenderer.snippet } : {}),
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
return bindingBase;
|
|
489
|
+
}
|
|
490
|
+
async buildCatalogListPresentationSurface(context, catalogId, options) {
|
|
491
|
+
const map = await this.buildCatalogListRenderMap(context, catalogId, {
|
|
492
|
+
...(options?.limit != null ? { limit: options.limit } : {}),
|
|
493
|
+
...(options?.resolveSources != null ? { resolveSources: options.resolveSources } : {}),
|
|
494
|
+
...(options?.maxSourceOptions != null ? { maxSourceOptions: options.maxSourceOptions } : {}),
|
|
495
|
+
...(options?.displayContext != null ? { displayContext: options.displayContext } : {}),
|
|
496
|
+
});
|
|
497
|
+
const binding = await this.getCatalogPresentationBinding(context, catalogId, (options?.displayContext ?? "grid") === "grid" ? "grid" : "list", {
|
|
498
|
+
...(options?.mode ? { mode: options.mode } : {}),
|
|
499
|
+
displayContext: options?.displayContext ?? "grid",
|
|
500
|
+
...(options?.includeSnippet ? { includeSnippet: true } : {}),
|
|
501
|
+
...(options?.includeDesignObjects ? { includeDesignObjects: true } : {}),
|
|
502
|
+
...(options?.designScope ? { designScope: options.designScope } : {}),
|
|
503
|
+
...(options?.profiles ? { profiles: options.profiles } : {}),
|
|
504
|
+
...(options?.designObjects ? { designObjects: options.designObjects } : {}),
|
|
505
|
+
});
|
|
506
|
+
return { map, binding };
|
|
507
|
+
}
|
|
508
|
+
async buildCatalogItemPresentationSurface(context, catalogId, itemId, options) {
|
|
509
|
+
const map = await this.buildCatalogItemRenderMap(context, catalogId, itemId, {
|
|
510
|
+
...(options?.includeReferences != null ? { includeReferences: options.includeReferences } : {}),
|
|
511
|
+
...(options?.resolveSources != null ? { resolveSources: options.resolveSources } : {}),
|
|
512
|
+
...(options?.maxSourceOptions != null ? { maxSourceOptions: options.maxSourceOptions } : {}),
|
|
513
|
+
...(options?.displayContext != null ? { displayContext: options.displayContext } : {}),
|
|
514
|
+
});
|
|
515
|
+
const binding = await this.getCatalogPresentationBinding(context, catalogId, "item", {
|
|
516
|
+
...(options?.mode ? { mode: options.mode } : {}),
|
|
517
|
+
displayContext: "form",
|
|
518
|
+
...(options?.includeSnippet ? { includeSnippet: true } : {}),
|
|
519
|
+
...(options?.includeDesignObjects ? { includeDesignObjects: true } : {}),
|
|
520
|
+
...(options?.designScope ? { designScope: options.designScope } : {}),
|
|
521
|
+
...(options?.profiles ? { profiles: options.profiles } : {}),
|
|
522
|
+
...(options?.designObjects ? { designObjects: options.designObjects } : {}),
|
|
523
|
+
});
|
|
524
|
+
return { map, binding };
|
|
525
|
+
}
|
|
272
526
|
async buildCatalogItemRenderMap(context, catalogId, itemId, options) {
|
|
273
527
|
const descriptor = await this.getCatalogDescriptor(context, catalogId);
|
|
274
528
|
const got = await this.getCatalogItem(context, catalogId, itemId);
|
|
@@ -295,6 +549,15 @@ export class Catalox {
|
|
|
295
549
|
const refs = options?.includeReferences
|
|
296
550
|
? await this.getCatalogItemReferences(context, catalogId, itemId)
|
|
297
551
|
: [];
|
|
552
|
+
const referenceViews = refs.length
|
|
553
|
+
? refs.map((r) => ({
|
|
554
|
+
toCatalogId: String(r.toCatalogId),
|
|
555
|
+
toItemId: String(r.toItemId),
|
|
556
|
+
relationType: String(r.relationType),
|
|
557
|
+
...(r.label != null ? { label: r.label } : {}),
|
|
558
|
+
...(r.metadata != null ? { metadata: r.metadata } : {}),
|
|
559
|
+
}))
|
|
560
|
+
: [];
|
|
298
561
|
return {
|
|
299
562
|
catalogId: String(catalogId),
|
|
300
563
|
itemId: String(itemId),
|
|
@@ -302,23 +565,13 @@ export class Catalox {
|
|
|
302
565
|
data: (item.data ?? {}),
|
|
303
566
|
...(item.title != null ? { title: item.title } : {}),
|
|
304
567
|
...(item.subtitle != null ? { subtitle: item.subtitle } : {}),
|
|
305
|
-
...(item.status != null ? { status: item.status } : {}),
|
|
568
|
+
...(item.metadata?.status != null ? { status: String(item.metadata.status) } : {}),
|
|
306
569
|
...(item.metadata != null ? { metadata: item.metadata } : {}),
|
|
307
|
-
...(item.createdAt != null ? { createdAt: item.createdAt } : {}),
|
|
308
|
-
...(item.
|
|
570
|
+
...(item.metadata?.createdAt != null ? { createdAt: String(item.metadata.createdAt) } : {}),
|
|
571
|
+
...(item.metadata?.lastUpdate != null ? { updatedAt: String(item.metadata.lastUpdate) } : {}),
|
|
309
572
|
},
|
|
310
573
|
resolvedSources,
|
|
311
|
-
...(
|
|
312
|
-
? {
|
|
313
|
-
references: refs.map((r) => ({
|
|
314
|
-
toCatalogId: String(r.toCatalogId),
|
|
315
|
-
toItemId: String(r.toItemId),
|
|
316
|
-
relationType: String(r.relationType),
|
|
317
|
-
...(r.label != null ? { label: r.label } : {}),
|
|
318
|
-
...(r.metadata != null ? { metadata: r.metadata } : {}),
|
|
319
|
-
})),
|
|
320
|
-
}
|
|
321
|
-
: {}),
|
|
574
|
+
...(referenceViews.length ? { relations: referenceViews, references: referenceViews } : {}),
|
|
322
575
|
...(descriptor?.presentationSpec ? { presentation: descriptor.presentationSpec } : {}),
|
|
323
576
|
capabilities: descriptor?.capabilities ?? {
|
|
324
577
|
canList: true,
|
|
@@ -343,12 +596,14 @@ export class Catalox {
|
|
|
343
596
|
return Object.keys(out).length ? out : undefined;
|
|
344
597
|
}
|
|
345
598
|
stripReservedWriteFields(input) {
|
|
346
|
-
const { indexed, scope, ...rest } = input;
|
|
599
|
+
const { indexed, scope, relations, references, ...rest } = input;
|
|
347
600
|
const parsedScope = scope != null ? parseWriteScopeInput(scope) : undefined;
|
|
601
|
+
const parsedRelations = this.normalizeRelationWrites(relations ?? references);
|
|
348
602
|
return {
|
|
349
603
|
data: rest,
|
|
350
604
|
...(indexed != null ? { indexed: indexed } : {}),
|
|
351
605
|
...(parsedScope != null ? { scope: parsedScope } : {}),
|
|
606
|
+
...(parsedRelations.length ? { relations: parsedRelations } : {}),
|
|
352
607
|
};
|
|
353
608
|
}
|
|
354
609
|
normalizeListFetchScope(scope) {
|
|
@@ -453,8 +708,13 @@ export class Catalox {
|
|
|
453
708
|
...item,
|
|
454
709
|
...(title != null ? { title: String(title) } : {}),
|
|
455
710
|
...(subtitle != null ? { subtitle: String(subtitle) } : {}),
|
|
456
|
-
|
|
457
|
-
|
|
711
|
+
metadata: {
|
|
712
|
+
...(item.metadata ?? {}),
|
|
713
|
+
...(item.metadata?.domainIds == null ? { domainIds: [] } : {}),
|
|
714
|
+
...(item.metadata?.agentIds == null ? { agentIds: [] } : {}),
|
|
715
|
+
...(status != null ? { status: String(status) } : {}),
|
|
716
|
+
...(updatedAt != null && item.metadata?.lastUpdate == null ? { lastUpdate: String(updatedAt) } : {}),
|
|
717
|
+
},
|
|
458
718
|
};
|
|
459
719
|
}
|
|
460
720
|
async listAppCatalogs(context, input) {
|
|
@@ -470,26 +730,26 @@ export class Catalox {
|
|
|
470
730
|
const cat = byId.get(b.catalogId);
|
|
471
731
|
if (!cat)
|
|
472
732
|
continue;
|
|
473
|
-
if (!input?.includeDisabled && cat.status !== "active")
|
|
733
|
+
if (!input?.includeDisabled && cat.metadata.status !== "active")
|
|
474
734
|
continue;
|
|
475
735
|
const descriptor = await this.deps.descriptors.get(cat.catalogId);
|
|
476
736
|
const description = descriptor?.descriptor.description ?? cat.description;
|
|
477
737
|
const itemLabel = descriptor?.descriptor.itemLabel ?? cat.itemLabel;
|
|
478
738
|
const visibility = descriptor?.descriptor.visibility;
|
|
479
739
|
const descriptorVersion = descriptor?.descriptorVersion;
|
|
480
|
-
const metadata = descriptor?.descriptor.metadata ??
|
|
740
|
+
const metadata = { ...cat.metadata, ...(descriptor?.descriptor.metadata ?? {}) };
|
|
481
741
|
out.push({
|
|
482
742
|
catalogId: cat.catalogId,
|
|
483
743
|
label: descriptor?.descriptor.label ?? cat.name,
|
|
484
744
|
...(description != null ? { description } : {}),
|
|
485
745
|
...(itemLabel != null ? { itemLabel } : {}),
|
|
746
|
+
catalogType: cat.catalogType,
|
|
486
747
|
sourceMode: cat.sourceMode,
|
|
487
748
|
...(cat.mappedSourceType != null ? { mappedSourceType: cat.mappedSourceType } : {}),
|
|
488
|
-
status: cat.status,
|
|
489
749
|
...(visibility != null ? { visibility } : {}),
|
|
490
750
|
access: b.access,
|
|
491
751
|
...(descriptorVersion != null ? { descriptorVersion } : {}),
|
|
492
|
-
|
|
752
|
+
metadata,
|
|
493
753
|
});
|
|
494
754
|
}
|
|
495
755
|
return out.filter((e) => (input?.includeHidden ? true : e.visibility !== "hidden"));
|
|
@@ -536,26 +796,26 @@ export class Catalox {
|
|
|
536
796
|
const cat = byId.get(b.catalogId);
|
|
537
797
|
if (!cat)
|
|
538
798
|
continue;
|
|
539
|
-
if (!input.includeDisabled && cat.status !== "active")
|
|
799
|
+
if (!input.includeDisabled && cat.metadata.status !== "active")
|
|
540
800
|
continue;
|
|
541
801
|
const descriptor = await this.deps.descriptors.get(cat.catalogId);
|
|
542
802
|
const description = descriptor?.descriptor.description ?? cat.description;
|
|
543
803
|
const itemLabel = descriptor?.descriptor.itemLabel ?? cat.itemLabel;
|
|
544
804
|
const visibility = descriptor?.descriptor.visibility;
|
|
545
805
|
const descriptorVersion = descriptor?.descriptorVersion;
|
|
546
|
-
const metadata = descriptor?.descriptor.metadata ??
|
|
806
|
+
const metadata = { ...cat.metadata, ...(descriptor?.descriptor.metadata ?? {}) };
|
|
547
807
|
out.push({
|
|
548
808
|
catalogId: cat.catalogId,
|
|
549
809
|
label: descriptor?.descriptor.label ?? cat.name,
|
|
550
810
|
...(description != null ? { description } : {}),
|
|
551
811
|
...(itemLabel != null ? { itemLabel } : {}),
|
|
812
|
+
catalogType: cat.catalogType,
|
|
552
813
|
sourceMode: cat.sourceMode,
|
|
553
814
|
...(cat.mappedSourceType != null ? { mappedSourceType: cat.mappedSourceType } : {}),
|
|
554
|
-
status: cat.status,
|
|
555
815
|
...(visibility != null ? { visibility } : {}),
|
|
556
816
|
access: b.access,
|
|
557
817
|
...(descriptorVersion != null ? { descriptorVersion } : {}),
|
|
558
|
-
|
|
818
|
+
metadata,
|
|
559
819
|
});
|
|
560
820
|
}
|
|
561
821
|
return out.filter((e) => (input.includeHidden ? true : e.visibility !== "hidden"));
|
|
@@ -679,9 +939,19 @@ export class Catalox {
|
|
|
679
939
|
};
|
|
680
940
|
}
|
|
681
941
|
const def = await this.deps.definitions.get(catalogId);
|
|
682
|
-
if (!def
|
|
942
|
+
if (!def)
|
|
683
943
|
throw new CatalogAdapterError({ catalogId, reason: "missing_definition" });
|
|
684
|
-
|
|
944
|
+
if (def.catalogItems.providerType !== "external") {
|
|
945
|
+
throw new CatalogAdapterError({ catalogId, reason: "missing_definition" });
|
|
946
|
+
}
|
|
947
|
+
const map = def.catalogItems.map;
|
|
948
|
+
const mappingId = map?.mappingId;
|
|
949
|
+
const adapterId = map?.adapterId;
|
|
950
|
+
if (!mappingId)
|
|
951
|
+
throw new CatalogAdapterError({ catalogId, reason: "missing_mapping" });
|
|
952
|
+
if (!adapterId)
|
|
953
|
+
throw new CatalogAdapterError({ catalogId, reason: "missing_adapter" });
|
|
954
|
+
const mapping = await this.deps.mappings.get(mappingId);
|
|
685
955
|
if (!mapping)
|
|
686
956
|
throw new CatalogAdapterError({ catalogId, reason: "missing_mapping" });
|
|
687
957
|
const mappingIssues = validateMappingSpec(mapping.mapping);
|
|
@@ -694,10 +964,10 @@ export class Catalox {
|
|
|
694
964
|
return rest;
|
|
695
965
|
})()
|
|
696
966
|
: undefined;
|
|
697
|
-
if (def.
|
|
967
|
+
if (def.catalogItems.provider === "mongo") {
|
|
698
968
|
if (!this.deps.mongoAdapter)
|
|
699
969
|
throw new CatalogAdapterError({ catalogId, reason: "mongo_adapter_unconfigured" });
|
|
700
|
-
const adapterConfig = await this.deps.adapters.get(
|
|
970
|
+
const adapterConfig = await this.deps.adapters.get(adapterId);
|
|
701
971
|
if (!adapterConfig)
|
|
702
972
|
throw new CatalogAdapterError({ catalogId, reason: "missing_adapter" });
|
|
703
973
|
const result = await this.deps.mongoAdapter.listItems(appCtx, catalogId, adapterConfig, { mapping: mapping.mapping, ...(mapping.options ? { options: mapping.options } : {}) }, mappedListQueryOptions);
|
|
@@ -705,10 +975,10 @@ export class Catalox {
|
|
|
705
975
|
? { listOutcome: "ok", items: result.items, issues: result.issues }
|
|
706
976
|
: { listOutcome: "ok", items: result.items };
|
|
707
977
|
}
|
|
708
|
-
if (def.
|
|
978
|
+
if (def.catalogItems.provider === "api") {
|
|
709
979
|
if (!this.deps.apiAdapter)
|
|
710
980
|
throw new CatalogAdapterError({ catalogId, reason: "api_adapter_unconfigured" });
|
|
711
|
-
const adapterConfig = await this.deps.adapters.get(
|
|
981
|
+
const adapterConfig = await this.deps.adapters.get(adapterId);
|
|
712
982
|
if (!adapterConfig)
|
|
713
983
|
throw new CatalogAdapterError({ catalogId, reason: "missing_adapter" });
|
|
714
984
|
const result = await this.deps.apiAdapter.listItems(appCtx, catalogId, adapterConfig, { responseMapping: mapping.mapping, ...(mapping.options ? { options: mapping.options } : {}) }, mappedListQueryOptions);
|
|
@@ -767,8 +1037,6 @@ export class Catalox {
|
|
|
767
1037
|
sourceType: "firebase",
|
|
768
1038
|
data: rec.data,
|
|
769
1039
|
metadata: meta,
|
|
770
|
-
createdAt: rec.createdAt,
|
|
771
|
-
updatedAt: rec.updatedAt,
|
|
772
1040
|
};
|
|
773
1041
|
return { outcome: "found", item: await this.decorateItem(catalogId, base) };
|
|
774
1042
|
}
|
|
@@ -829,8 +1097,12 @@ export class Catalox {
|
|
|
829
1097
|
async validateCatalog(_context, _catalogId) {
|
|
830
1098
|
return { isValid: true, issues: [] };
|
|
831
1099
|
}
|
|
832
|
-
async validateCatalogItem(
|
|
833
|
-
|
|
1100
|
+
async validateCatalogItem(context, catalogId, itemId) {
|
|
1101
|
+
const descriptor = await this.getCatalogDescriptor(context, catalogId);
|
|
1102
|
+
if (!descriptor)
|
|
1103
|
+
return { isValid: true, issues: [] };
|
|
1104
|
+
const refs = await this.getCatalogItemReferences(context, catalogId, itemId);
|
|
1105
|
+
return this.validateRelationsAgainstDescriptor(context, { catalogId, itemId, descriptor, refs });
|
|
834
1106
|
}
|
|
835
1107
|
async getCatalogItemReferences(context, catalogId, itemId) {
|
|
836
1108
|
const appId = await this.resolveAppIdForCatalogAccess({ context, catalogId, required: "read" });
|
|
@@ -844,6 +1116,59 @@ export class Catalox {
|
|
|
844
1116
|
const refs = await this.deps.references.listByCatalog(catalogId);
|
|
845
1117
|
return refs;
|
|
846
1118
|
}
|
|
1119
|
+
async upsertCatalogItemRelation(context, input) {
|
|
1120
|
+
const fromCatalogId = input.fromCatalogId;
|
|
1121
|
+
const toCatalogId = input.toCatalogId;
|
|
1122
|
+
const fromAppId = await this.resolveAppIdForCatalogAccess({ context, catalogId: fromCatalogId, required: "write" });
|
|
1123
|
+
await this.deps.authz.requireBindingAccess(context, fromAppId, fromCatalogId, "write");
|
|
1124
|
+
// Ensure target catalog is at least readable by the actor (prevents creating opaque links).
|
|
1125
|
+
const toAppId = await this.resolveAppIdForCatalogAccess({ context, catalogId: toCatalogId, required: "read" });
|
|
1126
|
+
await this.deps.authz.requireBindingAccess(context, toAppId, toCatalogId, "read");
|
|
1127
|
+
const now = new Date().toISOString();
|
|
1128
|
+
const referenceId = this.buildReferenceId({
|
|
1129
|
+
fromCatalogId: String(input.fromCatalogId),
|
|
1130
|
+
fromItemId: String(input.fromItemId),
|
|
1131
|
+
relationType: String(input.relationType),
|
|
1132
|
+
toCatalogId: String(input.toCatalogId),
|
|
1133
|
+
toItemId: String(input.toItemId),
|
|
1134
|
+
});
|
|
1135
|
+
await this.deps.references.upsert({
|
|
1136
|
+
referenceId,
|
|
1137
|
+
fromCatalogId,
|
|
1138
|
+
fromItemId: input.fromItemId,
|
|
1139
|
+
toCatalogId,
|
|
1140
|
+
toItemId: input.toItemId,
|
|
1141
|
+
relationType: String(input.relationType),
|
|
1142
|
+
...(input.label != null ? { label: input.label } : {}),
|
|
1143
|
+
...(input.metadata != null ? { metadata: input.metadata } : {}),
|
|
1144
|
+
createdAt: now,
|
|
1145
|
+
updatedAt: now,
|
|
1146
|
+
});
|
|
1147
|
+
return { referenceId };
|
|
1148
|
+
}
|
|
1149
|
+
async deleteCatalogItemRelation(context, input) {
|
|
1150
|
+
const referenceId = "referenceId" in input
|
|
1151
|
+
? String(input.referenceId)
|
|
1152
|
+
: this.buildReferenceId({
|
|
1153
|
+
fromCatalogId: String(input.fromCatalogId),
|
|
1154
|
+
fromItemId: String(input.fromItemId),
|
|
1155
|
+
relationType: String(input.relationType),
|
|
1156
|
+
toCatalogId: String(input.toCatalogId),
|
|
1157
|
+
toItemId: String(input.toItemId),
|
|
1158
|
+
});
|
|
1159
|
+
// Best-effort access check: if the caller provides endpoints, enforce write on fromCatalog.
|
|
1160
|
+
if (!("referenceId" in input)) {
|
|
1161
|
+
const fromCatalogId = input.fromCatalogId;
|
|
1162
|
+
const appId = await this.resolveAppIdForCatalogAccess({ context, catalogId: fromCatalogId, required: "write" });
|
|
1163
|
+
await this.deps.authz.requireBindingAccess(context, appId, fromCatalogId, "write");
|
|
1164
|
+
}
|
|
1165
|
+
await this.deps.references.delete(referenceId);
|
|
1166
|
+
}
|
|
1167
|
+
async listCatalogItemRelationsToItem(context, catalogId, itemId) {
|
|
1168
|
+
const appId = await this.resolveAppIdForCatalogAccess({ context, catalogId, required: "read" });
|
|
1169
|
+
await this.deps.authz.requireBindingAccess(context, appId, catalogId, "read");
|
|
1170
|
+
return this.deps.references.listToItem(catalogId, itemId);
|
|
1171
|
+
}
|
|
847
1172
|
// Spec methods below are stubs for now; filled in by later todos.
|
|
848
1173
|
async getApp(_context, appId) {
|
|
849
1174
|
const resolved = appId ?? _context.appId;
|
|
@@ -854,18 +1179,35 @@ export class Catalox {
|
|
|
854
1179
|
async createCatalog(_context, _input) {
|
|
855
1180
|
const now = new Date().toISOString();
|
|
856
1181
|
const catalogId = (_input.catalogId ?? randomUUID());
|
|
1182
|
+
await this.validateCatalogTypeForContext(_context, _input.catalogType);
|
|
857
1183
|
await this.deps.catalogs.upsert({
|
|
858
1184
|
catalogId,
|
|
859
1185
|
name: _input.name,
|
|
860
1186
|
...(_input.description != null ? { description: _input.description } : {}),
|
|
1187
|
+
catalogType: _input.catalogType,
|
|
861
1188
|
sourceMode: _input.sourceMode,
|
|
862
1189
|
...(_input.sourceMode === "mapped" && _input.mapped?.sourceType
|
|
863
1190
|
? { mappedSourceType: _input.mapped.sourceType }
|
|
864
1191
|
: {}),
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
1192
|
+
catalogItems: _input.sourceMode === "native"
|
|
1193
|
+
? {
|
|
1194
|
+
providerType: "internal",
|
|
1195
|
+
...(_input.native?.itemSchema != null ? { itemSchema: _input.native.itemSchema } : {}),
|
|
1196
|
+
metadata: {},
|
|
1197
|
+
}
|
|
1198
|
+
: {
|
|
1199
|
+
providerType: "external",
|
|
1200
|
+
provider: _input.mapped?.sourceType ?? "api",
|
|
1201
|
+
// map references existing adapter + mapping documents (see createCatalog mapped branch below)
|
|
1202
|
+
map: {},
|
|
1203
|
+
metadata: {},
|
|
1204
|
+
},
|
|
1205
|
+
metadata: {
|
|
1206
|
+
status: "active",
|
|
1207
|
+
createdAt: now,
|
|
1208
|
+
lastUpdate: now,
|
|
1209
|
+
...(_input.metadata ?? {}),
|
|
1210
|
+
},
|
|
869
1211
|
});
|
|
870
1212
|
if (_input.sourceMode === "native") {
|
|
871
1213
|
if (!_input.native)
|
|
@@ -873,12 +1215,12 @@ export class Catalox {
|
|
|
873
1215
|
const collectionPath = _input.native.firestoreCollectionPath ?? nativeItemsCollectionId(catalogId);
|
|
874
1216
|
await this.deps.definitions.upsert({
|
|
875
1217
|
catalogId,
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
1218
|
+
sourceMode: "native",
|
|
1219
|
+
catalogItems: {
|
|
1220
|
+
providerType: "internal",
|
|
1221
|
+
...(_input.native.itemSchema != null ? { itemSchema: _input.native.itemSchema } : {}),
|
|
1222
|
+
metadata: { firestoreCollectionPath: collectionPath },
|
|
880
1223
|
},
|
|
881
|
-
...(_input.native.itemSchema != null ? { itemSchema: _input.native.itemSchema } : {}),
|
|
882
1224
|
createdAt: now,
|
|
883
1225
|
updatedAt: now,
|
|
884
1226
|
});
|
|
@@ -902,16 +1244,31 @@ export class Catalox {
|
|
|
902
1244
|
});
|
|
903
1245
|
await this.deps.definitions.upsert({
|
|
904
1246
|
catalogId,
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
1247
|
+
sourceMode: "mapped",
|
|
1248
|
+
catalogItems: {
|
|
1249
|
+
providerType: "external",
|
|
1250
|
+
provider: _input.mapped.sourceType,
|
|
1251
|
+
map: { adapterId, mappingId },
|
|
1252
|
+
metadata: {},
|
|
1253
|
+
},
|
|
1254
|
+
...(_input.mapped.syncMode ? { sync: { mode: _input.mapped.syncMode, syncStatus: "idle" } } : {}),
|
|
912
1255
|
createdAt: now,
|
|
913
1256
|
updatedAt: now,
|
|
914
1257
|
});
|
|
1258
|
+
// Update catalog record to point at the external map references.
|
|
1259
|
+
await this.deps.catalogs.upsert({
|
|
1260
|
+
...(await this.deps.catalogs.get(catalogId)),
|
|
1261
|
+
catalogItems: {
|
|
1262
|
+
providerType: "external",
|
|
1263
|
+
provider: _input.mapped.sourceType,
|
|
1264
|
+
map: { adapterId, mappingId },
|
|
1265
|
+
metadata: {},
|
|
1266
|
+
},
|
|
1267
|
+
metadata: {
|
|
1268
|
+
...(await this.deps.catalogs.get(catalogId)).metadata,
|
|
1269
|
+
lastUpdate: now,
|
|
1270
|
+
},
|
|
1271
|
+
});
|
|
915
1272
|
}
|
|
916
1273
|
// Seed a minimal descriptor if none exists.
|
|
917
1274
|
const existingDescriptor = await this.deps.descriptors.get(catalogId);
|
|
@@ -959,12 +1316,19 @@ export class Catalox {
|
|
|
959
1316
|
if (!existing)
|
|
960
1317
|
throw new CatalogNotFoundError({ catalogId: _catalogId });
|
|
961
1318
|
const now = new Date().toISOString();
|
|
1319
|
+
if (_patch.catalogType != null) {
|
|
1320
|
+
await this.validateCatalogTypeForContext(_context, String(_patch.catalogType));
|
|
1321
|
+
}
|
|
962
1322
|
await this.deps.catalogs.upsert({
|
|
963
1323
|
...existing,
|
|
964
1324
|
...(_patch.name != null ? { name: _patch.name } : {}),
|
|
965
1325
|
...(_patch.description != null ? { description: _patch.description } : {}),
|
|
966
|
-
...(_patch.
|
|
967
|
-
|
|
1326
|
+
...(_patch.catalogType != null ? { catalogType: String(_patch.catalogType) } : {}),
|
|
1327
|
+
metadata: {
|
|
1328
|
+
...existing.metadata,
|
|
1329
|
+
...(_patch.metadata ?? {}),
|
|
1330
|
+
lastUpdate: now,
|
|
1331
|
+
},
|
|
968
1332
|
});
|
|
969
1333
|
return (await this.deps.catalogs.get(_catalogId));
|
|
970
1334
|
}
|
|
@@ -1264,12 +1628,67 @@ export class Catalox {
|
|
|
1264
1628
|
await this.deps.catalogs.upsert({
|
|
1265
1629
|
catalogId: catalog.catalogId,
|
|
1266
1630
|
name: catalog.name,
|
|
1631
|
+
catalogType: "generic",
|
|
1267
1632
|
sourceMode: "native",
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1633
|
+
catalogItems: { providerType: "internal", metadata: {} },
|
|
1634
|
+
metadata: {
|
|
1635
|
+
status: catalog.status ?? "active",
|
|
1636
|
+
createdAt: now,
|
|
1637
|
+
lastUpdate: now,
|
|
1638
|
+
},
|
|
1639
|
+
});
|
|
1640
|
+
}
|
|
1641
|
+
async resolveAllowedCatalogTypes(context) {
|
|
1642
|
+
if (!this.deps.catalogTypes)
|
|
1643
|
+
return null;
|
|
1644
|
+
return this.deps.catalogTypes.resolveForContext({
|
|
1645
|
+
...(context.storeId ? { storeId: context.storeId } : {}),
|
|
1646
|
+
...(context.appId ? { appId: context.appId } : {}),
|
|
1647
|
+
});
|
|
1648
|
+
}
|
|
1649
|
+
async upsertCatalogTypeRegistry(context, scope, input) {
|
|
1650
|
+
if (!context.superAdmin)
|
|
1651
|
+
throw new CatalogAccessDeniedError({ reason: "super_admin_required" });
|
|
1652
|
+
if (!this.deps.catalogTypes)
|
|
1653
|
+
throw new Error("catalogTypes dependency is not configured");
|
|
1654
|
+
await this.deps.catalogTypes.upsert(scope, input);
|
|
1655
|
+
}
|
|
1656
|
+
async resolveAllowedDomains(context) {
|
|
1657
|
+
if (!this.deps.domains)
|
|
1658
|
+
return null;
|
|
1659
|
+
return this.deps.domains.resolveForContext({
|
|
1660
|
+
...(context.storeId ? { storeId: context.storeId } : {}),
|
|
1661
|
+
...(context.appId ? { appId: context.appId } : {}),
|
|
1271
1662
|
});
|
|
1272
1663
|
}
|
|
1664
|
+
async upsertDomainRegistry(context, scope, input) {
|
|
1665
|
+
if (!context.superAdmin)
|
|
1666
|
+
throw new CatalogAccessDeniedError({ reason: "super_admin_required" });
|
|
1667
|
+
if (!this.deps.domains)
|
|
1668
|
+
throw new Error("domains dependency is not configured");
|
|
1669
|
+
await this.deps.domains.upsert(scope, input);
|
|
1670
|
+
}
|
|
1671
|
+
async resolveAllowedAgents(context) {
|
|
1672
|
+
if (!this.deps.agents)
|
|
1673
|
+
return null;
|
|
1674
|
+
return this.deps.agents.resolveForContext({
|
|
1675
|
+
...(context.storeId ? { storeId: context.storeId } : {}),
|
|
1676
|
+
...(context.appId ? { appId: context.appId } : {}),
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1679
|
+
async upsertAgentRegistry(context, scope, input) {
|
|
1680
|
+
if (!context.superAdmin)
|
|
1681
|
+
throw new CatalogAccessDeniedError({ reason: "super_admin_required" });
|
|
1682
|
+
if (!this.deps.agents)
|
|
1683
|
+
throw new Error("agents dependency is not configured");
|
|
1684
|
+
await this.deps.agents.upsert(scope, input);
|
|
1685
|
+
}
|
|
1686
|
+
async backfillCatalogModel(context, input = {}) {
|
|
1687
|
+
if (!context.superAdmin)
|
|
1688
|
+
throw new CatalogAccessDeniedError({ reason: "super_admin_required" });
|
|
1689
|
+
const fs = this.deps.firestoreStore.firestore;
|
|
1690
|
+
return backfillCatalogModel(fs, input);
|
|
1691
|
+
}
|
|
1273
1692
|
async ensureBinding(context, input) {
|
|
1274
1693
|
// only super-admin apps can provision cross-app bindings.
|
|
1275
1694
|
if (!context.superAdmin && (!context.appId || input.appId !== context.appId)) {
|
|
@@ -1314,12 +1733,43 @@ export class Catalox {
|
|
|
1314
1733
|
}
|
|
1315
1734
|
if (!existing)
|
|
1316
1735
|
throw new CatalogNotFoundError({ catalogId: _catalogId, itemId: _itemId });
|
|
1317
|
-
const { data: patchData, indexed: patchIndexed, scope: patchScope } = this.stripReservedWriteFields(_patch);
|
|
1736
|
+
const { data: patchData, indexed: patchIndexed, scope: patchScope, relations: patchRelations } = this.stripReservedWriteFields(_patch);
|
|
1318
1737
|
let nextScope = normalizeStoredScope(scopeFromRecordField(existing.scope));
|
|
1319
1738
|
if (patchScope != null) {
|
|
1320
1739
|
nextScope = normalizeStoredScope(patchScope);
|
|
1321
1740
|
assertSuperAdminForNonGlobalScope(_context.superAdmin, nextScope);
|
|
1322
1741
|
}
|
|
1742
|
+
// Validate relation rules using existing + provided relations (if any).
|
|
1743
|
+
const descriptorRec = await this.deps.descriptors.get(_catalogId);
|
|
1744
|
+
if (descriptorRec?.descriptor?.relationRules?.length) {
|
|
1745
|
+
const existingRefs = await this.deps.references.listByItem(_catalogId, _itemId);
|
|
1746
|
+
const nextRefs = [
|
|
1747
|
+
...existingRefs,
|
|
1748
|
+
...(patchRelations ?? []).map((r) => ({
|
|
1749
|
+
fromCatalogId: _catalogId,
|
|
1750
|
+
fromItemId: _itemId,
|
|
1751
|
+
toCatalogId: r.toCatalogId,
|
|
1752
|
+
toItemId: r.toItemId,
|
|
1753
|
+
relationType: r.relationType,
|
|
1754
|
+
...(r.label != null ? { label: r.label } : {}),
|
|
1755
|
+
...(r.metadata != null ? { metadata: r.metadata } : {}),
|
|
1756
|
+
})),
|
|
1757
|
+
];
|
|
1758
|
+
const report = await this.validateRelationsAgainstDescriptor(_context, {
|
|
1759
|
+
catalogId: _catalogId,
|
|
1760
|
+
itemId: _itemId,
|
|
1761
|
+
descriptor: descriptorRec.descriptor,
|
|
1762
|
+
refs: nextRefs,
|
|
1763
|
+
});
|
|
1764
|
+
if (!report.isValid) {
|
|
1765
|
+
throw new CatalogValidationError({
|
|
1766
|
+
reason: "invalid_relations",
|
|
1767
|
+
catalogId: String(_catalogId),
|
|
1768
|
+
itemId: String(_itemId),
|
|
1769
|
+
report,
|
|
1770
|
+
});
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1323
1773
|
const updatedAt = new Date().toISOString();
|
|
1324
1774
|
const mergedData = { ...(existing.data ?? {}), ...patchData };
|
|
1325
1775
|
const idx = {
|
|
@@ -1334,7 +1784,13 @@ export class Catalox {
|
|
|
1334
1784
|
data: mergedData,
|
|
1335
1785
|
indexed: idx,
|
|
1336
1786
|
...(nextScope.kind !== "global" ? { scope: nextScope } : {}),
|
|
1337
|
-
|
|
1787
|
+
metadata: {
|
|
1788
|
+
...(existing.metadata ?? {}),
|
|
1789
|
+
createdAt: String(existing.metadata?.createdAt ?? updatedAt),
|
|
1790
|
+
lastUpdate: updatedAt,
|
|
1791
|
+
domainIds: Array.isArray(existing.metadata?.domainIds) ? existing.metadata.domainIds : [],
|
|
1792
|
+
agentIds: Array.isArray(existing.metadata?.agentIds) ? existing.metadata.agentIds : [],
|
|
1793
|
+
},
|
|
1338
1794
|
version: (existing.version ?? 0) + 1,
|
|
1339
1795
|
...(this.resolveActorId(_context) ? { updatedBy: this.resolveActorId(_context) } : {}),
|
|
1340
1796
|
};
|
|
@@ -1358,9 +1814,25 @@ export class Catalox {
|
|
|
1358
1814
|
sourceMode: "native",
|
|
1359
1815
|
sourceType: "firebase",
|
|
1360
1816
|
data: mergedData,
|
|
1361
|
-
|
|
1362
|
-
|
|
1817
|
+
metadata: {
|
|
1818
|
+
createdAt: String(existing.metadata?.createdAt ?? updatedAt),
|
|
1819
|
+
lastUpdate: updatedAt,
|
|
1820
|
+
domainIds: Array.isArray(existing.metadata?.domainIds) ? existing.metadata.domainIds : [],
|
|
1821
|
+
agentIds: Array.isArray(existing.metadata?.agentIds) ? existing.metadata.agentIds : [],
|
|
1822
|
+
},
|
|
1363
1823
|
};
|
|
1824
|
+
// Upsert any provided relations after the item is persisted.
|
|
1825
|
+
for (const r of patchRelations ?? []) {
|
|
1826
|
+
await this.upsertCatalogItemRelation(_context, {
|
|
1827
|
+
fromCatalogId: String(_catalogId),
|
|
1828
|
+
fromItemId: String(_itemId),
|
|
1829
|
+
toCatalogId: r.toCatalogId,
|
|
1830
|
+
toItemId: r.toItemId,
|
|
1831
|
+
relationType: r.relationType,
|
|
1832
|
+
...(r.label != null ? { label: r.label } : {}),
|
|
1833
|
+
...(r.metadata != null ? { metadata: r.metadata } : {}),
|
|
1834
|
+
});
|
|
1835
|
+
}
|
|
1364
1836
|
return this.decorateItem(_catalogId, out);
|
|
1365
1837
|
}
|
|
1366
1838
|
async deleteNativeCatalogItem(_context, _catalogId, _itemId, _options) {
|
|
@@ -1431,7 +1903,7 @@ export class Catalox {
|
|
|
1431
1903
|
const descriptor = await this.deps.descriptors.get(catalogId);
|
|
1432
1904
|
if (!descriptor)
|
|
1433
1905
|
throw new CatalogAdapterError({ catalogId, reason: "missing_descriptor" });
|
|
1434
|
-
const { data, indexed: callerIndexed, scope: inputScope } = this.stripReservedWriteFields(input);
|
|
1906
|
+
const { data, indexed: callerIndexed, scope: inputScope, relations: inputRelations } = this.stripReservedWriteFields(input);
|
|
1435
1907
|
const storedScope = normalizeStoredScope(inputScope ?? { kind: "global" });
|
|
1436
1908
|
assertSuperAdminForNonGlobalScope(context.superAdmin, storedScope);
|
|
1437
1909
|
const derived = this.deriveIndexed(descriptor.descriptor, data);
|
|
@@ -1442,6 +1914,36 @@ export class Catalox {
|
|
|
1442
1914
|
const now = new Date().toISOString();
|
|
1443
1915
|
const existing = await this.deps.nativeItems.get(catalogId, storageDocId);
|
|
1444
1916
|
const actorId = this.resolveActorId(context);
|
|
1917
|
+
// Enforce relation rules on create/upsert when relationRules are declared.
|
|
1918
|
+
if (descriptor.descriptor?.relationRules?.length) {
|
|
1919
|
+
const existingRefs = await this.deps.references.listByItem(catalogId, logicalItemId);
|
|
1920
|
+
const nextRefs = [
|
|
1921
|
+
...existingRefs,
|
|
1922
|
+
...(inputRelations ?? []).map((r) => ({
|
|
1923
|
+
fromCatalogId: catalogId,
|
|
1924
|
+
fromItemId: logicalItemId,
|
|
1925
|
+
toCatalogId: r.toCatalogId,
|
|
1926
|
+
toItemId: r.toItemId,
|
|
1927
|
+
relationType: r.relationType,
|
|
1928
|
+
...(r.label != null ? { label: r.label } : {}),
|
|
1929
|
+
...(r.metadata != null ? { metadata: r.metadata } : {}),
|
|
1930
|
+
})),
|
|
1931
|
+
];
|
|
1932
|
+
const report = await this.validateRelationsAgainstDescriptor(context, {
|
|
1933
|
+
catalogId,
|
|
1934
|
+
itemId: logicalItemId,
|
|
1935
|
+
descriptor: descriptor.descriptor,
|
|
1936
|
+
refs: nextRefs,
|
|
1937
|
+
});
|
|
1938
|
+
if (!report.isValid) {
|
|
1939
|
+
throw new CatalogValidationError({
|
|
1940
|
+
reason: "invalid_relations",
|
|
1941
|
+
catalogId: String(catalogId),
|
|
1942
|
+
itemId: String(logicalItemId),
|
|
1943
|
+
report,
|
|
1944
|
+
});
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1445
1947
|
await this.deps.nativeItems.upsert(catalogId, {
|
|
1446
1948
|
itemId: logicalItemId,
|
|
1447
1949
|
catalogId,
|
|
@@ -1449,9 +1951,13 @@ export class Catalox {
|
|
|
1449
1951
|
appScopedOwnerId: appId,
|
|
1450
1952
|
...(indexed != null ? { indexed } : {}),
|
|
1451
1953
|
data,
|
|
1954
|
+
metadata: {
|
|
1955
|
+
createdAt: String(existing?.metadata?.createdAt ?? now),
|
|
1956
|
+
lastUpdate: now,
|
|
1957
|
+
domainIds: Array.isArray(existing?.metadata?.domainIds) ? (existing?.metadata).domainIds : [],
|
|
1958
|
+
agentIds: Array.isArray(existing?.metadata?.agentIds) ? (existing?.metadata).agentIds : [],
|
|
1959
|
+
},
|
|
1452
1960
|
version: (existing?.version ?? 0) + 1,
|
|
1453
|
-
...(existing?.createdAt ? { createdAt: existing.createdAt } : { createdAt: now }),
|
|
1454
|
-
updatedAt: now,
|
|
1455
1961
|
...(existing?.createdBy ? { createdBy: existing.createdBy } : actorId ? { createdBy: actorId } : {}),
|
|
1456
1962
|
...(actorId ? { updatedBy: actorId } : {}),
|
|
1457
1963
|
});
|
|
@@ -1464,6 +1970,18 @@ export class Catalox {
|
|
|
1464
1970
|
before: existing ?? null,
|
|
1465
1971
|
after: persisted,
|
|
1466
1972
|
});
|
|
1973
|
+
// Upsert any provided relations after the item is persisted.
|
|
1974
|
+
for (const r of inputRelations ?? []) {
|
|
1975
|
+
await this.upsertCatalogItemRelation(context, {
|
|
1976
|
+
fromCatalogId: String(catalogId),
|
|
1977
|
+
fromItemId: String(logicalItemId),
|
|
1978
|
+
toCatalogId: r.toCatalogId,
|
|
1979
|
+
toItemId: r.toItemId,
|
|
1980
|
+
relationType: r.relationType,
|
|
1981
|
+
...(r.label != null ? { label: r.label } : {}),
|
|
1982
|
+
...(r.metadata != null ? { metadata: r.metadata } : {}),
|
|
1983
|
+
});
|
|
1984
|
+
}
|
|
1467
1985
|
return {
|
|
1468
1986
|
itemId: logicalItemId,
|
|
1469
1987
|
catalogId,
|
|
@@ -1471,8 +1989,12 @@ export class Catalox {
|
|
|
1471
1989
|
sourceMode: "native",
|
|
1472
1990
|
sourceType: "firebase",
|
|
1473
1991
|
data,
|
|
1474
|
-
|
|
1475
|
-
|
|
1992
|
+
metadata: {
|
|
1993
|
+
createdAt: String(existing?.metadata?.createdAt ?? now),
|
|
1994
|
+
lastUpdate: now,
|
|
1995
|
+
domainIds: Array.isArray(existing?.metadata?.domainIds) ? (existing?.metadata).domainIds : [],
|
|
1996
|
+
agentIds: Array.isArray(existing?.metadata?.agentIds) ? (existing?.metadata).agentIds : [],
|
|
1997
|
+
},
|
|
1476
1998
|
};
|
|
1477
1999
|
}
|
|
1478
2000
|
async batchUpsertNativeCatalogItems(context, catalogId, items) {
|
|
@@ -1501,8 +2023,7 @@ export class Catalox {
|
|
|
1501
2023
|
appScopedOwnerId: appId,
|
|
1502
2024
|
...(indexed != null ? { indexed } : {}),
|
|
1503
2025
|
data: stripped.data,
|
|
1504
|
-
createdAt: now,
|
|
1505
|
-
updatedAt: now,
|
|
2026
|
+
metadata: { createdAt: now, lastUpdate: now, domainIds: [], agentIds: [] },
|
|
1506
2027
|
...(actorId ? { updatedBy: actorId } : {}),
|
|
1507
2028
|
};
|
|
1508
2029
|
});
|
|
@@ -1622,13 +2143,13 @@ export class Catalox {
|
|
|
1622
2143
|
itemId: it.itemId,
|
|
1623
2144
|
...(it.title ? { title: it.title } : {}),
|
|
1624
2145
|
...(it.subtitle ? { subtitle: it.subtitle } : {}),
|
|
1625
|
-
...(it.
|
|
2146
|
+
...(it.metadata?.lastUpdate ? { updatedAt: String(it.metadata.lastUpdate) } : {}),
|
|
1626
2147
|
}));
|
|
1627
2148
|
catalogStats.push({
|
|
1628
2149
|
catalogId,
|
|
1629
2150
|
label: c.label,
|
|
1630
2151
|
sourceMode: c.sourceMode,
|
|
1631
|
-
status: c.status,
|
|
2152
|
+
status: c.metadata.status,
|
|
1632
2153
|
...(list.issues ? { mappingIssueCount: list.issues.length } : {}),
|
|
1633
2154
|
topExamples: examples,
|
|
1634
2155
|
});
|
|
@@ -1705,8 +2226,9 @@ export class Catalox {
|
|
|
1705
2226
|
if (!catalog)
|
|
1706
2227
|
throw new CatalogNotFoundError({ catalogId: _catalogId });
|
|
1707
2228
|
const def = await this.deps.definitions.get(_catalogId);
|
|
1708
|
-
if (!def || def.
|
|
2229
|
+
if (!def || def.catalogItems.providerType !== "external") {
|
|
1709
2230
|
throw new CatalogAdapterError({ catalogId: _catalogId, reason: "not_mapped" });
|
|
2231
|
+
}
|
|
1710
2232
|
const mode = def.sync?.mode;
|
|
1711
2233
|
if (mode !== "snapshot")
|
|
1712
2234
|
return { syncStatus: "idle" };
|
|
@@ -1726,7 +2248,7 @@ export class Catalox {
|
|
|
1726
2248
|
itemId: item.itemId,
|
|
1727
2249
|
catalogId: _catalogId,
|
|
1728
2250
|
sourceFingerprint: this.fingerprint(item.data),
|
|
1729
|
-
...(item.
|
|
2251
|
+
...(item.metadata?.lastUpdate ? { sourceUpdatedAt: String(item.metadata.lastUpdate) } : {}),
|
|
1730
2252
|
data: item.data,
|
|
1731
2253
|
sync: { lastSyncedAt: now, syncStatus: "success" },
|
|
1732
2254
|
...(actorId ? { metadata: { syncedBy: actorId } } : {}),
|
|
@@ -1920,8 +2442,7 @@ export class Catalox {
|
|
|
1920
2442
|
sourceMode: "native",
|
|
1921
2443
|
sourceType: "firebase",
|
|
1922
2444
|
data: liveAfter.data,
|
|
1923
|
-
|
|
1924
|
-
updatedAt: liveAfter.updatedAt,
|
|
2445
|
+
metadata: liveAfter.metadata ?? {},
|
|
1925
2446
|
});
|
|
1926
2447
|
}
|
|
1927
2448
|
async replayCatalogToPointInTime(context, catalogId, input) {
|