@x12i/catalox 3.6.0 → 3.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -9
- package/dist/src/catalox/catalox-bound.d.ts +6 -1
- package/dist/src/catalox/catalox-bound.d.ts.map +1 -1
- package/dist/src/catalox/catalox-bound.js +9 -0
- package/dist/src/catalox/catalox-bound.js.map +1 -1
- package/dist/src/catalox/catalox.d.ts +11 -2
- package/dist/src/catalox/catalox.d.ts.map +1 -1
- package/dist/src/catalox/catalox.js +278 -17
- package/dist/src/catalox/catalox.js.map +1 -1
- package/dist/src/contracts/descriptors.d.ts +6 -0
- package/dist/src/contracts/descriptors.d.ts.map +1 -1
- package/dist/src/contracts/index.d.ts +4 -4
- package/dist/src/contracts/index.d.ts.map +1 -1
- package/dist/src/contracts/index.js.map +1 -1
- package/dist/src/contracts/inputs.d.ts +18 -0
- package/dist/src/contracts/inputs.d.ts.map +1 -1
- package/dist/src/contracts/presentation.d.ts +35 -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/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/validation/ui-spec-schema.d.ts.map +1 -1
- package/dist/src/validation/ui-spec-schema.js +25 -0
- package/dist/src/validation/ui-spec-schema.js.map +1 -1
- package/package.json +1 -1
|
@@ -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";
|
|
@@ -52,6 +52,124 @@ export class Catalox {
|
|
|
52
52
|
constructor(deps) {
|
|
53
53
|
this.deps = deps;
|
|
54
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
|
+
}
|
|
55
173
|
async validateCatalogTypeForContext(context, catalogType) {
|
|
56
174
|
const registry = this.deps.catalogTypes
|
|
57
175
|
? await this.deps.catalogTypes.resolveForContext({
|
|
@@ -431,6 +549,15 @@ export class Catalox {
|
|
|
431
549
|
const refs = options?.includeReferences
|
|
432
550
|
? await this.getCatalogItemReferences(context, catalogId, itemId)
|
|
433
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
|
+
: [];
|
|
434
561
|
return {
|
|
435
562
|
catalogId: String(catalogId),
|
|
436
563
|
itemId: String(itemId),
|
|
@@ -444,17 +571,7 @@ export class Catalox {
|
|
|
444
571
|
...(item.metadata?.lastUpdate != null ? { updatedAt: String(item.metadata.lastUpdate) } : {}),
|
|
445
572
|
},
|
|
446
573
|
resolvedSources,
|
|
447
|
-
...(
|
|
448
|
-
? {
|
|
449
|
-
references: refs.map((r) => ({
|
|
450
|
-
toCatalogId: String(r.toCatalogId),
|
|
451
|
-
toItemId: String(r.toItemId),
|
|
452
|
-
relationType: String(r.relationType),
|
|
453
|
-
...(r.label != null ? { label: r.label } : {}),
|
|
454
|
-
...(r.metadata != null ? { metadata: r.metadata } : {}),
|
|
455
|
-
})),
|
|
456
|
-
}
|
|
457
|
-
: {}),
|
|
574
|
+
...(referenceViews.length ? { relations: referenceViews, references: referenceViews } : {}),
|
|
458
575
|
...(descriptor?.presentationSpec ? { presentation: descriptor.presentationSpec } : {}),
|
|
459
576
|
capabilities: descriptor?.capabilities ?? {
|
|
460
577
|
canList: true,
|
|
@@ -479,12 +596,14 @@ export class Catalox {
|
|
|
479
596
|
return Object.keys(out).length ? out : undefined;
|
|
480
597
|
}
|
|
481
598
|
stripReservedWriteFields(input) {
|
|
482
|
-
const { indexed, scope, ...rest } = input;
|
|
599
|
+
const { indexed, scope, relations, references, ...rest } = input;
|
|
483
600
|
const parsedScope = scope != null ? parseWriteScopeInput(scope) : undefined;
|
|
601
|
+
const parsedRelations = this.normalizeRelationWrites(relations ?? references);
|
|
484
602
|
return {
|
|
485
603
|
data: rest,
|
|
486
604
|
...(indexed != null ? { indexed: indexed } : {}),
|
|
487
605
|
...(parsedScope != null ? { scope: parsedScope } : {}),
|
|
606
|
+
...(parsedRelations.length ? { relations: parsedRelations } : {}),
|
|
488
607
|
};
|
|
489
608
|
}
|
|
490
609
|
normalizeListFetchScope(scope) {
|
|
@@ -978,8 +1097,12 @@ export class Catalox {
|
|
|
978
1097
|
async validateCatalog(_context, _catalogId) {
|
|
979
1098
|
return { isValid: true, issues: [] };
|
|
980
1099
|
}
|
|
981
|
-
async validateCatalogItem(
|
|
982
|
-
|
|
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 });
|
|
983
1106
|
}
|
|
984
1107
|
async getCatalogItemReferences(context, catalogId, itemId) {
|
|
985
1108
|
const appId = await this.resolveAppIdForCatalogAccess({ context, catalogId, required: "read" });
|
|
@@ -993,6 +1116,59 @@ export class Catalox {
|
|
|
993
1116
|
const refs = await this.deps.references.listByCatalog(catalogId);
|
|
994
1117
|
return refs;
|
|
995
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
|
+
}
|
|
996
1172
|
// Spec methods below are stubs for now; filled in by later todos.
|
|
997
1173
|
async getApp(_context, appId) {
|
|
998
1174
|
const resolved = appId ?? _context.appId;
|
|
@@ -1557,12 +1733,43 @@ export class Catalox {
|
|
|
1557
1733
|
}
|
|
1558
1734
|
if (!existing)
|
|
1559
1735
|
throw new CatalogNotFoundError({ catalogId: _catalogId, itemId: _itemId });
|
|
1560
|
-
const { data: patchData, indexed: patchIndexed, scope: patchScope } = this.stripReservedWriteFields(_patch);
|
|
1736
|
+
const { data: patchData, indexed: patchIndexed, scope: patchScope, relations: patchRelations } = this.stripReservedWriteFields(_patch);
|
|
1561
1737
|
let nextScope = normalizeStoredScope(scopeFromRecordField(existing.scope));
|
|
1562
1738
|
if (patchScope != null) {
|
|
1563
1739
|
nextScope = normalizeStoredScope(patchScope);
|
|
1564
1740
|
assertSuperAdminForNonGlobalScope(_context.superAdmin, nextScope);
|
|
1565
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
|
+
}
|
|
1566
1773
|
const updatedAt = new Date().toISOString();
|
|
1567
1774
|
const mergedData = { ...(existing.data ?? {}), ...patchData };
|
|
1568
1775
|
const idx = {
|
|
@@ -1614,6 +1821,18 @@ export class Catalox {
|
|
|
1614
1821
|
agentIds: Array.isArray(existing.metadata?.agentIds) ? existing.metadata.agentIds : [],
|
|
1615
1822
|
},
|
|
1616
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
|
+
}
|
|
1617
1836
|
return this.decorateItem(_catalogId, out);
|
|
1618
1837
|
}
|
|
1619
1838
|
async deleteNativeCatalogItem(_context, _catalogId, _itemId, _options) {
|
|
@@ -1684,7 +1903,7 @@ export class Catalox {
|
|
|
1684
1903
|
const descriptor = await this.deps.descriptors.get(catalogId);
|
|
1685
1904
|
if (!descriptor)
|
|
1686
1905
|
throw new CatalogAdapterError({ catalogId, reason: "missing_descriptor" });
|
|
1687
|
-
const { data, indexed: callerIndexed, scope: inputScope } = this.stripReservedWriteFields(input);
|
|
1906
|
+
const { data, indexed: callerIndexed, scope: inputScope, relations: inputRelations } = this.stripReservedWriteFields(input);
|
|
1688
1907
|
const storedScope = normalizeStoredScope(inputScope ?? { kind: "global" });
|
|
1689
1908
|
assertSuperAdminForNonGlobalScope(context.superAdmin, storedScope);
|
|
1690
1909
|
const derived = this.deriveIndexed(descriptor.descriptor, data);
|
|
@@ -1695,6 +1914,36 @@ export class Catalox {
|
|
|
1695
1914
|
const now = new Date().toISOString();
|
|
1696
1915
|
const existing = await this.deps.nativeItems.get(catalogId, storageDocId);
|
|
1697
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
|
+
}
|
|
1698
1947
|
await this.deps.nativeItems.upsert(catalogId, {
|
|
1699
1948
|
itemId: logicalItemId,
|
|
1700
1949
|
catalogId,
|
|
@@ -1721,6 +1970,18 @@ export class Catalox {
|
|
|
1721
1970
|
before: existing ?? null,
|
|
1722
1971
|
after: persisted,
|
|
1723
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
|
+
}
|
|
1724
1985
|
return {
|
|
1725
1986
|
itemId: logicalItemId,
|
|
1726
1987
|
catalogId,
|