@x12i/catalox 2.7.0 → 3.1.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 +73 -31
- package/dist/src/catalox/authorization.js +1 -1
- package/dist/src/catalox/authorization.js.map +1 -1
- package/dist/src/catalox/catalog-lifecycle.d.ts +29 -0
- package/dist/src/catalox/catalog-lifecycle.d.ts.map +1 -0
- package/dist/src/catalox/catalog-lifecycle.js +480 -0
- package/dist/src/catalox/catalog-lifecycle.js.map +1 -0
- package/dist/src/catalox/catalox-bound.d.ts +135 -0
- package/dist/src/catalox/catalox-bound.d.ts.map +1 -0
- package/dist/src/catalox/catalox-bound.js +190 -0
- package/dist/src/catalox/catalox-bound.js.map +1 -0
- package/dist/src/catalox/catalox.d.ts +34 -4
- package/dist/src/catalox/catalox.d.ts.map +1 -1
- package/dist/src/catalox/catalox.js +508 -72
- package/dist/src/catalox/catalox.js.map +1 -1
- package/dist/src/catalox/context.js +2 -2
- package/dist/src/catalox/context.js.map +1 -1
- package/dist/src/catalox/create-catalox.d.ts +28 -0
- package/dist/src/catalox/create-catalox.d.ts.map +1 -0
- package/dist/src/catalox/create-catalox.js +90 -0
- package/dist/src/catalox/create-catalox.js.map +1 -0
- package/dist/src/catalox/index.d.ts +4 -0
- package/dist/src/catalox/index.d.ts.map +1 -1
- package/dist/src/catalox/index.js +4 -0
- package/dist/src/catalox/index.js.map +1 -1
- package/dist/src/catalox/native-catalog-merge.d.ts +12 -0
- package/dist/src/catalox/native-catalog-merge.d.ts.map +1 -0
- package/dist/src/catalox/native-catalog-merge.js +102 -0
- package/dist/src/catalox/native-catalog-merge.js.map +1 -0
- package/dist/src/catalox/native-scope.d.ts +28 -0
- package/dist/src/catalox/native-scope.d.ts.map +1 -0
- package/dist/src/catalox/native-scope.js +184 -0
- package/dist/src/catalox/native-scope.js.map +1 -0
- package/dist/src/catalox/record-history.d.ts +53 -0
- package/dist/src/catalox/record-history.d.ts.map +1 -0
- package/dist/src/catalox/record-history.js +158 -0
- package/dist/src/catalox/record-history.js.map +1 -0
- package/dist/src/cli/index.js +148 -42
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/contracts/apps.d.ts +2 -0
- package/dist/src/contracts/apps.d.ts.map +1 -1
- package/dist/src/contracts/catalog-lifecycle.d.ts +70 -0
- package/dist/src/contracts/catalog-lifecycle.d.ts.map +1 -0
- package/dist/src/contracts/catalog-lifecycle.js +2 -0
- package/dist/src/contracts/catalog-lifecycle.js.map +1 -0
- package/dist/src/contracts/catalogs.d.ts +54 -0
- package/dist/src/contracts/catalogs.d.ts.map +1 -1
- package/dist/src/contracts/catalogs.js.map +1 -1
- package/dist/src/contracts/context.d.ts +5 -1
- package/dist/src/contracts/context.d.ts.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 +5 -2
- package/dist/src/contracts/index.d.ts.map +1 -1
- package/dist/src/contracts/index.js.map +1 -1
- package/dist/src/contracts/items.d.ts +19 -0
- package/dist/src/contracts/items.d.ts.map +1 -1
- package/dist/src/contracts/record-history.d.ts +66 -0
- package/dist/src/contracts/record-history.d.ts.map +1 -0
- package/dist/src/contracts/record-history.js +2 -0
- package/dist/src/contracts/record-history.js.map +1 -0
- package/dist/src/embedder.d.ts +17 -0
- package/dist/src/embedder.d.ts.map +1 -0
- package/dist/src/embedder.js +17 -0
- package/dist/src/embedder.js.map +1 -0
- package/dist/src/firebase/adapter-store.d.ts +1 -0
- package/dist/src/firebase/adapter-store.d.ts.map +1 -1
- package/dist/src/firebase/adapter-store.js +3 -0
- package/dist/src/firebase/adapter-store.js.map +1 -1
- package/dist/src/firebase/binding-store.d.ts +2 -0
- package/dist/src/firebase/binding-store.d.ts.map +1 -1
- package/dist/src/firebase/binding-store.js +10 -0
- package/dist/src/firebase/binding-store.js.map +1 -1
- package/dist/src/firebase/catalog-data-index-store.d.ts +1 -0
- package/dist/src/firebase/catalog-data-index-store.d.ts.map +1 -1
- package/dist/src/firebase/catalog-data-index-store.js +3 -0
- package/dist/src/firebase/catalog-data-index-store.js.map +1 -1
- package/dist/src/firebase/catalog-item-history-store.d.ts +21 -0
- package/dist/src/firebase/catalog-item-history-store.d.ts.map +1 -0
- package/dist/src/firebase/catalog-item-history-store.js +61 -0
- package/dist/src/firebase/catalog-item-history-store.js.map +1 -0
- package/dist/src/firebase/catalog-store.d.ts +1 -0
- package/dist/src/firebase/catalog-store.d.ts.map +1 -1
- package/dist/src/firebase/catalog-store.js +3 -0
- package/dist/src/firebase/catalog-store.js.map +1 -1
- package/dist/src/firebase/index.d.ts +1 -0
- package/dist/src/firebase/index.d.ts.map +1 -1
- package/dist/src/firebase/index.js +1 -0
- package/dist/src/firebase/index.js.map +1 -1
- package/dist/src/firebase/mapping-store.d.ts +1 -0
- package/dist/src/firebase/mapping-store.d.ts.map +1 -1
- package/dist/src/firebase/mapping-store.js +3 -0
- package/dist/src/firebase/mapping-store.js.map +1 -1
- package/dist/src/firebase/native-item-store.d.ts +8 -2
- package/dist/src/firebase/native-item-store.d.ts.map +1 -1
- package/dist/src/firebase/native-item-store.js +22 -6
- package/dist/src/firebase/native-item-store.js.map +1 -1
- package/dist/src/firebase/reference-store.d.ts +3 -0
- package/dist/src/firebase/reference-store.d.ts.map +1 -1
- package/dist/src/firebase/reference-store.js +16 -0
- package/dist/src/firebase/reference-store.js.map +1 -1
- package/dist/src/firebase/renderer-snippet-store.d.ts +3 -0
- package/dist/src/firebase/renderer-snippet-store.d.ts.map +1 -1
- package/dist/src/firebase/renderer-snippet-store.js +17 -0
- package/dist/src/firebase/renderer-snippet-store.js.map +1 -1
- package/dist/src/firebase/snapshot-store.d.ts +1 -0
- package/dist/src/firebase/snapshot-store.d.ts.map +1 -1
- package/dist/src/firebase/snapshot-store.js +8 -0
- package/dist/src/firebase/snapshot-store.js.map +1 -1
- package/dist/src/index.d.ts +2 -15
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -15
- package/dist/src/index.js.map +1 -1
- package/dist/src/operator.d.ts +24 -0
- package/dist/src/operator.d.ts.map +1 -0
- package/dist/src/operator.js +25 -0
- package/dist/src/operator.js.map +1 -0
- package/dist/test/integration/firestore.emulator.test.js +16 -29
- package/dist/test/integration/firestore.emulator.test.js.map +1 -1
- package/dist/test/integration/record-history.live.test.d.ts +2 -0
- package/dist/test/integration/record-history.live.test.d.ts.map +1 -0
- package/dist/test/integration/record-history.live.test.js +126 -0
- package/dist/test/integration/record-history.live.test.js.map +1 -0
- package/dist/test/unit/native-catalog-merge.test.d.ts +2 -0
- package/dist/test/unit/native-catalog-merge.test.d.ts.map +1 -0
- package/dist/test/unit/native-catalog-merge.test.js +33 -0
- package/dist/test/unit/native-catalog-merge.test.js.map +1 -0
- package/dist/test/unit/native-scope.test.d.ts +2 -0
- package/dist/test/unit/native-scope.test.d.ts.map +1 -0
- package/dist/test/unit/native-scope.test.js +29 -0
- package/dist/test/unit/native-scope.test.js.map +1 -0
- package/dist/test/unit/record-history-path.test.d.ts +2 -0
- package/dist/test/unit/record-history-path.test.d.ts.map +1 -0
- package/dist/test/unit/record-history-path.test.js +24 -0
- package/dist/test/unit/record-history-path.test.js.map +1 -0
- package/firestore.indexes.json +39 -0
- package/package.json +23 -6
|
@@ -6,6 +6,8 @@ import { ApiCatalogAdapter } from "../adapters/api/api-adapter.js";
|
|
|
6
6
|
import { MongoCatalogAdapter } from "../adapters/mongo/mongo-adapter.js";
|
|
7
7
|
import { AuthorizationService } from "./authorization.js";
|
|
8
8
|
import { resolveCatalogItemId } from "./identity.js";
|
|
9
|
+
import { assertSuperAdminForNonGlobalScope, encodeNativeItemStorageDocId, indexedScopeFields, isGlobalPhysicalRow, normalizeStoredScope, parseWriteScopeInput, scopeFromRecordField, } from "./native-scope.js";
|
|
10
|
+
import { filterPhysicalForTenantFetch, mergeNativePhysicalRows, pickWinningPhysicalRow, } from "./native-catalog-merge.js";
|
|
9
11
|
import { parseJson, toJson } from "./json-io.js";
|
|
10
12
|
import { createHash, randomUUID } from "node:crypto";
|
|
11
13
|
import { renderInventoryReportMarkdown } from "./reporting/render-inventory-report.js";
|
|
@@ -19,7 +21,7 @@ import { DefinitionStore } from "../firebase/definition-store.js";
|
|
|
19
21
|
import { DescriptorStore } from "../firebase/descriptor-store.js";
|
|
20
22
|
import { FirestoreStore } from "../firebase/firestore-store.js";
|
|
21
23
|
import { MappingStore } from "../firebase/mapping-store.js";
|
|
22
|
-
import { NativeItemStore } from "../firebase/native-item-store.js";
|
|
24
|
+
import { NativeItemStore, storageDocIdForNativeRecord } from "../firebase/native-item-store.js";
|
|
23
25
|
import { ReferenceStore } from "../firebase/reference-store.js";
|
|
24
26
|
import { RendererSnippetStore } from "../firebase/renderer-snippet-store.js";
|
|
25
27
|
import { SnapshotStore } from "../firebase/snapshot-store.js";
|
|
@@ -33,11 +35,45 @@ import { migrateNativeCatalogLayout as runMigrateNativeCatalogLayout, } from "..
|
|
|
33
35
|
import { reportNativeCatalogLayoutDiagnostics as runReportNativeCatalogLayoutDiagnostics, } from "./native-catalog-layout-diagnostics.js";
|
|
34
36
|
import { exportAllFirestoreCollectionsToGcs, exportFirestoreCollectionToGcs, restoreAllFirestoreCollectionsFromGcsManifest, restoreFirestoreCollectionFromGcs, } from "./firestore-gcs-transfer.js";
|
|
35
37
|
import { compareAllFirestoreCollectionsWithGcsManifest, compareFirestoreCollectionWithGcsNdjson, } from "./firestore-gcs-compare.js";
|
|
38
|
+
import { CataloxBound } from "./catalox-bound.js";
|
|
39
|
+
import { emitRecordHistoryEvent, readRecordHistoryEventPayload, } from "./record-history.js";
|
|
40
|
+
import { runDeleteCatalog, runRenameCatalog, runRestoreDeletedCatalog, } from "./catalog-lifecycle.js";
|
|
36
41
|
export class Catalox {
|
|
37
42
|
deps;
|
|
38
43
|
constructor(deps) {
|
|
39
44
|
this.deps = deps;
|
|
40
45
|
}
|
|
46
|
+
catalogLifecycleDeps() {
|
|
47
|
+
return {
|
|
48
|
+
firestoreStore: this.deps.firestoreStore,
|
|
49
|
+
catalogs: this.deps.catalogs,
|
|
50
|
+
bindings: this.deps.bindings,
|
|
51
|
+
definitions: this.deps.definitions,
|
|
52
|
+
descriptors: this.deps.descriptors,
|
|
53
|
+
mappings: this.deps.mappings,
|
|
54
|
+
adapters: this.deps.adapters,
|
|
55
|
+
references: this.deps.references,
|
|
56
|
+
nativeItems: this.deps.nativeItems,
|
|
57
|
+
snapshots: this.deps.snapshots,
|
|
58
|
+
catalogDataIndex: this.deps.catalogDataIndex,
|
|
59
|
+
...(this.deps.rendererSnippets ? { rendererSnippets: this.deps.rendererSnippets } : {}),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
async emitNativeItemHistory(context, params) {
|
|
63
|
+
const rh = this.deps.recordHistory;
|
|
64
|
+
if (!rh)
|
|
65
|
+
return;
|
|
66
|
+
const res = await emitRecordHistoryEvent(rh, context, {
|
|
67
|
+
catalogId: params.catalogId,
|
|
68
|
+
itemId: params.itemId,
|
|
69
|
+
...(params.storageDocId != null ? { storageDocId: params.storageDocId } : {}),
|
|
70
|
+
op: params.op,
|
|
71
|
+
...(params.before !== undefined ? { before: params.before } : {}),
|
|
72
|
+
...(params.after !== undefined ? { after: params.after } : {}),
|
|
73
|
+
});
|
|
74
|
+
if (!res.ok && rh.failClosed)
|
|
75
|
+
throw new Error(`recordHistory: ${res.error}`);
|
|
76
|
+
}
|
|
41
77
|
resolveActorId(context) {
|
|
42
78
|
return context.userId ?? context.actor?.id;
|
|
43
79
|
}
|
|
@@ -194,9 +230,13 @@ export class Catalox {
|
|
|
194
230
|
}
|
|
195
231
|
async buildCatalogItemRenderMap(context, catalogId, itemId, options) {
|
|
196
232
|
const descriptor = await this.getCatalogDescriptor(context, catalogId);
|
|
197
|
-
const
|
|
198
|
-
if (
|
|
233
|
+
const got = await this.getCatalogItem(context, catalogId, itemId);
|
|
234
|
+
if (got.outcome === "mapping_blocked") {
|
|
235
|
+
throw new CatalogAdapterError({ catalogId, reason: "mapping_validation_failed", issues: got.issues });
|
|
236
|
+
}
|
|
237
|
+
if (got.outcome === "not_found")
|
|
199
238
|
throw new CatalogNotFoundError({ catalogId, itemId });
|
|
239
|
+
const item = got.item;
|
|
200
240
|
const sources = this.gatherDescriptorSources(descriptor);
|
|
201
241
|
const resolvedSources = {};
|
|
202
242
|
if (options?.resolveSources !== false) {
|
|
@@ -262,8 +302,83 @@ export class Catalox {
|
|
|
262
302
|
return Object.keys(out).length ? out : undefined;
|
|
263
303
|
}
|
|
264
304
|
stripReservedWriteFields(input) {
|
|
265
|
-
const { indexed, ...rest } = input;
|
|
266
|
-
|
|
305
|
+
const { indexed, scope, ...rest } = input;
|
|
306
|
+
const parsedScope = scope != null ? parseWriteScopeInput(scope) : undefined;
|
|
307
|
+
return {
|
|
308
|
+
data: rest,
|
|
309
|
+
...(indexed != null ? { indexed: indexed } : {}),
|
|
310
|
+
...(parsedScope != null ? { scope: parsedScope } : {}),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
normalizeListFetchScope(scope) {
|
|
314
|
+
if (!scope)
|
|
315
|
+
return undefined;
|
|
316
|
+
if (!scope.accountId && !scope.agentId && !scope.userId && scope.superAdmin !== true)
|
|
317
|
+
return undefined;
|
|
318
|
+
return scope;
|
|
319
|
+
}
|
|
320
|
+
buildAppliedScope(scope, effectiveSuperList) {
|
|
321
|
+
return {
|
|
322
|
+
...(scope?.accountId != null ? { accountId: scope.accountId } : {}),
|
|
323
|
+
...(scope?.agentId != null ? { agentId: scope.agentId } : {}),
|
|
324
|
+
...(scope?.userId != null ? { userId: scope.userId } : {}),
|
|
325
|
+
superAdmin: effectiveSuperList,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
async fetchGlobalPhysicalRows(catalogId, listOpts) {
|
|
329
|
+
const userFe = { ...(listOpts.filterEq ?? {}) };
|
|
330
|
+
const withGlobal = { ...userFe, scopeLayer: "global" };
|
|
331
|
+
let recs = await this.deps.nativeItems.list(catalogId, { ...listOpts, filterEq: withGlobal });
|
|
332
|
+
if (recs.length === 0 && Object.keys(userFe).length === 0) {
|
|
333
|
+
const broad = await this.deps.nativeItems.list(catalogId, { ...listOpts, filterEq: {} });
|
|
334
|
+
recs = broad.filter((r) => isGlobalPhysicalRow(r));
|
|
335
|
+
}
|
|
336
|
+
return recs;
|
|
337
|
+
}
|
|
338
|
+
async fetchTenantPhysicalRows(catalogId, fetch, listOpts) {
|
|
339
|
+
const userFe = { ...(listOpts.filterEq ?? {}) };
|
|
340
|
+
const cap = { ...listOpts, limit: 500, offset: 0 };
|
|
341
|
+
const seen = new Set();
|
|
342
|
+
const out = [];
|
|
343
|
+
const take = (rows) => {
|
|
344
|
+
for (const r of rows) {
|
|
345
|
+
const id = storageDocIdForNativeRecord(r);
|
|
346
|
+
if (seen.has(id))
|
|
347
|
+
continue;
|
|
348
|
+
seen.add(id);
|
|
349
|
+
out.push(r);
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
take(await this.deps.nativeItems.list(catalogId, { ...cap, filterEq: { ...userFe, scopeLayer: "global" } }));
|
|
353
|
+
if (fetch.accountId) {
|
|
354
|
+
take(await this.deps.nativeItems.list(catalogId, {
|
|
355
|
+
...cap,
|
|
356
|
+
filterEq: { ...userFe, scopeLayer: "account", scopeAccountId: fetch.accountId },
|
|
357
|
+
}));
|
|
358
|
+
if (fetch.agentId) {
|
|
359
|
+
take(await this.deps.nativeItems.list(catalogId, {
|
|
360
|
+
...cap,
|
|
361
|
+
filterEq: {
|
|
362
|
+
...userFe,
|
|
363
|
+
scopeLayer: "agent",
|
|
364
|
+
scopeAccountId: fetch.accountId,
|
|
365
|
+
scopeAgentId: fetch.agentId,
|
|
366
|
+
},
|
|
367
|
+
}));
|
|
368
|
+
}
|
|
369
|
+
if (fetch.userId) {
|
|
370
|
+
take(await this.deps.nativeItems.list(catalogId, {
|
|
371
|
+
...cap,
|
|
372
|
+
filterEq: {
|
|
373
|
+
...userFe,
|
|
374
|
+
scopeLayer: "user",
|
|
375
|
+
scopeAccountId: fetch.accountId,
|
|
376
|
+
scopeUserId: fetch.userId,
|
|
377
|
+
},
|
|
378
|
+
}));
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return filterPhysicalForTenantFetch(out, fetch);
|
|
267
382
|
}
|
|
268
383
|
async decorateItem(catalogId, item) {
|
|
269
384
|
const descriptor = await this.deps.descriptors.get(catalogId);
|
|
@@ -356,26 +471,38 @@ export class Catalox {
|
|
|
356
471
|
};
|
|
357
472
|
})();
|
|
358
473
|
if (catalog.sourceMode === "native") {
|
|
474
|
+
const descRec = await this.deps.descriptors.get(catalogId);
|
|
475
|
+
if (!descRec)
|
|
476
|
+
throw new CatalogAdapterError({ catalogId, reason: "missing_descriptor" });
|
|
359
477
|
const filterEq = (listQueryOptions?.filter ?? {});
|
|
360
|
-
const
|
|
478
|
+
const baseListOpts = {
|
|
361
479
|
...(listQueryOptions?.limit != null ? { limit: listQueryOptions.limit } : {}),
|
|
362
480
|
...(listQueryOptions?.offset != null ? { offset: listQueryOptions.offset } : {}),
|
|
363
481
|
...(Object.keys(filterEq).length ? { filterEq } : {}),
|
|
364
482
|
...(listQueryOptions?.sort ? { sort: listQueryOptions.sort } : {}),
|
|
365
483
|
};
|
|
366
|
-
const
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
}
|
|
378
|
-
|
|
484
|
+
const rawScope = this.normalizeListFetchScope(listQueryOptions?.scope);
|
|
485
|
+
const wantsSuperList = rawScope?.superAdmin === true && context.superAdmin === true;
|
|
486
|
+
let physical = [];
|
|
487
|
+
if (wantsSuperList) {
|
|
488
|
+
physical = await this.deps.nativeItems.list(catalogId, baseListOpts);
|
|
489
|
+
}
|
|
490
|
+
else if (!rawScope?.accountId && !rawScope?.agentId && !rawScope?.userId) {
|
|
491
|
+
physical = await this.fetchGlobalPhysicalRows(catalogId, baseListOpts);
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
physical = await this.fetchTenantPhysicalRows(catalogId, rawScope, baseListOpts);
|
|
495
|
+
}
|
|
496
|
+
const merged = mergeNativePhysicalRows(physical, descRec.descriptor.identity, String(context.appId), wantsSuperList);
|
|
497
|
+
const off = listQueryOptions?.offset ?? 0;
|
|
498
|
+
const lim = listQueryOptions?.limit ?? 100;
|
|
499
|
+
const sliced = merged.slice(off, off + lim);
|
|
500
|
+
const decorated = await Promise.all(sliced.map((it) => this.decorateItem(catalogId, it)));
|
|
501
|
+
return {
|
|
502
|
+
listOutcome: "ok",
|
|
503
|
+
items: decorated,
|
|
504
|
+
appliedScope: this.buildAppliedScope(rawScope, wantsSuperList),
|
|
505
|
+
};
|
|
379
506
|
}
|
|
380
507
|
const def = await this.deps.definitions.get(catalogId);
|
|
381
508
|
if (!def || def.type !== "mapped")
|
|
@@ -385,16 +512,24 @@ export class Catalox {
|
|
|
385
512
|
throw new CatalogAdapterError({ catalogId, reason: "missing_mapping" });
|
|
386
513
|
const mappingIssues = validateMappingSpec(mapping.mapping);
|
|
387
514
|
if (mappingIssues.some((i) => i.severity === "error")) {
|
|
388
|
-
return { items: [], issues: mappingIssues };
|
|
515
|
+
return { listOutcome: "mapping_blocked", items: [], issues: mappingIssues };
|
|
389
516
|
}
|
|
517
|
+
const mappedListQueryOptions = listQueryOptions != null
|
|
518
|
+
? (() => {
|
|
519
|
+
const { scope: _omitScope, ...rest } = listQueryOptions;
|
|
520
|
+
return rest;
|
|
521
|
+
})()
|
|
522
|
+
: undefined;
|
|
390
523
|
if (def.adapterType === "mongo") {
|
|
391
524
|
if (!this.deps.mongoAdapter)
|
|
392
525
|
throw new CatalogAdapterError({ catalogId, reason: "mongo_adapter_unconfigured" });
|
|
393
526
|
const adapterConfig = await this.deps.adapters.get(def.adapterId);
|
|
394
527
|
if (!adapterConfig)
|
|
395
528
|
throw new CatalogAdapterError({ catalogId, reason: "missing_adapter" });
|
|
396
|
-
const result = await this.deps.mongoAdapter.listItems(context, catalogId, adapterConfig, { mapping: mapping.mapping, ...(mapping.options ? { options: mapping.options } : {}) },
|
|
397
|
-
return result.issues
|
|
529
|
+
const result = await this.deps.mongoAdapter.listItems(context, catalogId, adapterConfig, { mapping: mapping.mapping, ...(mapping.options ? { options: mapping.options } : {}) }, mappedListQueryOptions);
|
|
530
|
+
return result.issues?.length
|
|
531
|
+
? { listOutcome: "ok", items: result.items, issues: result.issues }
|
|
532
|
+
: { listOutcome: "ok", items: result.items };
|
|
398
533
|
}
|
|
399
534
|
if (def.adapterType === "api") {
|
|
400
535
|
if (!this.deps.apiAdapter)
|
|
@@ -402,8 +537,9 @@ export class Catalox {
|
|
|
402
537
|
const adapterConfig = await this.deps.adapters.get(def.adapterId);
|
|
403
538
|
if (!adapterConfig)
|
|
404
539
|
throw new CatalogAdapterError({ catalogId, reason: "missing_adapter" });
|
|
405
|
-
const result = await this.deps.apiAdapter.listItems(context, catalogId, adapterConfig, { responseMapping: mapping.mapping, ...(mapping.options ? { options: mapping.options } : {}) },
|
|
540
|
+
const result = await this.deps.apiAdapter.listItems(context, catalogId, adapterConfig, { responseMapping: mapping.mapping, ...(mapping.options ? { options: mapping.options } : {}) }, mappedListQueryOptions);
|
|
406
541
|
return {
|
|
542
|
+
listOutcome: "ok",
|
|
407
543
|
items: result.items,
|
|
408
544
|
...(result.nextCursor ? { nextCursor: result.nextCursor } : {}),
|
|
409
545
|
...(result.issues ? { issues: result.issues } : {}),
|
|
@@ -411,35 +547,66 @@ export class Catalox {
|
|
|
411
547
|
}
|
|
412
548
|
throw new CatalogAdapterError({ catalogId, reason: "unknown_adapter_type" });
|
|
413
549
|
}
|
|
414
|
-
async getCatalogItem(context, catalogId, itemId) {
|
|
550
|
+
async getCatalogItem(context, catalogId, itemId, options) {
|
|
415
551
|
await this.deps.authz.requireBindingAccess(context, context.appId, catalogId, "read");
|
|
416
552
|
const catalog = await this.deps.catalogs.get(catalogId);
|
|
417
553
|
if (!catalog)
|
|
418
554
|
throw new CatalogNotFoundError({ catalogId });
|
|
419
555
|
if (catalog.sourceMode === "native") {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
556
|
+
if (options?.storageDocId) {
|
|
557
|
+
const rec = await this.deps.nativeItems.get(catalogId, options.storageDocId);
|
|
558
|
+
if (!rec)
|
|
559
|
+
return { outcome: "not_found" };
|
|
560
|
+
const meta = {
|
|
561
|
+
...(rec.metadata && typeof rec.metadata === "object" ? rec.metadata : {}),
|
|
562
|
+
scope: rec.scope ?? { kind: "global" },
|
|
563
|
+
};
|
|
564
|
+
const base = {
|
|
565
|
+
itemId: rec.itemId,
|
|
566
|
+
catalogId: rec.catalogId,
|
|
567
|
+
appId: context.appId,
|
|
568
|
+
sourceMode: "native",
|
|
569
|
+
sourceType: "firebase",
|
|
570
|
+
data: rec.data,
|
|
571
|
+
metadata: meta,
|
|
572
|
+
createdAt: rec.createdAt,
|
|
573
|
+
updatedAt: rec.updatedAt,
|
|
574
|
+
};
|
|
575
|
+
return { outcome: "found", item: await this.decorateItem(catalogId, base) };
|
|
576
|
+
}
|
|
577
|
+
const descRec = await this.deps.descriptors.get(catalogId);
|
|
578
|
+
if (!descRec)
|
|
579
|
+
throw new CatalogAdapterError({ catalogId, reason: "missing_descriptor" });
|
|
580
|
+
let rows = await this.deps.nativeItems.findByLogicalItemId(catalogId, itemId);
|
|
581
|
+
if (!rows.length) {
|
|
582
|
+
const legacy = await this.deps.nativeItems.get(catalogId, itemId);
|
|
583
|
+
if (legacy)
|
|
584
|
+
rows = [legacy];
|
|
585
|
+
}
|
|
586
|
+
if (!rows.length)
|
|
587
|
+
return { outcome: "not_found" };
|
|
588
|
+
const scope = options?.scope;
|
|
589
|
+
const filtered = scope?.accountId || scope?.agentId || scope?.userId
|
|
590
|
+
? filterPhysicalForTenantFetch(rows, scope)
|
|
591
|
+
: rows.filter((r) => isGlobalPhysicalRow(r));
|
|
592
|
+
const merged = mergeNativePhysicalRows(filtered, descRec.descriptor.identity, String(context.appId), false);
|
|
593
|
+
const hit = merged.find((m) => m.itemId === itemId) ?? merged[0];
|
|
594
|
+
if (!hit)
|
|
595
|
+
return { outcome: "not_found" };
|
|
596
|
+
return { outcome: "found", item: await this.decorateItem(catalogId, hit) };
|
|
435
597
|
}
|
|
436
598
|
// For mapped catalogs, use list path with filter if adapter supports.
|
|
437
599
|
const result = await this.listCatalogItems(context, catalogId, {
|
|
438
600
|
limit: 1,
|
|
439
601
|
filter: { itemId },
|
|
440
602
|
});
|
|
603
|
+
if (result.listOutcome === "mapping_blocked") {
|
|
604
|
+
return { outcome: "mapping_blocked", issues: result.issues ?? [] };
|
|
605
|
+
}
|
|
441
606
|
const first = result.items[0];
|
|
442
|
-
|
|
607
|
+
if (!first)
|
|
608
|
+
return { outcome: "not_found" };
|
|
609
|
+
return { outcome: "found", item: await this.decorateItem(catalogId, first) };
|
|
443
610
|
}
|
|
444
611
|
async validateCatalog(_context, _catalogId) {
|
|
445
612
|
return { isValid: true, issues: [] };
|
|
@@ -619,8 +786,8 @@ export class Catalox {
|
|
|
619
786
|
async bindAppToStore(context, input) {
|
|
620
787
|
if (!this.deps.storeAppBindings)
|
|
621
788
|
throw new Error("storeAppBindings dependency is not configured");
|
|
622
|
-
if (!context.
|
|
623
|
-
throw new CatalogAccessDeniedError({ reason: "
|
|
789
|
+
if (!context.superAdmin && input.appId !== context.appId) {
|
|
790
|
+
throw new CatalogAccessDeniedError({ reason: "not_super_admin" });
|
|
624
791
|
}
|
|
625
792
|
const existing = await this.deps.storeAppBindings.findByStoreApp(input.storeId, input.appId);
|
|
626
793
|
if (existing)
|
|
@@ -643,8 +810,8 @@ export class Catalox {
|
|
|
643
810
|
async unbindAppFromStore(context, storeId, appId) {
|
|
644
811
|
if (!this.deps.storeAppBindings)
|
|
645
812
|
throw new Error("storeAppBindings dependency is not configured");
|
|
646
|
-
if (!context.
|
|
647
|
-
throw new CatalogAccessDeniedError({ reason: "
|
|
813
|
+
if (!context.superAdmin && appId !== context.appId) {
|
|
814
|
+
throw new CatalogAccessDeniedError({ reason: "not_super_admin" });
|
|
648
815
|
}
|
|
649
816
|
const existing = await this.deps.storeAppBindings.findByStoreApp(storeId, appId);
|
|
650
817
|
if (!existing)
|
|
@@ -660,9 +827,9 @@ export class Catalox {
|
|
|
660
827
|
async listAppsForStore(context, storeId) {
|
|
661
828
|
if (!this.deps.storeAppBindings)
|
|
662
829
|
throw new Error("storeAppBindings dependency is not configured");
|
|
663
|
-
// listing is allowed for any caller;
|
|
830
|
+
// listing is allowed for any caller; super-admin sees all store memberships.
|
|
664
831
|
const records = await this.deps.storeAppBindings.listAppsByStore(storeId);
|
|
665
|
-
if (context.
|
|
832
|
+
if (context.superAdmin)
|
|
666
833
|
return records;
|
|
667
834
|
// non-god: only reveal memberships that include the caller's own appId
|
|
668
835
|
return records.filter((r) => r.appId === context.appId);
|
|
@@ -683,9 +850,9 @@ export class Catalox {
|
|
|
683
850
|
});
|
|
684
851
|
}
|
|
685
852
|
async ensureBinding(context, input) {
|
|
686
|
-
// only
|
|
687
|
-
if (!context.
|
|
688
|
-
throw new CatalogAccessDeniedError({ reason: "
|
|
853
|
+
// only super-admin apps can provision cross-app bindings.
|
|
854
|
+
if (!context.superAdmin && input.appId !== context.appId) {
|
|
855
|
+
throw new CatalogAccessDeniedError({ reason: "not_super_admin" });
|
|
689
856
|
}
|
|
690
857
|
const existing = await this.deps.bindings.findByAppCatalog(input.appId, input.catalogId);
|
|
691
858
|
if (existing)
|
|
@@ -708,51 +875,152 @@ export class Catalox {
|
|
|
708
875
|
async createNativeCatalogItem(_context, _catalogId, _input) {
|
|
709
876
|
return this.upsertNativeCatalogItem(_context, _catalogId, _input);
|
|
710
877
|
}
|
|
711
|
-
async updateNativeCatalogItem(_context, _catalogId, _itemId, _patch) {
|
|
878
|
+
async updateNativeCatalogItem(_context, _catalogId, _itemId, _patch, _options) {
|
|
712
879
|
await this.deps.authz.requireBindingAccess(_context, _context.appId, _catalogId, "write");
|
|
713
|
-
|
|
880
|
+
let existing = null;
|
|
881
|
+
if (_options?.storageDocId) {
|
|
882
|
+
existing = await this.deps.nativeItems.get(_catalogId, _options.storageDocId);
|
|
883
|
+
}
|
|
884
|
+
else {
|
|
885
|
+
let rows = await this.deps.nativeItems.findByLogicalItemId(_catalogId, _itemId);
|
|
886
|
+
if (!rows.length) {
|
|
887
|
+
const legacy = await this.deps.nativeItems.get(_catalogId, _itemId);
|
|
888
|
+
if (legacy)
|
|
889
|
+
rows = [legacy];
|
|
890
|
+
}
|
|
891
|
+
existing = pickWinningPhysicalRow(rows, _options?.scope);
|
|
892
|
+
}
|
|
714
893
|
if (!existing)
|
|
715
894
|
throw new CatalogNotFoundError({ catalogId: _catalogId, itemId: _itemId });
|
|
895
|
+
const { data: patchData, indexed: patchIndexed, scope: patchScope } = this.stripReservedWriteFields(_patch);
|
|
896
|
+
let nextScope = normalizeStoredScope(scopeFromRecordField(existing.scope));
|
|
897
|
+
if (patchScope != null) {
|
|
898
|
+
nextScope = normalizeStoredScope(patchScope);
|
|
899
|
+
assertSuperAdminForNonGlobalScope(_context.superAdmin, nextScope);
|
|
900
|
+
}
|
|
716
901
|
const updatedAt = new Date().toISOString();
|
|
717
|
-
const
|
|
718
|
-
const
|
|
719
|
-
|
|
720
|
-
...
|
|
721
|
-
|
|
902
|
+
const mergedData = { ...(existing.data ?? {}), ...patchData };
|
|
903
|
+
const idx = {
|
|
904
|
+
...(existing.indexed ?? {}),
|
|
905
|
+
...(patchIndexed ?? {}),
|
|
906
|
+
...indexedScopeFields(nextScope),
|
|
907
|
+
};
|
|
908
|
+
const oldDocId = storageDocIdForNativeRecord(existing);
|
|
909
|
+
const { scope: _dropScope, ...existingRest } = existing;
|
|
910
|
+
const nextRec = {
|
|
911
|
+
...existingRest,
|
|
912
|
+
data: mergedData,
|
|
913
|
+
indexed: idx,
|
|
914
|
+
...(nextScope.kind !== "global" ? { scope: nextScope } : {}),
|
|
722
915
|
updatedAt,
|
|
723
|
-
...(actorId ? { updatedBy: actorId } : {}),
|
|
724
916
|
version: (existing.version ?? 0) + 1,
|
|
917
|
+
...(this.resolveActorId(_context) ? { updatedBy: this.resolveActorId(_context) } : {}),
|
|
918
|
+
};
|
|
919
|
+
const newDocId = storageDocIdForNativeRecord(nextRec);
|
|
920
|
+
await this.deps.nativeItems.upsert(_catalogId, nextRec);
|
|
921
|
+
if (newDocId !== oldDocId)
|
|
922
|
+
await this.deps.nativeItems.delete(_catalogId, oldDocId);
|
|
923
|
+
const persisted = (await this.deps.nativeItems.get(_catalogId, newDocId)) ?? nextRec;
|
|
924
|
+
await this.emitNativeItemHistory(_context, {
|
|
925
|
+
catalogId: _catalogId,
|
|
926
|
+
itemId: String(persisted.itemId),
|
|
927
|
+
storageDocId: newDocId,
|
|
928
|
+
op: "update",
|
|
929
|
+
before: existing,
|
|
930
|
+
after: persisted,
|
|
725
931
|
});
|
|
726
932
|
const out = {
|
|
727
|
-
itemId:
|
|
933
|
+
itemId: nextRec.itemId,
|
|
728
934
|
catalogId: _catalogId,
|
|
729
935
|
appId: _context.appId,
|
|
730
936
|
sourceMode: "native",
|
|
731
937
|
sourceType: "firebase",
|
|
732
|
-
data:
|
|
938
|
+
data: mergedData,
|
|
733
939
|
createdAt: existing.createdAt,
|
|
734
940
|
updatedAt,
|
|
735
941
|
};
|
|
736
942
|
return this.decorateItem(_catalogId, out);
|
|
737
943
|
}
|
|
738
|
-
async deleteNativeCatalogItem(_context, _catalogId, _itemId) {
|
|
944
|
+
async deleteNativeCatalogItem(_context, _catalogId, _itemId, _options) {
|
|
739
945
|
await this.deps.authz.requireBindingAccess(_context, _context.appId, _catalogId, "write");
|
|
740
|
-
|
|
946
|
+
let docId = _options?.storageDocId;
|
|
947
|
+
if (!docId) {
|
|
948
|
+
let rows = await this.deps.nativeItems.findByLogicalItemId(_catalogId, _itemId);
|
|
949
|
+
if (!rows.length) {
|
|
950
|
+
const legacy = await this.deps.nativeItems.get(_catalogId, _itemId);
|
|
951
|
+
if (legacy)
|
|
952
|
+
rows = [legacy];
|
|
953
|
+
}
|
|
954
|
+
const winner = pickWinningPhysicalRow(rows, _options?.scope);
|
|
955
|
+
if (!winner)
|
|
956
|
+
throw new CatalogNotFoundError({ catalogId: _catalogId, itemId: _itemId });
|
|
957
|
+
docId = storageDocIdForNativeRecord(winner);
|
|
958
|
+
}
|
|
959
|
+
const beforeDel = await this.deps.nativeItems.get(_catalogId, docId);
|
|
960
|
+
await this.deps.nativeItems.delete(_catalogId, docId);
|
|
961
|
+
if (beforeDel) {
|
|
962
|
+
await this.emitNativeItemHistory(_context, {
|
|
963
|
+
catalogId: _catalogId,
|
|
964
|
+
itemId: String(beforeDel.itemId),
|
|
965
|
+
storageDocId: docId,
|
|
966
|
+
op: "delete",
|
|
967
|
+
before: beforeDel,
|
|
968
|
+
after: null,
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
async moveNativeCatalogItemScope(context, catalogId, itemId, input) {
|
|
973
|
+
if (!context.superAdmin) {
|
|
974
|
+
throw new CatalogAccessDeniedError({ reason: "super_admin_required" });
|
|
975
|
+
}
|
|
976
|
+
await this.deps.authz.requireBindingAccess(context, context.appId, catalogId, "write");
|
|
977
|
+
const fromDoc = input.fromStorageDocId ??
|
|
978
|
+
encodeNativeItemStorageDocId(String(itemId), normalizeStoredScope(input.fromScope ?? { kind: "global" }));
|
|
979
|
+
const rec = await this.deps.nativeItems.get(catalogId, fromDoc);
|
|
980
|
+
if (!rec)
|
|
981
|
+
throw new CatalogNotFoundError({ catalogId, itemId });
|
|
982
|
+
const toScope = normalizeStoredScope(input.toScope);
|
|
983
|
+
const { scope: _dropScope, ...recRest } = rec;
|
|
984
|
+
const nextRec = {
|
|
985
|
+
...recRest,
|
|
986
|
+
...(toScope.kind !== "global" ? { scope: toScope } : {}),
|
|
987
|
+
indexed: { ...(rec.indexed ?? {}), ...indexedScopeFields(toScope) },
|
|
988
|
+
updatedAt: new Date().toISOString(),
|
|
989
|
+
};
|
|
990
|
+
const toDoc = storageDocIdForNativeRecord(nextRec);
|
|
991
|
+
await this.deps.nativeItems.upsert(catalogId, nextRec);
|
|
992
|
+
if (toDoc !== fromDoc)
|
|
993
|
+
await this.deps.nativeItems.delete(catalogId, fromDoc);
|
|
994
|
+
const persisted = (await this.deps.nativeItems.get(catalogId, toDoc)) ?? nextRec;
|
|
995
|
+
await this.emitNativeItemHistory(context, {
|
|
996
|
+
catalogId,
|
|
997
|
+
itemId: String(persisted.itemId),
|
|
998
|
+
storageDocId: toDoc,
|
|
999
|
+
op: "update",
|
|
1000
|
+
before: rec,
|
|
1001
|
+
after: persisted,
|
|
1002
|
+
});
|
|
741
1003
|
}
|
|
742
1004
|
async upsertNativeCatalogItem(context, catalogId, input) {
|
|
743
1005
|
await this.deps.authz.requireBindingAccess(context, context.appId, catalogId, "write");
|
|
744
1006
|
const descriptor = await this.deps.descriptors.get(catalogId);
|
|
745
1007
|
if (!descriptor)
|
|
746
1008
|
throw new CatalogAdapterError({ catalogId, reason: "missing_descriptor" });
|
|
747
|
-
const { data, indexed: callerIndexed } = this.stripReservedWriteFields(input);
|
|
748
|
-
const
|
|
749
|
-
|
|
1009
|
+
const { data, indexed: callerIndexed, scope: inputScope } = this.stripReservedWriteFields(input);
|
|
1010
|
+
const storedScope = normalizeStoredScope(inputScope ?? { kind: "global" });
|
|
1011
|
+
assertSuperAdminForNonGlobalScope(context.superAdmin, storedScope);
|
|
1012
|
+
const derived = this.deriveIndexed(descriptor.descriptor, data);
|
|
1013
|
+
const scopeIdx = indexedScopeFields(storedScope);
|
|
1014
|
+
const indexed = derived != null ? { ...derived, ...scopeIdx } : (Object.keys(scopeIdx).length ? scopeIdx : undefined);
|
|
1015
|
+
const logicalItemId = resolveCatalogItemId({ identity: descriptor.descriptor.identity, data });
|
|
1016
|
+
const storageDocId = encodeNativeItemStorageDocId(String(logicalItemId), storedScope);
|
|
750
1017
|
const now = new Date().toISOString();
|
|
751
|
-
const existing = await this.deps.nativeItems.get(catalogId,
|
|
1018
|
+
const existing = await this.deps.nativeItems.get(catalogId, storageDocId);
|
|
752
1019
|
const actorId = this.resolveActorId(context);
|
|
753
1020
|
await this.deps.nativeItems.upsert(catalogId, {
|
|
754
|
-
itemId,
|
|
1021
|
+
itemId: logicalItemId,
|
|
755
1022
|
catalogId,
|
|
1023
|
+
...(storedScope.kind !== "global" ? { scope: storedScope } : {}),
|
|
756
1024
|
appScopedOwnerId: context.appId,
|
|
757
1025
|
...(indexed != null ? { indexed } : {}),
|
|
758
1026
|
data,
|
|
@@ -762,8 +1030,17 @@ export class Catalox {
|
|
|
762
1030
|
...(existing?.createdBy ? { createdBy: existing.createdBy } : actorId ? { createdBy: actorId } : {}),
|
|
763
1031
|
...(actorId ? { updatedBy: actorId } : {}),
|
|
764
1032
|
});
|
|
1033
|
+
const persisted = (await this.deps.nativeItems.get(catalogId, storageDocId));
|
|
1034
|
+
await this.emitNativeItemHistory(context, {
|
|
1035
|
+
catalogId,
|
|
1036
|
+
itemId: String(logicalItemId),
|
|
1037
|
+
storageDocId,
|
|
1038
|
+
op: "update",
|
|
1039
|
+
before: existing ?? null,
|
|
1040
|
+
after: persisted,
|
|
1041
|
+
});
|
|
765
1042
|
return {
|
|
766
|
-
itemId,
|
|
1043
|
+
itemId: logicalItemId,
|
|
767
1044
|
catalogId,
|
|
768
1045
|
appId: context.appId,
|
|
769
1046
|
sourceMode: "native",
|
|
@@ -780,13 +1057,21 @@ export class Catalox {
|
|
|
780
1057
|
throw new CatalogAdapterError({ catalogId, reason: "missing_descriptor" });
|
|
781
1058
|
const now = new Date().toISOString();
|
|
782
1059
|
const actorId = this.resolveActorId(context);
|
|
783
|
-
const records = items.map((
|
|
784
|
-
const stripped = this.stripReservedWriteFields(
|
|
785
|
-
const
|
|
786
|
-
|
|
1060
|
+
const records = items.map((row) => {
|
|
1061
|
+
const stripped = this.stripReservedWriteFields(row);
|
|
1062
|
+
const storedScope = normalizeStoredScope(stripped.scope ?? { kind: "global" });
|
|
1063
|
+
assertSuperAdminForNonGlobalScope(context.superAdmin, storedScope);
|
|
1064
|
+
const derived = this.deriveIndexed(descriptor.descriptor, stripped.data);
|
|
1065
|
+
const scopeIdx = indexedScopeFields(storedScope);
|
|
1066
|
+
const indexed = derived != null ? { ...derived, ...scopeIdx } : (Object.keys(scopeIdx).length ? scopeIdx : undefined);
|
|
1067
|
+
const logicalItemId = resolveCatalogItemId({
|
|
1068
|
+
identity: descriptor.descriptor.identity,
|
|
1069
|
+
data: stripped.data,
|
|
1070
|
+
});
|
|
787
1071
|
return {
|
|
788
|
-
itemId,
|
|
1072
|
+
itemId: logicalItemId,
|
|
789
1073
|
catalogId,
|
|
1074
|
+
...(storedScope.kind !== "global" ? { scope: storedScope } : {}),
|
|
790
1075
|
appScopedOwnerId: context.appId,
|
|
791
1076
|
...(indexed != null ? { indexed } : {}),
|
|
792
1077
|
data: stripped.data,
|
|
@@ -795,7 +1080,25 @@ export class Catalox {
|
|
|
795
1080
|
...(actorId ? { updatedBy: actorId } : {}),
|
|
796
1081
|
};
|
|
797
1082
|
});
|
|
1083
|
+
const beforeRows = [];
|
|
1084
|
+
for (const r of records) {
|
|
1085
|
+
const docId = storageDocIdForNativeRecord(r);
|
|
1086
|
+
const ex = await this.deps.nativeItems.get(catalogId, docId);
|
|
1087
|
+
beforeRows.push({ docId, rec: ex });
|
|
1088
|
+
}
|
|
798
1089
|
await this.deps.nativeItems.batchUpsert(catalogId, records);
|
|
1090
|
+
for (let i = 0; i < records.length; i++) {
|
|
1091
|
+
const { docId, rec: before } = beforeRows[i];
|
|
1092
|
+
const after = (await this.deps.nativeItems.get(catalogId, docId));
|
|
1093
|
+
await this.emitNativeItemHistory(context, {
|
|
1094
|
+
catalogId,
|
|
1095
|
+
itemId: String(after.itemId),
|
|
1096
|
+
storageDocId: docId,
|
|
1097
|
+
op: "update",
|
|
1098
|
+
before,
|
|
1099
|
+
after,
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
799
1102
|
}
|
|
800
1103
|
importCatalogItemsFromJson(json) {
|
|
801
1104
|
return parseJson(json);
|
|
@@ -1153,6 +1456,139 @@ export class Catalox {
|
|
|
1153
1456
|
firestore: this.deps.firestoreStore.firestore,
|
|
1154
1457
|
});
|
|
1155
1458
|
}
|
|
1459
|
+
async listCatalogItemHistory(context, catalogId, input) {
|
|
1460
|
+
await this.deps.authz.requireBindingAccess(context, context.appId, catalogId, "read");
|
|
1461
|
+
const rh = this.deps.recordHistory;
|
|
1462
|
+
if (!rh)
|
|
1463
|
+
return { events: [] };
|
|
1464
|
+
return rh.store.listByCatalog(catalogId, input);
|
|
1465
|
+
}
|
|
1466
|
+
async getCatalogItemHistoryEvent(context, eventId) {
|
|
1467
|
+
const rh = this.deps.recordHistory;
|
|
1468
|
+
if (!rh)
|
|
1469
|
+
return null;
|
|
1470
|
+
const row = await rh.store.get(eventId);
|
|
1471
|
+
if (!row)
|
|
1472
|
+
return null;
|
|
1473
|
+
await this.deps.authz.requireBindingAccess(context, context.appId, row.catalogId, "read");
|
|
1474
|
+
const payload = await readRecordHistoryEventPayload(rh, row);
|
|
1475
|
+
return { index: row, payload };
|
|
1476
|
+
}
|
|
1477
|
+
async restoreCatalogItemFromHistory(context, eventId, input) {
|
|
1478
|
+
const rh = this.deps.recordHistory;
|
|
1479
|
+
if (!rh)
|
|
1480
|
+
throw new Error("recordHistory is not configured on Catalox");
|
|
1481
|
+
const row = await rh.store.get(eventId);
|
|
1482
|
+
if (!row)
|
|
1483
|
+
throw new Error(`catalogItemHistory event not found: ${eventId}`);
|
|
1484
|
+
await this.deps.authz.requireBindingAccess(context, context.appId, row.catalogId, "write");
|
|
1485
|
+
const payload = await readRecordHistoryEventPayload(rh, row);
|
|
1486
|
+
const rec = input.mode === "before" ? payload.before ?? undefined : payload.after ?? undefined;
|
|
1487
|
+
if (!rec)
|
|
1488
|
+
throw new CatalogNotFoundError({ catalogId: row.catalogId, itemId: row.itemId });
|
|
1489
|
+
const docId = storageDocIdForNativeRecord(rec);
|
|
1490
|
+
const liveBefore = await this.deps.nativeItems.get(row.catalogId, docId);
|
|
1491
|
+
await this.deps.nativeItems.upsert(row.catalogId, rec);
|
|
1492
|
+
const liveAfter = (await this.deps.nativeItems.get(row.catalogId, docId));
|
|
1493
|
+
await this.emitNativeItemHistory(context, {
|
|
1494
|
+
catalogId: row.catalogId,
|
|
1495
|
+
itemId: String(rec.itemId),
|
|
1496
|
+
storageDocId: docId,
|
|
1497
|
+
op: "restore",
|
|
1498
|
+
before: liveBefore ?? null,
|
|
1499
|
+
after: liveAfter,
|
|
1500
|
+
});
|
|
1501
|
+
return this.decorateItem(row.catalogId, {
|
|
1502
|
+
itemId: rec.itemId,
|
|
1503
|
+
catalogId: row.catalogId,
|
|
1504
|
+
appId: context.appId,
|
|
1505
|
+
sourceMode: "native",
|
|
1506
|
+
sourceType: "firebase",
|
|
1507
|
+
data: liveAfter.data,
|
|
1508
|
+
createdAt: liveAfter.createdAt,
|
|
1509
|
+
updatedAt: liveAfter.updatedAt,
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
async replayCatalogToPointInTime(context, catalogId, input) {
|
|
1513
|
+
await this.deps.authz.requireBindingAccess(context, context.appId, catalogId, "write");
|
|
1514
|
+
const rh = this.deps.recordHistory;
|
|
1515
|
+
if (!rh)
|
|
1516
|
+
throw new Error("recordHistory is not configured on Catalox");
|
|
1517
|
+
const rows = await rh.store.listByCatalogChronologicalUntil(catalogId, input.asOf);
|
|
1518
|
+
const state = new Map();
|
|
1519
|
+
for (const idx of rows) {
|
|
1520
|
+
const full = await readRecordHistoryEventPayload(rh, idx);
|
|
1521
|
+
const key = String(full.storageDocId ?? full.itemId);
|
|
1522
|
+
if (full.op === "catalog_delete_bulk") {
|
|
1523
|
+
state.clear();
|
|
1524
|
+
continue;
|
|
1525
|
+
}
|
|
1526
|
+
if (full.op === "catalog_rename")
|
|
1527
|
+
continue;
|
|
1528
|
+
if (full.op === "delete") {
|
|
1529
|
+
state.delete(key);
|
|
1530
|
+
continue;
|
|
1531
|
+
}
|
|
1532
|
+
if (full.after)
|
|
1533
|
+
state.set(key, full.after);
|
|
1534
|
+
else
|
|
1535
|
+
state.delete(key);
|
|
1536
|
+
}
|
|
1537
|
+
const live = [];
|
|
1538
|
+
let off = 0;
|
|
1539
|
+
const page = 400;
|
|
1540
|
+
while (true) {
|
|
1541
|
+
const chunk = await this.deps.nativeItems.list(catalogId, { limit: page, offset: off });
|
|
1542
|
+
if (!chunk.length)
|
|
1543
|
+
break;
|
|
1544
|
+
live.push(...chunk);
|
|
1545
|
+
off += chunk.length;
|
|
1546
|
+
if (chunk.length < page)
|
|
1547
|
+
break;
|
|
1548
|
+
}
|
|
1549
|
+
let upserted = 0;
|
|
1550
|
+
let deleted = 0;
|
|
1551
|
+
for (const rec of state.values()) {
|
|
1552
|
+
await this.deps.nativeItems.upsert(catalogId, rec);
|
|
1553
|
+
upserted += 1;
|
|
1554
|
+
}
|
|
1555
|
+
for (const r of live) {
|
|
1556
|
+
const k = storageDocIdForNativeRecord(r);
|
|
1557
|
+
if (!state.has(k)) {
|
|
1558
|
+
await this.deps.nativeItems.delete(catalogId, k);
|
|
1559
|
+
deleted += 1;
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
return { upserted, deleted };
|
|
1563
|
+
}
|
|
1564
|
+
async deleteCatalog(context, catalogId, input) {
|
|
1565
|
+
await this.deps.authz.requireBindingAccess(context, context.appId, catalogId, "admin");
|
|
1566
|
+
return runDeleteCatalog(this.catalogLifecycleDeps(), this.deps.recordHistory, context, catalogId, input);
|
|
1567
|
+
}
|
|
1568
|
+
async restoreDeletedCatalog(context, input) {
|
|
1569
|
+
if (!context.superAdmin) {
|
|
1570
|
+
throw new CatalogAccessDeniedError({ reason: "super_admin_required" });
|
|
1571
|
+
}
|
|
1572
|
+
const rh = this.deps.recordHistory;
|
|
1573
|
+
if (!rh) {
|
|
1574
|
+
return {
|
|
1575
|
+
ok: false,
|
|
1576
|
+
catalogId: "",
|
|
1577
|
+
issues: [{ code: "record_history_required", message: "recordHistory is not configured on Catalox" }],
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
return runRestoreDeletedCatalog(this.catalogLifecycleDeps(), rh, input);
|
|
1581
|
+
}
|
|
1582
|
+
async renameCatalog(context, fromCatalogId, toCatalogId, input = {}) {
|
|
1583
|
+
await this.deps.authz.requireBindingAccess(context, context.appId, fromCatalogId, "admin");
|
|
1584
|
+
return runRenameCatalog(this.catalogLifecycleDeps(), this.deps.recordHistory, context, fromCatalogId, toCatalogId, input);
|
|
1585
|
+
}
|
|
1586
|
+
/**
|
|
1587
|
+
* Fix {@link CataloxContext} for subsequent calls so embedders omit it on each method (no globals).
|
|
1588
|
+
*/
|
|
1589
|
+
withContext(context) {
|
|
1590
|
+
return new CataloxBound(this, context);
|
|
1591
|
+
}
|
|
1156
1592
|
}
|
|
1157
1593
|
// (moved to `src/contracts/bootstrap.ts`)
|
|
1158
1594
|
//# sourceMappingURL=catalox.js.map
|