@x12i/catalox 4.1.2 → 4.2.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 +7 -0
- package/dist/src/catalox/catalog-snapshot-export.d.ts +46 -0
- package/dist/src/catalox/catalog-snapshot-export.d.ts.map +1 -0
- package/dist/src/catalox/catalog-snapshot-export.js +103 -0
- package/dist/src/catalox/catalog-snapshot-export.js.map +1 -0
- package/dist/src/catalox/catalox-bound.d.ts +3 -1
- package/dist/src/catalox/catalox-bound.d.ts.map +1 -1
- package/dist/src/catalox/catalox-bound.js +6 -0
- package/dist/src/catalox/catalox-bound.js.map +1 -1
- package/dist/src/catalox/catalox.d.ts +23 -6
- package/dist/src/catalox/catalox.d.ts.map +1 -1
- package/dist/src/catalox/catalox.js +232 -5
- package/dist/src/catalox/catalox.js.map +1 -1
- package/dist/src/catalox/create-catalox.d.ts.map +1 -1
- package/dist/src/catalox/create-catalox.js +3 -0
- package/dist/src/catalox/create-catalox.js.map +1 -1
- package/dist/src/catalox/seed-preset.d.ts +15 -0
- package/dist/src/catalox/seed-preset.d.ts.map +1 -1
- package/dist/src/catalox/seed-preset.js +35 -0
- package/dist/src/catalox/seed-preset.js.map +1 -1
- package/dist/src/cli/index.js +116 -6
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/contracts/catalog-snapshot-export.d.ts +55 -0
- package/dist/src/contracts/catalog-snapshot-export.d.ts.map +1 -0
- package/dist/src/contracts/catalog-snapshot-export.js +3 -0
- package/dist/src/contracts/catalog-snapshot-export.js.map +1 -0
- package/dist/src/contracts/catalog-types.d.ts +13 -1
- package/dist/src/contracts/catalog-types.d.ts.map +1 -1
- package/dist/src/contracts/index.d.ts +2 -0
- 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 +2 -0
- package/dist/src/contracts/inputs.d.ts.map +1 -1
- package/dist/src/contracts/validation.d.ts +2 -1
- package/dist/src/contracts/validation.d.ts.map +1 -1
- package/dist/src/embedder.d.ts +3 -1
- package/dist/src/embedder.d.ts.map +1 -1
- package/dist/src/embedder.js +3 -1
- package/dist/src/embedder.js.map +1 -1
- package/dist/src/validation/index.d.ts +2 -0
- package/dist/src/validation/index.d.ts.map +1 -1
- package/dist/src/validation/index.js +2 -0
- package/dist/src/validation/index.js.map +1 -1
- package/dist/src/validation/payload-schema-registry.d.ts +20 -0
- package/dist/src/validation/payload-schema-registry.d.ts.map +1 -0
- package/dist/src/validation/payload-schema-registry.js +76 -0
- package/dist/src/validation/payload-schema-registry.js.map +1 -0
- package/dist/src/validation/payload-schema.d.ts +30 -0
- package/dist/src/validation/payload-schema.d.ts.map +1 -0
- package/dist/src/validation/payload-schema.js +160 -0
- package/dist/src/validation/payload-schema.js.map +1 -0
- package/docs/cli-items.md +94 -93
- package/docs/cli-snapshot-export.md +65 -0
- package/package.json +3 -1
- package/presets/native-map-v1.json +14 -2
- package/schemas/native-map-item/v1.json +10 -0
- package/schemas/registry.json +3 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { compactCatalogFilter } from "../contracts/catalogs.js";
|
|
2
2
|
import { CatalogAccessDeniedError, CatalogAdapterError, CatalogBindingError, CatalogNotFoundError, CatalogValidationError, } from "../contracts/errors.js";
|
|
3
3
|
import { validateMappingSpec } from "../mapping/validate-mapping.js";
|
|
4
|
+
import { resolvePayloadSchema, validateNativeItemPayload, } from "../validation/payload-schema.js";
|
|
5
|
+
import { createBundledPayloadSchemaRegistry } from "../validation/payload-schema-registry.js";
|
|
4
6
|
import { executeMapping } from "../mapping/execute-mapping.js";
|
|
5
7
|
import { ApiCatalogAdapter } from "../adapters/api/api-adapter.js";
|
|
6
8
|
import { MongoCatalogAdapter } from "../adapters/mongo/mongo-adapter.js";
|
|
@@ -9,6 +11,7 @@ import { resolveCatalogItemId } from "./identity.js";
|
|
|
9
11
|
import { assertSuperAdminForNonGlobalScope, encodeNativeItemStorageDocId, indexedScopeFields, isGlobalPhysicalRow, normalizeStoredScope, parseWriteScopeInput, scopeFromRecordField, } from "./native-scope.js";
|
|
10
12
|
import { filterPhysicalForTenantFetch, mergeNativePhysicalRows, pickWinningPhysicalRow, } from "./native-catalog-merge.js";
|
|
11
13
|
import { parseJson, toJson } from "./json-io.js";
|
|
14
|
+
import { buildCataloxExportV1, filterCatalogEntries, listAllCatalogItemsForExport, stripItemData, } from "./catalog-snapshot-export.js";
|
|
12
15
|
import { createHash, randomUUID } from "node:crypto";
|
|
13
16
|
import { renderInventoryReportMarkdown } from "./reporting/render-inventory-report.js";
|
|
14
17
|
import { optionsFromCatalogItems, optionsFromStaticSource, resolveFilterBy } from "./field-source-resolution.js";
|
|
@@ -48,8 +51,51 @@ import { emitRecordHistoryEvent, readRecordHistoryEventPayload, } from "./record
|
|
|
48
51
|
import { runDeleteCatalog, runRenameCatalog, runRestoreDeletedCatalog, } from "./catalog-lifecycle.js";
|
|
49
52
|
export class Catalox {
|
|
50
53
|
deps;
|
|
54
|
+
payloadSchemaRegistry;
|
|
51
55
|
constructor(deps) {
|
|
52
56
|
this.deps = deps;
|
|
57
|
+
this.payloadSchemaRegistry = deps.payloadSchemas ?? createBundledPayloadSchemaRegistry();
|
|
58
|
+
}
|
|
59
|
+
getPayloadSchemaRegistry() {
|
|
60
|
+
return this.payloadSchemaRegistry;
|
|
61
|
+
}
|
|
62
|
+
async resolveCatalogTypeRegistry(context) {
|
|
63
|
+
if (!this.deps.catalogTypes)
|
|
64
|
+
return null;
|
|
65
|
+
return this.deps.catalogTypes.resolveForContext({
|
|
66
|
+
...(context.storeId ? { storeId: context.storeId } : {}),
|
|
67
|
+
...(context.appId ? { appId: context.appId } : {}),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
resolvePayloadSchemaForCatalog(catalog, catalogTypeRegistry) {
|
|
71
|
+
return resolvePayloadSchema({
|
|
72
|
+
catalog,
|
|
73
|
+
catalogTypeRegistry: catalogTypeRegistry ?? null,
|
|
74
|
+
registry: this.getPayloadSchemaRegistry(),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
validateNativeItemData(data, catalog, catalogTypeRegistry) {
|
|
78
|
+
const resolved = this.resolvePayloadSchemaForCatalog(catalog, catalogTypeRegistry);
|
|
79
|
+
if (!resolved)
|
|
80
|
+
return { isValid: true, issues: [] };
|
|
81
|
+
return validateNativeItemPayload(data, resolved, this.getPayloadSchemaRegistry());
|
|
82
|
+
}
|
|
83
|
+
async assertNativeItemPayloadValid(context, catalogId, data, logicalItemId) {
|
|
84
|
+
if (!this.deps.catalogs)
|
|
85
|
+
return;
|
|
86
|
+
const catalog = await this.deps.catalogs.get(catalogId);
|
|
87
|
+
if (!catalog)
|
|
88
|
+
return;
|
|
89
|
+
const typeRegistry = await this.resolveCatalogTypeRegistry(context);
|
|
90
|
+
const report = this.validateNativeItemData(data, catalog, typeRegistry);
|
|
91
|
+
if (report.isValid)
|
|
92
|
+
return;
|
|
93
|
+
throw new CatalogValidationError({
|
|
94
|
+
reason: "invalid_payload",
|
|
95
|
+
catalogId: String(catalogId),
|
|
96
|
+
...(logicalItemId != null ? { itemId: logicalItemId } : {}),
|
|
97
|
+
report,
|
|
98
|
+
});
|
|
53
99
|
}
|
|
54
100
|
encodeRefSegment(value) {
|
|
55
101
|
// Keep IDs stable and URL-safe for use as Firestore doc ids.
|
|
@@ -1111,15 +1157,126 @@ export class Catalox {
|
|
|
1111
1157
|
throw e;
|
|
1112
1158
|
}
|
|
1113
1159
|
}
|
|
1114
|
-
async validateCatalog(
|
|
1115
|
-
|
|
1160
|
+
async validateCatalog(context, catalogId) {
|
|
1161
|
+
const issues = [];
|
|
1162
|
+
const catalog = await this.deps.catalogs.get(catalogId);
|
|
1163
|
+
if (!catalog) {
|
|
1164
|
+
issues.push({
|
|
1165
|
+
code: "catalog.not_found",
|
|
1166
|
+
severity: "error",
|
|
1167
|
+
message: `Catalog "${String(catalogId)}" was not found.`,
|
|
1168
|
+
});
|
|
1169
|
+
return { isValid: false, issues };
|
|
1170
|
+
}
|
|
1171
|
+
const descriptor = await this.deps.descriptors.get(catalogId);
|
|
1172
|
+
if (!descriptor) {
|
|
1173
|
+
issues.push({
|
|
1174
|
+
code: "catalog.missing_descriptor",
|
|
1175
|
+
severity: "error",
|
|
1176
|
+
message: `Descriptor missing for catalog "${String(catalogId)}".`,
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
const definition = await this.deps.definitions.get(catalogId);
|
|
1180
|
+
if (catalog.sourceMode === "native") {
|
|
1181
|
+
if (!definition || definition.sourceMode !== "native") {
|
|
1182
|
+
issues.push({
|
|
1183
|
+
code: "catalog.missing_native_config",
|
|
1184
|
+
severity: "error",
|
|
1185
|
+
message: `Native definition missing for catalog "${String(catalogId)}".`,
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
else if (catalog.sourceMode === "mapped") {
|
|
1190
|
+
if (!definition || definition.sourceMode !== "mapped") {
|
|
1191
|
+
issues.push({
|
|
1192
|
+
code: "catalog.missing_mapped_config",
|
|
1193
|
+
severity: "error",
|
|
1194
|
+
message: `Mapped definition missing for catalog "${String(catalogId)}".`,
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
else {
|
|
1198
|
+
const map = definition.catalogItems?.map;
|
|
1199
|
+
const mappingId = map?.mappingId != null ? String(map.mappingId) : "";
|
|
1200
|
+
if (!mappingId) {
|
|
1201
|
+
issues.push({
|
|
1202
|
+
code: "catalog.mapping_invalid",
|
|
1203
|
+
severity: "error",
|
|
1204
|
+
message: `Mapped catalog "${String(catalogId)}" has no mappingId.`,
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
else {
|
|
1208
|
+
const mapping = await this.deps.mappings.get(mappingId);
|
|
1209
|
+
if (!mapping) {
|
|
1210
|
+
issues.push({
|
|
1211
|
+
code: "catalog.mapping_invalid",
|
|
1212
|
+
severity: "error",
|
|
1213
|
+
message: `Mapping "${mappingId}" not found for catalog "${String(catalogId)}".`,
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
else {
|
|
1217
|
+
const mappingIssues = validateMappingSpec(mapping.mapping);
|
|
1218
|
+
for (const mi of mappingIssues) {
|
|
1219
|
+
issues.push({
|
|
1220
|
+
code: "catalog.mapping_invalid",
|
|
1221
|
+
severity: "error",
|
|
1222
|
+
message: mi.message ?? String(mi.code ?? "mapping_invalid"),
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
if (this.deps.catalogTypes) {
|
|
1230
|
+
const registry = await this.resolveCatalogTypeRegistry(context);
|
|
1231
|
+
if (registry?.types?.length) {
|
|
1232
|
+
const allowed = new Set(registry.types.map((t) => String(t)));
|
|
1233
|
+
if (!allowed.has(String(catalog.catalogType))) {
|
|
1234
|
+
issues.push({
|
|
1235
|
+
code: "catalog.type_not_allowed",
|
|
1236
|
+
severity: "error",
|
|
1237
|
+
message: `catalogType "${String(catalog.catalogType)}" is not in the scoped registry.`,
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
return { isValid: !issues.some((i) => i.severity === "error"), issues };
|
|
1116
1243
|
}
|
|
1117
1244
|
async validateCatalogItem(context, catalogId, itemId) {
|
|
1245
|
+
const issues = [];
|
|
1118
1246
|
const descriptor = await this.getCatalogDescriptor(context, catalogId);
|
|
1119
|
-
if (!descriptor)
|
|
1120
|
-
return {
|
|
1247
|
+
if (!descriptor) {
|
|
1248
|
+
return {
|
|
1249
|
+
isValid: false,
|
|
1250
|
+
issues: [
|
|
1251
|
+
{
|
|
1252
|
+
code: "catalog.missing_descriptor",
|
|
1253
|
+
severity: "error",
|
|
1254
|
+
message: `Descriptor missing for catalog "${String(catalogId)}".`,
|
|
1255
|
+
},
|
|
1256
|
+
],
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1121
1259
|
const refs = await this.getCatalogItemReferences(context, catalogId, itemId);
|
|
1122
|
-
|
|
1260
|
+
const relationReport = await this.validateRelationsAgainstDescriptor(context, {
|
|
1261
|
+
catalogId,
|
|
1262
|
+
itemId,
|
|
1263
|
+
descriptor,
|
|
1264
|
+
refs,
|
|
1265
|
+
});
|
|
1266
|
+
issues.push(...relationReport.issues);
|
|
1267
|
+
const catalog = await this.deps.catalogs.get(catalogId);
|
|
1268
|
+
if (catalog?.sourceMode === "native") {
|
|
1269
|
+
const got = await this.getCatalogItem(context, catalogId, itemId);
|
|
1270
|
+
if (got.outcome === "found" && got.item?.data != null) {
|
|
1271
|
+
const typeRegistry = await this.resolveCatalogTypeRegistry(context);
|
|
1272
|
+
const payloadReport = this.validateNativeItemData(got.item.data, catalog, typeRegistry);
|
|
1273
|
+
issues.push(...payloadReport.issues);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
return {
|
|
1277
|
+
isValid: !issues.some((i) => i.severity === "error"),
|
|
1278
|
+
issues,
|
|
1279
|
+
};
|
|
1123
1280
|
}
|
|
1124
1281
|
async getCatalogItemReferences(context, catalogId, itemId) {
|
|
1125
1282
|
const appId = await this.resolveAppIdForCatalogAccess({ context, catalogId, required: "read" });
|
|
@@ -1202,6 +1359,7 @@ export class Catalox {
|
|
|
1202
1359
|
name: _input.name,
|
|
1203
1360
|
...(_input.description != null ? { description: _input.description } : {}),
|
|
1204
1361
|
catalogType: _input.catalogType,
|
|
1362
|
+
...(_input.schemaVersion != null ? { schemaVersion: String(_input.schemaVersion) } : {}),
|
|
1205
1363
|
sourceMode: _input.sourceMode,
|
|
1206
1364
|
...(_input.sourceMode === "mapped" && _input.mapped?.sourceType
|
|
1207
1365
|
? { mappedSourceType: _input.mapped.sourceType }
|
|
@@ -1762,6 +1920,8 @@ export class Catalox {
|
|
|
1762
1920
|
const { data, indexed: callerIndexed, scope: inputScope, relations: inputRelations } = this.stripReservedWriteFields(input);
|
|
1763
1921
|
const storedScope = normalizeStoredScope(inputScope ?? { kind: "global" });
|
|
1764
1922
|
assertSuperAdminForNonGlobalScope(context.superAdmin, storedScope);
|
|
1923
|
+
const logicalItemIdEarly = resolveCatalogItemId({ identity: descriptor.descriptor.identity, data });
|
|
1924
|
+
await this.assertNativeItemPayloadValid(context, catalogId, data, String(logicalItemIdEarly));
|
|
1765
1925
|
const derived = this.deriveIndexed(descriptor.descriptor, data);
|
|
1766
1926
|
const scopeIdx = indexedScopeFields(storedScope);
|
|
1767
1927
|
const indexed = derived != null ? { ...derived, ...scopeIdx } : (Object.keys(scopeIdx).length ? scopeIdx : undefined);
|
|
@@ -1861,10 +2021,24 @@ export class Catalox {
|
|
|
1861
2021
|
throw new CatalogAdapterError({ catalogId, reason: "missing_descriptor" });
|
|
1862
2022
|
const now = new Date().toISOString();
|
|
1863
2023
|
const actorId = this.resolveActorId(context);
|
|
2024
|
+
const catalog = await this.deps.catalogs.get(catalogId);
|
|
2025
|
+
const typeRegistry = catalog ? await this.resolveCatalogTypeRegistry(context) : null;
|
|
1864
2026
|
const records = items.map((row) => {
|
|
1865
2027
|
const stripped = this.stripReservedWriteFields(row);
|
|
1866
2028
|
const storedScope = normalizeStoredScope(stripped.scope ?? { kind: "global" });
|
|
1867
2029
|
assertSuperAdminForNonGlobalScope(context.superAdmin, storedScope);
|
|
2030
|
+
if (catalog) {
|
|
2031
|
+
const report = this.validateNativeItemData(stripped.data, catalog, typeRegistry);
|
|
2032
|
+
if (!report.isValid) {
|
|
2033
|
+
const logicalItemId = resolveCatalogItemId({ identity: descriptor.descriptor.identity, data: stripped.data });
|
|
2034
|
+
throw new CatalogValidationError({
|
|
2035
|
+
reason: "invalid_payload",
|
|
2036
|
+
catalogId: String(catalogId),
|
|
2037
|
+
itemId: String(logicalItemId),
|
|
2038
|
+
report,
|
|
2039
|
+
});
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
1868
2042
|
const derived = this.deriveIndexed(descriptor.descriptor, stripped.data);
|
|
1869
2043
|
const scopeIdx = indexedScopeFields(storedScope);
|
|
1870
2044
|
const indexed = derived != null ? { ...derived, ...scopeIdx } : (Object.keys(scopeIdx).length ? scopeIdx : undefined);
|
|
@@ -1968,6 +2142,59 @@ export class Catalox {
|
|
|
1968
2142
|
const data = await this.exportInventory(context, input);
|
|
1969
2143
|
return toJson(data, pretty);
|
|
1970
2144
|
}
|
|
2145
|
+
/**
|
|
2146
|
+
* Versioned full-catalog snapshot (`catalox-export/v1`) for backup and drift detection.
|
|
2147
|
+
* Paginates all items per catalog; catalogs and items are sorted deterministically.
|
|
2148
|
+
*/
|
|
2149
|
+
async exportCatalogSnapshot(context, input = {}) {
|
|
2150
|
+
const appId = context.appId;
|
|
2151
|
+
if (!appId)
|
|
2152
|
+
throw new Error("exportCatalogSnapshot: missing context.appId");
|
|
2153
|
+
const appCtx = {
|
|
2154
|
+
...context,
|
|
2155
|
+
appId,
|
|
2156
|
+
...(input.storeId != null ? { storeId: input.storeId } : {}),
|
|
2157
|
+
};
|
|
2158
|
+
const catalogs = await this.listAppCatalogs(appCtx, {
|
|
2159
|
+
appId,
|
|
2160
|
+
...(input.includeDisabledCatalogs != null ? { includeDisabled: input.includeDisabledCatalogs } : {}),
|
|
2161
|
+
...(input.includeHiddenCatalogs != null ? { includeHidden: input.includeHiddenCatalogs } : {}),
|
|
2162
|
+
});
|
|
2163
|
+
const filtered = filterCatalogEntries(catalogs, input);
|
|
2164
|
+
const pageSize = input.pageSize ?? 200;
|
|
2165
|
+
const superList = input.includeAllNativeScopes !== false && Boolean(context.superAdmin);
|
|
2166
|
+
const includeItemData = input.includeItemData !== false;
|
|
2167
|
+
const exported = [];
|
|
2168
|
+
for (const c of filtered.sort((a, b) => String(a.catalogId).localeCompare(String(b.catalogId), "en"))) {
|
|
2169
|
+
const { items, truncated, listOutcome, issues } = await listAllCatalogItemsForExport(c.catalogId, (catalogId, opts) => this.listCatalogItems(appCtx, catalogId, {
|
|
2170
|
+
limit: opts.limit,
|
|
2171
|
+
...(opts.offset != null ? { offset: opts.offset } : {}),
|
|
2172
|
+
...(opts.cursor ? { cursor: opts.cursor } : {}),
|
|
2173
|
+
...(opts.scope ? { scope: opts.scope } : {}),
|
|
2174
|
+
}), {
|
|
2175
|
+
pageSize,
|
|
2176
|
+
...(input.maxItemsPerCatalog != null ? { maxItems: input.maxItemsPerCatalog } : {}),
|
|
2177
|
+
...(superList && c.sourceMode === "native" ? { superList: true } : {}),
|
|
2178
|
+
});
|
|
2179
|
+
const payloadItems = includeItemData ? items : stripItemData(items);
|
|
2180
|
+
const catalogRecord = await this.deps.catalogs.get(c.catalogId);
|
|
2181
|
+
exported.push({
|
|
2182
|
+
catalogId: c.catalogId,
|
|
2183
|
+
catalog: c,
|
|
2184
|
+
...(catalogRecord?.schemaVersion != null ? { schemaVersion: String(catalogRecord.schemaVersion) } : {}),
|
|
2185
|
+
itemCount: payloadItems.length,
|
|
2186
|
+
...(truncated ? { truncated: true } : {}),
|
|
2187
|
+
listOutcome,
|
|
2188
|
+
...(issues?.length ? { issues } : {}),
|
|
2189
|
+
items: payloadItems,
|
|
2190
|
+
});
|
|
2191
|
+
}
|
|
2192
|
+
return buildCataloxExportV1({ context: appCtx, input, catalogs: exported });
|
|
2193
|
+
}
|
|
2194
|
+
async exportCatalogSnapshotToJson(context, input = {}, pretty = true) {
|
|
2195
|
+
const data = await this.exportCatalogSnapshot(context, input);
|
|
2196
|
+
return toJson(data, pretty);
|
|
2197
|
+
}
|
|
1971
2198
|
async generateInventoryReport(context, input = {}) {
|
|
1972
2199
|
const now = new Date().toISOString();
|
|
1973
2200
|
const top = input.top ?? 10;
|