@x12i/catalox 3.4.0 → 3.4.6
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 +69 -2
- package/dist/src/adapters/api/api-adapter.d.ts.map +1 -1
- package/dist/src/adapters/api/api-adapter.js +3 -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 +3 -0
- package/dist/src/adapters/mongo/mongo-adapter.js.map +1 -1
- package/dist/src/catalox/ai-functions-loader.d.ts +6 -0
- package/dist/src/catalox/ai-functions-loader.d.ts.map +1 -0
- package/dist/src/catalox/ai-functions-loader.js +19 -0
- package/dist/src/catalox/ai-functions-loader.js.map +1 -0
- package/dist/src/catalox/catalog-discovery.d.ts +32 -0
- package/dist/src/catalox/catalog-discovery.d.ts.map +1 -0
- package/dist/src/catalox/catalog-discovery.js +150 -0
- package/dist/src/catalox/catalog-discovery.js.map +1 -0
- package/dist/src/catalox/catalog-lifecycle.d.ts.map +1 -1
- package/dist/src/catalox/catalog-lifecycle.js +15 -0
- package/dist/src/catalox/catalog-lifecycle.js.map +1 -1
- package/dist/src/catalox/catalox-bound.d.ts +8 -1
- package/dist/src/catalox/catalox-bound.d.ts.map +1 -1
- package/dist/src/catalox/catalox-bound.js +21 -0
- package/dist/src/catalox/catalox-bound.js.map +1 -1
- package/dist/src/catalox/catalox.d.ts +13 -1
- package/dist/src/catalox/catalox.d.ts.map +1 -1
- package/dist/src/catalox/catalox.js +329 -37
- package/dist/src/catalox/catalox.js.map +1 -1
- package/dist/src/contracts/context.d.ts +8 -1
- package/dist/src/contracts/context.d.ts.map +1 -1
- package/dist/src/contracts/descriptor-rules.d.ts +76 -0
- package/dist/src/contracts/descriptor-rules.d.ts.map +1 -0
- package/dist/src/contracts/descriptor-rules.js +2 -0
- package/dist/src/contracts/descriptor-rules.js.map +1 -0
- package/dist/src/contracts/descriptors.d.ts +5 -0
- package/dist/src/contracts/descriptors.d.ts.map +1 -1
- package/dist/src/contracts/discovery.d.ts +70 -0
- package/dist/src/contracts/discovery.d.ts.map +1 -1
- package/dist/src/contracts/index.d.ts +3 -1
- package/dist/src/contracts/index.d.ts.map +1 -1
- package/dist/src/contracts/index.js.map +1 -1
- package/dist/src/contracts/search.d.ts +127 -0
- package/dist/src/contracts/search.d.ts.map +1 -0
- package/dist/src/contracts/search.js +2 -0
- package/dist/src/contracts/search.js.map +1 -0
- package/docs/catalox-ui-contract.md +6 -1
- package/package.json +2 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { compactCatalogFilter } from "../contracts/catalogs.js";
|
|
2
|
-
import { CatalogAccessDeniedError, CatalogAdapterError, CatalogNotFoundError } from "../contracts/errors.js";
|
|
2
|
+
import { CatalogAccessDeniedError, CatalogAdapterError, CatalogBindingError, CatalogNotFoundError, } 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";
|
|
@@ -28,6 +28,8 @@ import { SnapshotStore } from "../firebase/snapshot-store.js";
|
|
|
28
28
|
import { StoreAppBindingStore } from "../firebase/store-app-binding-store.js";
|
|
29
29
|
import { IdentityBindingStore } from "../firebase/identity-binding-store.js";
|
|
30
30
|
import { nativeItemsCollectionId } from "../firebase/catalog-data-paths.js";
|
|
31
|
+
import { applyCatalogVisibilityFilter, filterCatalogsByText, findCatalogsByAi as findCatalogsByAiHelper, summarizeCatalog, } from "./catalog-discovery.js";
|
|
32
|
+
import { loadAiFunctions } from "./ai-functions-loader.js";
|
|
31
33
|
import { runBackupData, pruneGcsBackupRuns } from "./backup-data.js";
|
|
32
34
|
import { runUndoFirestoreRestore } from "./restore-firestore-backup.js";
|
|
33
35
|
import { runRestoreFirestoreBackupFromGcs, runDeleteCataloxGcsBackupRun } from "./restore-firestore-backup-from-gcs.js";
|
|
@@ -108,8 +110,41 @@ export class Catalox {
|
|
|
108
110
|
if (active.length)
|
|
109
111
|
return { storeId, appIds: active };
|
|
110
112
|
}
|
|
113
|
+
if (!context.appId) {
|
|
114
|
+
throw new CatalogBindingError({
|
|
115
|
+
reason: "missing_appId",
|
|
116
|
+
message: "App id is required unless storeId/appIds are provided.",
|
|
117
|
+
});
|
|
118
|
+
}
|
|
111
119
|
return { ...(storeId ? { storeId } : {}), appIds: [context.appId] };
|
|
112
120
|
}
|
|
121
|
+
async resolveAppIdForCatalogAccess(params) {
|
|
122
|
+
const { context, catalogId, required } = params;
|
|
123
|
+
if (context.appId)
|
|
124
|
+
return context.appId;
|
|
125
|
+
// Super-admin callers may omit appId; access is still evaluated with explicit elevation.
|
|
126
|
+
if (context.superAdmin)
|
|
127
|
+
return "_super";
|
|
128
|
+
// CatalogId-first: infer appId via active bindings if there is exactly one viable binding.
|
|
129
|
+
const bindings = await this.deps.bindings.listByCatalog(catalogId);
|
|
130
|
+
const active = bindings.filter((b) => b.status === "active");
|
|
131
|
+
const viable = active.filter((b) => {
|
|
132
|
+
if (required === "read")
|
|
133
|
+
return Boolean(b.access.canRead);
|
|
134
|
+
if (required === "write")
|
|
135
|
+
return Boolean(b.access.canWrite);
|
|
136
|
+
return Boolean(b.access.canAdmin);
|
|
137
|
+
});
|
|
138
|
+
const distinctApps = [...new Set(viable.map((b) => String(b.appId)))];
|
|
139
|
+
if (distinctApps.length === 1)
|
|
140
|
+
return viable[0].appId;
|
|
141
|
+
throw new CatalogBindingError({
|
|
142
|
+
reason: "cannot_resolve_appId_for_catalog",
|
|
143
|
+
catalogId: String(catalogId),
|
|
144
|
+
required,
|
|
145
|
+
matchingAppIds: distinctApps,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
113
148
|
readPath(obj, path) {
|
|
114
149
|
if (!path)
|
|
115
150
|
return undefined;
|
|
@@ -160,7 +195,12 @@ export class Catalox {
|
|
|
160
195
|
}
|
|
161
196
|
if (source.type === "catalog") {
|
|
162
197
|
// Enforce authz: safest default is to require binding (unless god-mode).
|
|
163
|
-
await this.
|
|
198
|
+
const appId = await this.resolveAppIdForCatalogAccess({
|
|
199
|
+
context: params.context,
|
|
200
|
+
catalogId: source.catalogId,
|
|
201
|
+
required: "read",
|
|
202
|
+
});
|
|
203
|
+
await this.deps.authz.requireBindingAccess(params.context, appId, source.catalogId, "read");
|
|
164
204
|
const filterEq = resolveFilterBy(source.filterBy, params.currentItemData, (o, p) => this.readPath(o, p));
|
|
165
205
|
const sort = source.sortBy
|
|
166
206
|
? { [source.sortBy.field]: source.sortBy.direction === "asc" ? 1 : -1 }
|
|
@@ -419,6 +459,9 @@ export class Catalox {
|
|
|
419
459
|
}
|
|
420
460
|
async listAppCatalogs(context, input) {
|
|
421
461
|
const appId = input?.appId ?? context.appId;
|
|
462
|
+
if (!appId) {
|
|
463
|
+
throw new CatalogBindingError({ reason: "missing_appId", message: "listAppCatalogs requires an appId." });
|
|
464
|
+
}
|
|
422
465
|
const bindings = await this.deps.bindings.listByApp(appId);
|
|
423
466
|
const catalogs = await this.deps.catalogs.listAll();
|
|
424
467
|
const byId = new Map(catalogs.map((c) => [c.catalogId, c]));
|
|
@@ -517,13 +560,42 @@ export class Catalox {
|
|
|
517
560
|
}
|
|
518
561
|
return out.filter((e) => (input.includeHidden ? true : e.visibility !== "hidden"));
|
|
519
562
|
}
|
|
563
|
+
/**
|
|
564
|
+
* App-agnostic discovery: list catalog metadata across all apps.
|
|
565
|
+
* Visibility default: hide hidden catalogs unless `context.superAdmin`.
|
|
566
|
+
*/
|
|
567
|
+
async listAllCatalogs(context, input) {
|
|
568
|
+
const catalogs = await this.deps.catalogs.listAll();
|
|
569
|
+
const out = [];
|
|
570
|
+
for (const c of catalogs) {
|
|
571
|
+
const d = await this.deps.descriptors.get(c.catalogId);
|
|
572
|
+
out.push(summarizeCatalog(c, d));
|
|
573
|
+
}
|
|
574
|
+
return applyCatalogVisibilityFilter(out, context, input);
|
|
575
|
+
}
|
|
576
|
+
async findCatalogs(context, input) {
|
|
577
|
+
const all = await this.listAllCatalogs(context, {
|
|
578
|
+
...(input.includeDisabled != null ? { includeDisabled: input.includeDisabled } : {}),
|
|
579
|
+
...(input.includeHidden != null ? { includeHidden: input.includeHidden } : {}),
|
|
580
|
+
});
|
|
581
|
+
return filterCatalogsByText(all, input);
|
|
582
|
+
}
|
|
583
|
+
async findCatalogsByAi(context, input) {
|
|
584
|
+
const all = await this.listAllCatalogs(context, {
|
|
585
|
+
...(input.includeDisabled != null ? { includeDisabled: input.includeDisabled } : {}),
|
|
586
|
+
...(input.includeHidden != null ? { includeHidden: input.includeHidden } : {}),
|
|
587
|
+
});
|
|
588
|
+
return findCatalogsByAiHelper({ query: input.query, catalogs: all, input });
|
|
589
|
+
}
|
|
520
590
|
async getCatalogDescriptor(context, catalogId) {
|
|
521
|
-
await this.
|
|
591
|
+
const appId = await this.resolveAppIdForCatalogAccess({ context, catalogId, required: "read" });
|
|
592
|
+
await this.deps.authz.requireBindingAccess(context, appId, catalogId, "read");
|
|
522
593
|
const rec = await this.deps.descriptors.get(catalogId);
|
|
523
594
|
return rec?.descriptor ?? null;
|
|
524
595
|
}
|
|
525
596
|
async getCatalogRendererSnippet(context, catalogId, role, mode) {
|
|
526
|
-
await this.
|
|
597
|
+
const appId = await this.resolveAppIdForCatalogAccess({ context, catalogId, required: "read" });
|
|
598
|
+
await this.deps.authz.requireBindingAccess(context, appId, catalogId, "read");
|
|
527
599
|
if (!this.deps.rendererSnippets) {
|
|
528
600
|
throw new Error("rendererSnippets dependency is not configured");
|
|
529
601
|
}
|
|
@@ -531,6 +603,9 @@ export class Catalox {
|
|
|
531
603
|
}
|
|
532
604
|
async getAppCatalogBootstrap(context, appId) {
|
|
533
605
|
const resolved = appId ?? context.appId;
|
|
606
|
+
if (!resolved) {
|
|
607
|
+
throw new CatalogBindingError({ reason: "missing_appId", message: "getAppCatalogBootstrap requires an appId." });
|
|
608
|
+
}
|
|
534
609
|
const entries = await this.listAppCatalogs(context, { appId: resolved });
|
|
535
610
|
const descriptors = [];
|
|
536
611
|
for (const e of entries) {
|
|
@@ -553,7 +628,9 @@ export class Catalox {
|
|
|
553
628
|
return out;
|
|
554
629
|
}
|
|
555
630
|
async listCatalogItems(context, catalogId, options) {
|
|
556
|
-
await this.
|
|
631
|
+
const appId = await this.resolveAppIdForCatalogAccess({ context, catalogId, required: "read" });
|
|
632
|
+
await this.deps.authz.requireBindingAccess(context, appId, catalogId, "read");
|
|
633
|
+
const appCtx = context.appId ? context : { ...context, appId };
|
|
557
634
|
const catalog = await this.deps.catalogs.get(catalogId);
|
|
558
635
|
if (!catalog)
|
|
559
636
|
throw new CatalogNotFoundError({ catalogId });
|
|
@@ -590,7 +667,7 @@ export class Catalox {
|
|
|
590
667
|
else {
|
|
591
668
|
physical = await this.fetchScopedPhysicalRows(catalogId, rawScope, baseListOpts);
|
|
592
669
|
}
|
|
593
|
-
const merged = mergeNativePhysicalRows(physical, descRec.descriptor.identity, String(
|
|
670
|
+
const merged = mergeNativePhysicalRows(physical, descRec.descriptor.identity, String(appId), wantsSuperList);
|
|
594
671
|
const off = listQueryOptions?.offset ?? 0;
|
|
595
672
|
const lim = listQueryOptions?.limit ?? 100;
|
|
596
673
|
const sliced = merged.slice(off, off + lim);
|
|
@@ -623,7 +700,7 @@ export class Catalox {
|
|
|
623
700
|
const adapterConfig = await this.deps.adapters.get(def.adapterId);
|
|
624
701
|
if (!adapterConfig)
|
|
625
702
|
throw new CatalogAdapterError({ catalogId, reason: "missing_adapter" });
|
|
626
|
-
const result = await this.deps.mongoAdapter.listItems(
|
|
703
|
+
const result = await this.deps.mongoAdapter.listItems(appCtx, catalogId, adapterConfig, { mapping: mapping.mapping, ...(mapping.options ? { options: mapping.options } : {}) }, mappedListQueryOptions);
|
|
627
704
|
return result.issues?.length
|
|
628
705
|
? { listOutcome: "ok", items: result.items, issues: result.issues }
|
|
629
706
|
: { listOutcome: "ok", items: result.items };
|
|
@@ -634,7 +711,7 @@ export class Catalox {
|
|
|
634
711
|
const adapterConfig = await this.deps.adapters.get(def.adapterId);
|
|
635
712
|
if (!adapterConfig)
|
|
636
713
|
throw new CatalogAdapterError({ catalogId, reason: "missing_adapter" });
|
|
637
|
-
const result = await this.deps.apiAdapter.listItems(
|
|
714
|
+
const result = await this.deps.apiAdapter.listItems(appCtx, catalogId, adapterConfig, { responseMapping: mapping.mapping, ...(mapping.options ? { options: mapping.options } : {}) }, mappedListQueryOptions);
|
|
638
715
|
return {
|
|
639
716
|
listOutcome: "ok",
|
|
640
717
|
items: result.items,
|
|
@@ -668,7 +745,8 @@ export class Catalox {
|
|
|
668
745
|
}
|
|
669
746
|
}
|
|
670
747
|
async getCatalogItem(context, catalogId, itemId, options) {
|
|
671
|
-
await this.
|
|
748
|
+
const appId = await this.resolveAppIdForCatalogAccess({ context, catalogId, required: "read" });
|
|
749
|
+
await this.deps.authz.requireBindingAccess(context, appId, catalogId, "read");
|
|
672
750
|
const catalog = await this.deps.catalogs.get(catalogId);
|
|
673
751
|
if (!catalog)
|
|
674
752
|
throw new CatalogNotFoundError({ catalogId });
|
|
@@ -684,7 +762,7 @@ export class Catalox {
|
|
|
684
762
|
const base = {
|
|
685
763
|
itemId: rec.itemId,
|
|
686
764
|
catalogId: rec.catalogId,
|
|
687
|
-
appId
|
|
765
|
+
appId,
|
|
688
766
|
sourceMode: "native",
|
|
689
767
|
sourceType: "firebase",
|
|
690
768
|
data: rec.data,
|
|
@@ -709,7 +787,7 @@ export class Catalox {
|
|
|
709
787
|
const filtered = scope?.accountId || scope?.groupId || scope?.userId || scope?.channelId || scope?.visitorId || scope?.domainId || scope?.agentId
|
|
710
788
|
? filterPhysicalForTenantFetch(rows, scope)
|
|
711
789
|
: rows.filter((r) => isGlobalPhysicalRow(r));
|
|
712
|
-
const merged = mergeNativePhysicalRows(filtered, descRec.descriptor.identity, String(
|
|
790
|
+
const merged = mergeNativePhysicalRows(filtered, descRec.descriptor.identity, String(appId), false);
|
|
713
791
|
const hit = merged.find((m) => m.itemId === itemId) ?? merged[0];
|
|
714
792
|
if (!hit)
|
|
715
793
|
return { outcome: "not_found" };
|
|
@@ -755,18 +833,23 @@ export class Catalox {
|
|
|
755
833
|
return { isValid: true, issues: [] };
|
|
756
834
|
}
|
|
757
835
|
async getCatalogItemReferences(context, catalogId, itemId) {
|
|
758
|
-
await this.
|
|
836
|
+
const appId = await this.resolveAppIdForCatalogAccess({ context, catalogId, required: "read" });
|
|
837
|
+
await this.deps.authz.requireBindingAccess(context, appId, catalogId, "read");
|
|
759
838
|
const refs = await this.deps.references.listByItem(catalogId, itemId);
|
|
760
839
|
return refs;
|
|
761
840
|
}
|
|
762
841
|
async listCatalogReferences(context, catalogId) {
|
|
763
|
-
await this.
|
|
842
|
+
const appId = await this.resolveAppIdForCatalogAccess({ context, catalogId, required: "read" });
|
|
843
|
+
await this.deps.authz.requireBindingAccess(context, appId, catalogId, "read");
|
|
764
844
|
const refs = await this.deps.references.listByCatalog(catalogId);
|
|
765
845
|
return refs;
|
|
766
846
|
}
|
|
767
847
|
// Spec methods below are stubs for now; filled in by later todos.
|
|
768
848
|
async getApp(_context, appId) {
|
|
769
|
-
|
|
849
|
+
const resolved = appId ?? _context.appId;
|
|
850
|
+
if (!resolved)
|
|
851
|
+
throw new CatalogBindingError({ reason: "missing_appId" });
|
|
852
|
+
return this.deps.apps.get(resolved);
|
|
770
853
|
}
|
|
771
854
|
async createCatalog(_context, _input) {
|
|
772
855
|
const now = new Date().toISOString();
|
|
@@ -891,6 +974,199 @@ export class Catalox {
|
|
|
891
974
|
async listCatalogs(_context, _options) {
|
|
892
975
|
return this.deps.catalogs.listAll();
|
|
893
976
|
}
|
|
977
|
+
async searchCatalogItems(context, catalogId, input) {
|
|
978
|
+
const poolLimit = input.poolLimit ?? 200;
|
|
979
|
+
const limit = input.limit ?? 50;
|
|
980
|
+
const base = await this.listCatalogItems(context, catalogId, {
|
|
981
|
+
limit: poolLimit,
|
|
982
|
+
...(input.scope ? { scope: input.scope } : {}),
|
|
983
|
+
...(input.filter ? { filter: input.filter } : {}),
|
|
984
|
+
...(input.sort ? { sort: input.sort } : {}),
|
|
985
|
+
});
|
|
986
|
+
if (base.listOutcome !== "ok")
|
|
987
|
+
return base;
|
|
988
|
+
const q = String(input.text ?? "").toLowerCase().trim();
|
|
989
|
+
if (!q)
|
|
990
|
+
return { ...base, items: [] };
|
|
991
|
+
const textFields = input.textFields ?? [];
|
|
992
|
+
const filtered = base.items.filter((it) => {
|
|
993
|
+
const parts = [];
|
|
994
|
+
if (it.title)
|
|
995
|
+
parts.push(String(it.title));
|
|
996
|
+
if (it.subtitle)
|
|
997
|
+
parts.push(String(it.subtitle));
|
|
998
|
+
const data = it.data;
|
|
999
|
+
for (const p of textFields) {
|
|
1000
|
+
const v = this.readPath(data, p);
|
|
1001
|
+
if (v == null)
|
|
1002
|
+
continue;
|
|
1003
|
+
parts.push(typeof v === "string" || typeof v === "number" ? String(v) : JSON.stringify(v));
|
|
1004
|
+
}
|
|
1005
|
+
return parts.join(" ").toLowerCase().includes(q);
|
|
1006
|
+
});
|
|
1007
|
+
return { ...base, items: filtered.slice(0, limit) };
|
|
1008
|
+
}
|
|
1009
|
+
async findCatalogItemsByAi(context, catalogId, input) {
|
|
1010
|
+
const poolLimit = input.poolLimit ?? 300;
|
|
1011
|
+
const base = await this.listCatalogItems(context, catalogId, {
|
|
1012
|
+
limit: poolLimit,
|
|
1013
|
+
...(input.scope ? { scope: input.scope } : {}),
|
|
1014
|
+
...(input.filter ? { filter: input.filter } : {}),
|
|
1015
|
+
...(input.sort ? { sort: input.sort } : {}),
|
|
1016
|
+
});
|
|
1017
|
+
if (base.listOutcome !== "ok")
|
|
1018
|
+
return { query: input.query, hits: [], noMatch: true };
|
|
1019
|
+
const items = base.items;
|
|
1020
|
+
const candidates = items.map((it) => {
|
|
1021
|
+
const title = it.title ? String(it.title) : "";
|
|
1022
|
+
const subtitle = it.subtitle ? String(it.subtitle) : "";
|
|
1023
|
+
const label = [title, subtitle].filter(Boolean).join(" — ") || String(it.itemId);
|
|
1024
|
+
return {
|
|
1025
|
+
id: String(it.itemId),
|
|
1026
|
+
label,
|
|
1027
|
+
metadata: { catalogId: String(catalogId) },
|
|
1028
|
+
};
|
|
1029
|
+
});
|
|
1030
|
+
const { match } = await loadAiFunctions();
|
|
1031
|
+
const res = await match({
|
|
1032
|
+
query: input.query,
|
|
1033
|
+
candidates,
|
|
1034
|
+
...(input.guidance ? { guidance: input.guidance } : {}),
|
|
1035
|
+
...(input.maxResults != null ? { maxResults: input.maxResults } : {}),
|
|
1036
|
+
...(input.minScore != null ? { minScore: input.minScore } : {}),
|
|
1037
|
+
...(input.allowNoMatch != null ? { allowNoMatch: input.allowNoMatch } : {}),
|
|
1038
|
+
...(input.returnReasons != null ? { returnReasons: input.returnReasons } : {}),
|
|
1039
|
+
...(input.additionalInstructions ? { additionalInstructions: input.additionalInstructions } : {}),
|
|
1040
|
+
...(input.maxCandidates != null ? { maxCandidates: input.maxCandidates } : {}),
|
|
1041
|
+
...(input.mode ? { mode: input.mode } : {}),
|
|
1042
|
+
...(input.client ? { client: input.client } : {}),
|
|
1043
|
+
...(input.model ? { model: input.model } : {}),
|
|
1044
|
+
...(input.temperature != null ? { temperature: input.temperature } : {}),
|
|
1045
|
+
...(input.maxTokens != null ? { maxTokens: input.maxTokens } : {}),
|
|
1046
|
+
...(input.timeoutMs != null ? { timeoutMs: input.timeoutMs } : {}),
|
|
1047
|
+
...(input.vendor != null ? { vendor: input.vendor } : {}),
|
|
1048
|
+
});
|
|
1049
|
+
const byId = new Map(items.map((it) => [String(it.itemId), it]));
|
|
1050
|
+
const hits = res.matches
|
|
1051
|
+
.map((m) => {
|
|
1052
|
+
const it = byId.get(String(m.id));
|
|
1053
|
+
if (!it)
|
|
1054
|
+
return null;
|
|
1055
|
+
return { itemId: it.itemId, score: m.score, ...(m.reason ? { reason: m.reason } : {}), item: it };
|
|
1056
|
+
})
|
|
1057
|
+
.filter(Boolean);
|
|
1058
|
+
return { query: input.query, hits, noMatch: Boolean(res.noMatch) };
|
|
1059
|
+
}
|
|
1060
|
+
async createCatalogItemByAi(context, catalogId, input) {
|
|
1061
|
+
const appId = await this.resolveAppIdForCatalogAccess({ context, catalogId, required: "write" });
|
|
1062
|
+
await this.deps.authz.requireBindingAccess(context, appId, catalogId, "write");
|
|
1063
|
+
const descRec = await this.deps.descriptors.get(catalogId);
|
|
1064
|
+
if (!descRec)
|
|
1065
|
+
throw new CatalogAdapterError({ catalogId, reason: "missing_descriptor" });
|
|
1066
|
+
const rules = descRec.descriptor.creationRules;
|
|
1067
|
+
if (!rules)
|
|
1068
|
+
throw new CatalogAdapterError({ catalogId, reason: "creation_rules_missing" });
|
|
1069
|
+
if (rules.enabled === false)
|
|
1070
|
+
throw new CatalogAdapterError({ catalogId, reason: "creation_rules_disabled" });
|
|
1071
|
+
const catalog = await this.deps.catalogs.get(catalogId);
|
|
1072
|
+
if (!catalog)
|
|
1073
|
+
throw new CatalogNotFoundError({ catalogId });
|
|
1074
|
+
const existingLimit = input.existingItemsSampleLimit ?? 0;
|
|
1075
|
+
const existingItemsSample = existingLimit > 0
|
|
1076
|
+
? (await this.listCatalogItems(context, catalogId, { limit: existingLimit })).items.map((i) => i.data)
|
|
1077
|
+
: undefined;
|
|
1078
|
+
const { createItem } = await loadAiFunctions();
|
|
1079
|
+
const result = await createItem({
|
|
1080
|
+
...(descRec.descriptor.itemLabel ? { itemLabel: descRec.descriptor.itemLabel } : {}),
|
|
1081
|
+
creationRules: rules,
|
|
1082
|
+
provided: input.provided,
|
|
1083
|
+
fieldSchema: (descRec.descriptor.queryableFields ?? []),
|
|
1084
|
+
...(existingItemsSample ? { existingItemsSample } : {}),
|
|
1085
|
+
...(input.additionalInstructions ? { additionalInstructions: input.additionalInstructions } : {}),
|
|
1086
|
+
...(input.mode ? { mode: input.mode } : {}),
|
|
1087
|
+
...(input.model ? { model: input.model } : {}),
|
|
1088
|
+
...(input.client ? { client: input.client } : {}),
|
|
1089
|
+
...(input.temperature != null ? { temperature: input.temperature } : {}),
|
|
1090
|
+
...(input.maxTokens != null ? { maxTokens: input.maxTokens } : {}),
|
|
1091
|
+
...(input.timeoutMs != null ? { timeoutMs: input.timeoutMs } : {}),
|
|
1092
|
+
...(input.vendor != null ? { vendor: input.vendor } : {}),
|
|
1093
|
+
}, undefined);
|
|
1094
|
+
const out = {
|
|
1095
|
+
catalogId,
|
|
1096
|
+
proposed: result.item ?? {},
|
|
1097
|
+
issues: (result.issues ?? []),
|
|
1098
|
+
...(result.missingInputs ? { missingInputs: result.missingInputs } : {}),
|
|
1099
|
+
noCreate: Boolean(result.noCreate),
|
|
1100
|
+
...(result.reason ? { reason: result.reason } : {}),
|
|
1101
|
+
};
|
|
1102
|
+
const autoPersist = input.autoPersist === true;
|
|
1103
|
+
const hasErrors = (result.issues ?? []).some((i) => i?.severity === "error");
|
|
1104
|
+
if (autoPersist && !out.noCreate && !hasErrors) {
|
|
1105
|
+
if (catalog.sourceMode !== "native") {
|
|
1106
|
+
throw new CatalogAdapterError({ catalogId, reason: "autopersist_mapped_unsupported" });
|
|
1107
|
+
}
|
|
1108
|
+
const writePayload = input.scope != null ? { ...out.proposed, scope: input.scope } : out.proposed;
|
|
1109
|
+
out.item = await this.upsertNativeCatalogItem(context, catalogId, writePayload);
|
|
1110
|
+
}
|
|
1111
|
+
return out;
|
|
1112
|
+
}
|
|
1113
|
+
async modifyCatalogItemByAi(context, catalogId, itemId, input) {
|
|
1114
|
+
const appId = await this.resolveAppIdForCatalogAccess({ context, catalogId, required: "write" });
|
|
1115
|
+
await this.deps.authz.requireBindingAccess(context, appId, catalogId, "write");
|
|
1116
|
+
const descRec = await this.deps.descriptors.get(catalogId);
|
|
1117
|
+
if (!descRec)
|
|
1118
|
+
throw new CatalogAdapterError({ catalogId, reason: "missing_descriptor" });
|
|
1119
|
+
const rules = descRec.descriptor.modificationRules;
|
|
1120
|
+
if (!rules)
|
|
1121
|
+
throw new CatalogAdapterError({ catalogId, reason: "modification_rules_missing" });
|
|
1122
|
+
if (rules.enabled === false)
|
|
1123
|
+
throw new CatalogAdapterError({ catalogId, reason: "modification_rules_disabled" });
|
|
1124
|
+
const catalog = await this.deps.catalogs.get(catalogId);
|
|
1125
|
+
if (!catalog)
|
|
1126
|
+
throw new CatalogNotFoundError({ catalogId });
|
|
1127
|
+
const got = await this.getCatalogItem(context, catalogId, itemId, input.nativeItemOptions);
|
|
1128
|
+
if (got.outcome === "mapping_blocked") {
|
|
1129
|
+
throw new CatalogAdapterError({ catalogId, reason: "mapping_validation_failed", issues: got.issues });
|
|
1130
|
+
}
|
|
1131
|
+
if (got.outcome === "not_found")
|
|
1132
|
+
throw new CatalogNotFoundError({ catalogId, itemId });
|
|
1133
|
+
const currentItem = (got.item.data ?? {});
|
|
1134
|
+
const { modifyItem } = await loadAiFunctions();
|
|
1135
|
+
const result = await modifyItem({
|
|
1136
|
+
...(descRec.descriptor.itemLabel ? { itemLabel: descRec.descriptor.itemLabel } : {}),
|
|
1137
|
+
currentItem: currentItem,
|
|
1138
|
+
modificationRules: rules,
|
|
1139
|
+
patch: input.patch,
|
|
1140
|
+
fieldSchema: (descRec.descriptor.queryableFields ?? []),
|
|
1141
|
+
...(input.additionalInstructions ? { additionalInstructions: input.additionalInstructions } : {}),
|
|
1142
|
+
...(input.mode ? { mode: input.mode } : {}),
|
|
1143
|
+
...(input.model ? { model: input.model } : {}),
|
|
1144
|
+
...(input.client ? { client: input.client } : {}),
|
|
1145
|
+
...(input.temperature != null ? { temperature: input.temperature } : {}),
|
|
1146
|
+
...(input.maxTokens != null ? { maxTokens: input.maxTokens } : {}),
|
|
1147
|
+
...(input.timeoutMs != null ? { timeoutMs: input.timeoutMs } : {}),
|
|
1148
|
+
...(input.vendor != null ? { vendor: input.vendor } : {}),
|
|
1149
|
+
}, undefined);
|
|
1150
|
+
const out = {
|
|
1151
|
+
catalogId,
|
|
1152
|
+
itemId,
|
|
1153
|
+
proposed: result.item ?? {},
|
|
1154
|
+
diff: (result.diff ?? []),
|
|
1155
|
+
issues: (result.issues ?? []),
|
|
1156
|
+
...(result.violatedRules ? { violatedRules: result.violatedRules } : {}),
|
|
1157
|
+
noChange: Boolean(result.noChange),
|
|
1158
|
+
...(result.reason ? { reason: result.reason } : {}),
|
|
1159
|
+
};
|
|
1160
|
+
const autoPersist = input.autoPersist === true;
|
|
1161
|
+
const hasErrors = (result.issues ?? []).some((i) => i?.severity === "error");
|
|
1162
|
+
if (autoPersist && !out.noChange && !hasErrors) {
|
|
1163
|
+
if (catalog.sourceMode !== "native") {
|
|
1164
|
+
throw new CatalogAdapterError({ catalogId, reason: "autopersist_mapped_unsupported" });
|
|
1165
|
+
}
|
|
1166
|
+
out.item = await this.updateNativeCatalogItem(context, catalogId, itemId, out.proposed, input.nativeItemOptions);
|
|
1167
|
+
}
|
|
1168
|
+
return out;
|
|
1169
|
+
}
|
|
894
1170
|
async bindCatalogToApp(_context, _input) {
|
|
895
1171
|
const existing = await this.deps.bindings.findByAppCatalog(_input.appId, _input.catalogId);
|
|
896
1172
|
if (existing)
|
|
@@ -926,7 +1202,7 @@ export class Catalox {
|
|
|
926
1202
|
async bindAppToStore(context, input) {
|
|
927
1203
|
if (!this.deps.storeAppBindings)
|
|
928
1204
|
throw new Error("storeAppBindings dependency is not configured");
|
|
929
|
-
if (!context.superAdmin && input.appId !== context.appId) {
|
|
1205
|
+
if (!context.superAdmin && (!context.appId || input.appId !== context.appId)) {
|
|
930
1206
|
throw new CatalogAccessDeniedError({ reason: "not_super_admin" });
|
|
931
1207
|
}
|
|
932
1208
|
const existing = await this.deps.storeAppBindings.findByStoreApp(input.storeId, input.appId);
|
|
@@ -950,7 +1226,7 @@ export class Catalox {
|
|
|
950
1226
|
async unbindAppFromStore(context, storeId, appId) {
|
|
951
1227
|
if (!this.deps.storeAppBindings)
|
|
952
1228
|
throw new Error("storeAppBindings dependency is not configured");
|
|
953
|
-
if (!context.superAdmin && appId !== context.appId) {
|
|
1229
|
+
if (!context.superAdmin && (!context.appId || appId !== context.appId)) {
|
|
954
1230
|
throw new CatalogAccessDeniedError({ reason: "not_super_admin" });
|
|
955
1231
|
}
|
|
956
1232
|
const existing = await this.deps.storeAppBindings.findByStoreApp(storeId, appId);
|
|
@@ -972,10 +1248,15 @@ export class Catalox {
|
|
|
972
1248
|
if (context.superAdmin)
|
|
973
1249
|
return records;
|
|
974
1250
|
// non-god: only reveal memberships that include the caller's own appId
|
|
975
|
-
return records.filter((r) => r.appId === context.appId);
|
|
1251
|
+
return context.appId ? records.filter((r) => r.appId === context.appId) : [];
|
|
976
1252
|
}
|
|
977
1253
|
async ensureCatalog(context, catalog) {
|
|
978
|
-
await this.
|
|
1254
|
+
const appId = await this.resolveAppIdForCatalogAccess({
|
|
1255
|
+
context,
|
|
1256
|
+
catalogId: catalog.catalogId,
|
|
1257
|
+
required: "admin",
|
|
1258
|
+
});
|
|
1259
|
+
await this.deps.authz.requireBindingAccess(context, appId, catalog.catalogId, "admin");
|
|
979
1260
|
const existing = await this.deps.catalogs.get(catalog.catalogId);
|
|
980
1261
|
const now = new Date().toISOString();
|
|
981
1262
|
if (existing)
|
|
@@ -991,7 +1272,7 @@ export class Catalox {
|
|
|
991
1272
|
}
|
|
992
1273
|
async ensureBinding(context, input) {
|
|
993
1274
|
// only super-admin apps can provision cross-app bindings.
|
|
994
|
-
if (!context.superAdmin && input.appId !== context.appId) {
|
|
1275
|
+
if (!context.superAdmin && (!context.appId || input.appId !== context.appId)) {
|
|
995
1276
|
throw new CatalogAccessDeniedError({ reason: "not_super_admin" });
|
|
996
1277
|
}
|
|
997
1278
|
const existing = await this.deps.bindings.findByAppCatalog(input.appId, input.catalogId);
|
|
@@ -1016,7 +1297,8 @@ export class Catalox {
|
|
|
1016
1297
|
return this.upsertNativeCatalogItem(_context, _catalogId, _input);
|
|
1017
1298
|
}
|
|
1018
1299
|
async updateNativeCatalogItem(_context, _catalogId, _itemId, _patch, _options) {
|
|
1019
|
-
await this.
|
|
1300
|
+
const appId = await this.resolveAppIdForCatalogAccess({ context: _context, catalogId: _catalogId, required: "write" });
|
|
1301
|
+
await this.deps.authz.requireBindingAccess(_context, appId, _catalogId, "write");
|
|
1020
1302
|
let existing = null;
|
|
1021
1303
|
if (_options?.storageDocId) {
|
|
1022
1304
|
existing = await this.deps.nativeItems.get(_catalogId, _options.storageDocId);
|
|
@@ -1072,7 +1354,7 @@ export class Catalox {
|
|
|
1072
1354
|
const out = {
|
|
1073
1355
|
itemId: nextRec.itemId,
|
|
1074
1356
|
catalogId: _catalogId,
|
|
1075
|
-
appId
|
|
1357
|
+
appId,
|
|
1076
1358
|
sourceMode: "native",
|
|
1077
1359
|
sourceType: "firebase",
|
|
1078
1360
|
data: mergedData,
|
|
@@ -1082,7 +1364,8 @@ export class Catalox {
|
|
|
1082
1364
|
return this.decorateItem(_catalogId, out);
|
|
1083
1365
|
}
|
|
1084
1366
|
async deleteNativeCatalogItem(_context, _catalogId, _itemId, _options) {
|
|
1085
|
-
await this.
|
|
1367
|
+
const appId = await this.resolveAppIdForCatalogAccess({ context: _context, catalogId: _catalogId, required: "write" });
|
|
1368
|
+
await this.deps.authz.requireBindingAccess(_context, appId, _catalogId, "write");
|
|
1086
1369
|
let docId = _options?.storageDocId;
|
|
1087
1370
|
if (!docId) {
|
|
1088
1371
|
let rows = await this.deps.nativeItems.findByLogicalItemId(_catalogId, _itemId);
|
|
@@ -1113,7 +1396,8 @@ export class Catalox {
|
|
|
1113
1396
|
if (!context.superAdmin) {
|
|
1114
1397
|
throw new CatalogAccessDeniedError({ reason: "super_admin_required" });
|
|
1115
1398
|
}
|
|
1116
|
-
await this.
|
|
1399
|
+
const appId = await this.resolveAppIdForCatalogAccess({ context, catalogId, required: "write" });
|
|
1400
|
+
await this.deps.authz.requireBindingAccess(context, appId, catalogId, "write");
|
|
1117
1401
|
const fromDoc = input.fromStorageDocId ??
|
|
1118
1402
|
encodeNativeItemStorageDocId(String(itemId), normalizeStoredScope(input.fromScope ?? { kind: "global" }));
|
|
1119
1403
|
const rec = await this.deps.nativeItems.get(catalogId, fromDoc);
|
|
@@ -1142,7 +1426,8 @@ export class Catalox {
|
|
|
1142
1426
|
});
|
|
1143
1427
|
}
|
|
1144
1428
|
async upsertNativeCatalogItem(context, catalogId, input) {
|
|
1145
|
-
await this.
|
|
1429
|
+
const appId = await this.resolveAppIdForCatalogAccess({ context, catalogId, required: "write" });
|
|
1430
|
+
await this.deps.authz.requireBindingAccess(context, appId, catalogId, "write");
|
|
1146
1431
|
const descriptor = await this.deps.descriptors.get(catalogId);
|
|
1147
1432
|
if (!descriptor)
|
|
1148
1433
|
throw new CatalogAdapterError({ catalogId, reason: "missing_descriptor" });
|
|
@@ -1161,7 +1446,7 @@ export class Catalox {
|
|
|
1161
1446
|
itemId: logicalItemId,
|
|
1162
1447
|
catalogId,
|
|
1163
1448
|
...(storedScope.kind !== "global" ? { scope: storedScope } : {}),
|
|
1164
|
-
appScopedOwnerId:
|
|
1449
|
+
appScopedOwnerId: appId,
|
|
1165
1450
|
...(indexed != null ? { indexed } : {}),
|
|
1166
1451
|
data,
|
|
1167
1452
|
version: (existing?.version ?? 0) + 1,
|
|
@@ -1182,7 +1467,7 @@ export class Catalox {
|
|
|
1182
1467
|
return {
|
|
1183
1468
|
itemId: logicalItemId,
|
|
1184
1469
|
catalogId,
|
|
1185
|
-
appId
|
|
1470
|
+
appId,
|
|
1186
1471
|
sourceMode: "native",
|
|
1187
1472
|
sourceType: "firebase",
|
|
1188
1473
|
data,
|
|
@@ -1191,7 +1476,8 @@ export class Catalox {
|
|
|
1191
1476
|
};
|
|
1192
1477
|
}
|
|
1193
1478
|
async batchUpsertNativeCatalogItems(context, catalogId, items) {
|
|
1194
|
-
await this.
|
|
1479
|
+
const appId = await this.resolveAppIdForCatalogAccess({ context, catalogId, required: "write" });
|
|
1480
|
+
await this.deps.authz.requireBindingAccess(context, appId, catalogId, "write");
|
|
1195
1481
|
const descriptor = await this.deps.descriptors.get(catalogId);
|
|
1196
1482
|
if (!descriptor)
|
|
1197
1483
|
throw new CatalogAdapterError({ catalogId, reason: "missing_descriptor" });
|
|
@@ -1212,7 +1498,7 @@ export class Catalox {
|
|
|
1212
1498
|
itemId: logicalItemId,
|
|
1213
1499
|
catalogId,
|
|
1214
1500
|
...(storedScope.kind !== "global" ? { scope: storedScope } : {}),
|
|
1215
|
-
appScopedOwnerId:
|
|
1501
|
+
appScopedOwnerId: appId,
|
|
1216
1502
|
...(indexed != null ? { indexed } : {}),
|
|
1217
1503
|
data: stripped.data,
|
|
1218
1504
|
createdAt: now,
|
|
@@ -1583,7 +1869,8 @@ export class Catalox {
|
|
|
1583
1869
|
});
|
|
1584
1870
|
}
|
|
1585
1871
|
async listCatalogItemHistory(context, catalogId, input) {
|
|
1586
|
-
await this.
|
|
1872
|
+
const appId = await this.resolveAppIdForCatalogAccess({ context, catalogId, required: "read" });
|
|
1873
|
+
await this.deps.authz.requireBindingAccess(context, appId, catalogId, "read");
|
|
1587
1874
|
const rh = this.deps.recordHistory;
|
|
1588
1875
|
if (!rh)
|
|
1589
1876
|
return { events: [] };
|
|
@@ -1596,7 +1883,8 @@ export class Catalox {
|
|
|
1596
1883
|
const row = await rh.store.get(eventId);
|
|
1597
1884
|
if (!row)
|
|
1598
1885
|
return null;
|
|
1599
|
-
await this.
|
|
1886
|
+
const appId = await this.resolveAppIdForCatalogAccess({ context, catalogId: row.catalogId, required: "read" });
|
|
1887
|
+
await this.deps.authz.requireBindingAccess(context, appId, row.catalogId, "read");
|
|
1600
1888
|
const payload = await readRecordHistoryEventPayload(rh, row);
|
|
1601
1889
|
return { index: row, payload };
|
|
1602
1890
|
}
|
|
@@ -1607,7 +1895,8 @@ export class Catalox {
|
|
|
1607
1895
|
const row = await rh.store.get(eventId);
|
|
1608
1896
|
if (!row)
|
|
1609
1897
|
throw new Error(`catalogItemHistory event not found: ${eventId}`);
|
|
1610
|
-
await this.
|
|
1898
|
+
const appId = await this.resolveAppIdForCatalogAccess({ context, catalogId: row.catalogId, required: "write" });
|
|
1899
|
+
await this.deps.authz.requireBindingAccess(context, appId, row.catalogId, "write");
|
|
1611
1900
|
const payload = await readRecordHistoryEventPayload(rh, row);
|
|
1612
1901
|
const rec = input.mode === "before" ? payload.before ?? undefined : payload.after ?? undefined;
|
|
1613
1902
|
if (!rec)
|
|
@@ -1627,7 +1916,7 @@ export class Catalox {
|
|
|
1627
1916
|
return this.decorateItem(row.catalogId, {
|
|
1628
1917
|
itemId: rec.itemId,
|
|
1629
1918
|
catalogId: row.catalogId,
|
|
1630
|
-
appId
|
|
1919
|
+
appId,
|
|
1631
1920
|
sourceMode: "native",
|
|
1632
1921
|
sourceType: "firebase",
|
|
1633
1922
|
data: liveAfter.data,
|
|
@@ -1636,7 +1925,8 @@ export class Catalox {
|
|
|
1636
1925
|
});
|
|
1637
1926
|
}
|
|
1638
1927
|
async replayCatalogToPointInTime(context, catalogId, input) {
|
|
1639
|
-
await this.
|
|
1928
|
+
const appId = await this.resolveAppIdForCatalogAccess({ context, catalogId, required: "write" });
|
|
1929
|
+
await this.deps.authz.requireBindingAccess(context, appId, catalogId, "write");
|
|
1640
1930
|
const rh = this.deps.recordHistory;
|
|
1641
1931
|
if (!rh)
|
|
1642
1932
|
throw new Error("recordHistory is not configured on Catalox");
|
|
@@ -1688,8 +1978,9 @@ export class Catalox {
|
|
|
1688
1978
|
return { upserted, deleted };
|
|
1689
1979
|
}
|
|
1690
1980
|
async deleteCatalog(context, catalogId, input) {
|
|
1691
|
-
await this.
|
|
1692
|
-
|
|
1981
|
+
const appId = await this.resolveAppIdForCatalogAccess({ context, catalogId, required: "admin" });
|
|
1982
|
+
await this.deps.authz.requireBindingAccess(context, appId, catalogId, "admin");
|
|
1983
|
+
return runDeleteCatalog(this.catalogLifecycleDeps(), this.deps.recordHistory, { ...context, appId }, catalogId, input);
|
|
1693
1984
|
}
|
|
1694
1985
|
async restoreDeletedCatalog(context, input) {
|
|
1695
1986
|
if (!context.superAdmin) {
|
|
@@ -1706,8 +1997,9 @@ export class Catalox {
|
|
|
1706
1997
|
return runRestoreDeletedCatalog(this.catalogLifecycleDeps(), rh, input);
|
|
1707
1998
|
}
|
|
1708
1999
|
async renameCatalog(context, fromCatalogId, toCatalogId, input = {}) {
|
|
1709
|
-
await this.
|
|
1710
|
-
|
|
2000
|
+
const appId = await this.resolveAppIdForCatalogAccess({ context, catalogId: fromCatalogId, required: "admin" });
|
|
2001
|
+
await this.deps.authz.requireBindingAccess(context, appId, fromCatalogId, "admin");
|
|
2002
|
+
return runRenameCatalog(this.catalogLifecycleDeps(), this.deps.recordHistory, { ...context, appId }, fromCatalogId, toCatalogId, input);
|
|
1711
2003
|
}
|
|
1712
2004
|
/**
|
|
1713
2005
|
* Fix {@link CataloxContext} for subsequent calls so embedders omit it on each method (no globals).
|