@x12i/catalox 4.2.1 → 5.1.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 +61 -0
- package/dist/src/catalox/catalox.d.ts +7 -5
- package/dist/src/catalox/catalox.d.ts.map +1 -1
- package/dist/src/catalox/catalox.js +151 -155
- package/dist/src/catalox/catalox.js.map +1 -1
- package/dist/src/catalox/item-scope-match.d.ts +39 -0
- package/dist/src/catalox/item-scope-match.d.ts.map +1 -0
- package/dist/src/catalox/item-scope-match.js +114 -0
- package/dist/src/catalox/item-scope-match.js.map +1 -0
- package/dist/src/catalox/item-scope.d.ts +36 -0
- package/dist/src/catalox/item-scope.d.ts.map +1 -0
- package/dist/src/catalox/item-scope.js +137 -0
- package/dist/src/catalox/item-scope.js.map +1 -0
- package/dist/src/catalox/native-catalog-merge.d.ts +7 -9
- package/dist/src/catalox/native-catalog-merge.d.ts.map +1 -1
- package/dist/src/catalox/native-catalog-merge.js +23 -106
- package/dist/src/catalox/native-catalog-merge.js.map +1 -1
- package/dist/src/catalox/scope-legacy-convert.d.ts +19 -0
- package/dist/src/catalox/scope-legacy-convert.d.ts.map +1 -0
- package/dist/src/catalox/scope-legacy-convert.js +177 -0
- package/dist/src/catalox/scope-legacy-convert.js.map +1 -0
- package/dist/src/catalox/smart-properties.d.ts +33 -0
- package/dist/src/catalox/smart-properties.d.ts.map +1 -0
- package/dist/src/catalox/smart-properties.js +245 -0
- package/dist/src/catalox/smart-properties.js.map +1 -0
- package/dist/src/cli/index.js +96 -3
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/contracts/catalogs.d.ts +21 -25
- 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 +9 -4
- package/dist/src/contracts/context.d.ts.map +1 -1
- package/dist/src/contracts/descriptors.d.ts +14 -0
- package/dist/src/contracts/descriptors.d.ts.map +1 -1
- package/dist/src/contracts/index.d.ts +2 -2
- package/dist/src/contracts/index.d.ts.map +1 -1
- package/dist/src/contracts/items.d.ts +8 -51
- package/dist/src/contracts/items.d.ts.map +1 -1
- package/dist/src/firebase/native-item-store.d.ts +2 -1
- package/dist/src/firebase/native-item-store.d.ts.map +1 -1
- package/dist/src/firebase/native-item-store.js +2 -5
- package/dist/src/firebase/native-item-store.js.map +1 -1
- package/dist/src/migrations/index.d.ts +1 -0
- package/dist/src/migrations/index.d.ts.map +1 -1
- package/dist/src/migrations/index.js +1 -0
- package/dist/src/migrations/index.js.map +1 -1
- package/dist/src/migrations/migrate-native-item-scope.d.ts +36 -0
- package/dist/src/migrations/migrate-native-item-scope.d.ts.map +1 -0
- package/dist/src/migrations/migrate-native-item-scope.js +204 -0
- package/dist/src/migrations/migrate-native-item-scope.js.map +1 -0
- package/docs/cli-items.md +1 -1
- package/package.json +1 -1
- package/dist/src/catalox/native-scope.d.ts +0 -29
- package/dist/src/catalox/native-scope.d.ts.map +0 -1
- package/dist/src/catalox/native-scope.js +0 -324
- package/dist/src/catalox/native-scope.js.map +0 -1
|
@@ -8,8 +8,10 @@ import { ApiCatalogAdapter } from "../adapters/api/api-adapter.js";
|
|
|
8
8
|
import { MongoCatalogAdapter } from "../adapters/mongo/mongo-adapter.js";
|
|
9
9
|
import { AuthorizationService } from "./authorization.js";
|
|
10
10
|
import { resolveCatalogItemId } from "./identity.js";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
11
|
+
import { assertWriteScopeAllowed, indexedScopeFields, isGenericScope, metadataFromScope, normalizeItemScope, parseWriteScopeInput, scopeFromRecordField, } from "./item-scope.js";
|
|
12
|
+
import { isUnrestrictedScopeContext, mergeEffectiveScope, resolveCatalogScopeFetchMode, shouldIncludeItemInList, tokenScopeFromContext, } from "./item-scope-match.js";
|
|
13
|
+
import { mergeNativePhysicalRows, pickWinningPhysicalRow, toUnifiedNativeItem } from "./native-catalog-merge.js";
|
|
14
|
+
import { deriveIndexedFromSmartProperties, resolveSmartPropertiesOnWrite, validateSmartPropertiesForItem, validateSmartPropertyRules, } from "./smart-properties.js";
|
|
13
15
|
import { parseJson, toJson } from "./json-io.js";
|
|
14
16
|
import { buildCataloxExportV1, filterCatalogEntries, listAllCatalogItemsForExport, stripItemData, } from "./catalog-snapshot-export.js";
|
|
15
17
|
import { createHash, randomUUID } from "node:crypto";
|
|
@@ -43,6 +45,7 @@ import { runUndoFirestoreRestore } from "./restore-firestore-backup.js";
|
|
|
43
45
|
import { runRestoreFirestoreBackupFromGcs, runDeleteCataloxGcsBackupRun } from "./restore-firestore-backup-from-gcs.js";
|
|
44
46
|
import { cataloxGcsBackupManifestToFirestoreExportManifest } from "./catalox-gcs-backup-export-manifest.js";
|
|
45
47
|
import { migrateNativeCatalogLayout as runMigrateNativeCatalogLayout, } from "../migrations/migrate-native-catalog-layout.js";
|
|
48
|
+
import { migrateNativeItemScope as runMigrateNativeItemScope, } from "../migrations/migrate-native-item-scope.js";
|
|
46
49
|
import { reportNativeCatalogLayoutDiagnostics as runReportNativeCatalogLayoutDiagnostics, } from "./native-catalog-layout-diagnostics.js";
|
|
47
50
|
import { exportAllFirestoreCollectionsToGcs, exportFirestoreCollectionToGcs, restoreAllFirestoreCollectionsFromGcsManifest, restoreFirestoreCollectionFromGcs, } from "./firestore-gcs-transfer.js";
|
|
48
51
|
import { compareAllFirestoreCollectionsWithGcsManifest, compareFirestoreCollectionWithGcsNdjson, } from "./firestore-gcs-compare.js";
|
|
@@ -638,8 +641,20 @@ export class Catalox {
|
|
|
638
641
|
if (v !== undefined)
|
|
639
642
|
out[f.key] = v;
|
|
640
643
|
}
|
|
644
|
+
const smartIdx = deriveIndexedFromSmartProperties(descriptor, data);
|
|
645
|
+
if (smartIdx)
|
|
646
|
+
Object.assign(out, smartIdx);
|
|
641
647
|
return Object.keys(out).length ? out : undefined;
|
|
642
648
|
}
|
|
649
|
+
smartPropertyDeps() {
|
|
650
|
+
return {
|
|
651
|
+
catalogs: this.deps.catalogs,
|
|
652
|
+
descriptors: this.deps.descriptors,
|
|
653
|
+
nativeItems: this.deps.nativeItems,
|
|
654
|
+
authz: this.deps.authz,
|
|
655
|
+
resolveAppIdForCatalog: (input) => this.resolveAppIdForCatalogAccess(input),
|
|
656
|
+
};
|
|
657
|
+
}
|
|
643
658
|
stripReservedWriteFields(input) {
|
|
644
659
|
const { indexed, scope, relations, references, ...rest } = input;
|
|
645
660
|
const parsedScope = scope != null ? parseWriteScopeInput(scope) : undefined;
|
|
@@ -651,93 +666,44 @@ export class Catalox {
|
|
|
651
666
|
...(parsedRelations.length ? { relations: parsedRelations } : {}),
|
|
652
667
|
};
|
|
653
668
|
}
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
buildAppliedScope(scope, effectiveSuperList) {
|
|
669
|
-
return {
|
|
670
|
-
...(scope?.accountId != null ? { accountId: scope.accountId } : {}),
|
|
671
|
-
...(scope?.groupId != null ? { groupId: scope.groupId } : {}),
|
|
672
|
-
...(scope?.agentId != null ? { agentId: scope.agentId } : {}),
|
|
673
|
-
...(scope?.domainId != null ? { domainId: scope.domainId } : {}),
|
|
674
|
-
...(scope?.userId != null ? { userId: scope.userId } : {}),
|
|
675
|
-
...(scope?.channelId != null ? { channelId: scope.channelId } : {}),
|
|
676
|
-
...(scope?.visitorId != null ? { visitorId: scope.visitorId } : {}),
|
|
677
|
-
superAdmin: effectiveSuperList,
|
|
678
|
-
};
|
|
679
|
-
}
|
|
680
|
-
async fetchGlobalPhysicalRows(catalogId, listOpts) {
|
|
681
|
-
const userFe = { ...(listOpts.filterEq ?? {}) };
|
|
682
|
-
const withGlobal = { ...userFe, scopeLayer: "global" };
|
|
683
|
-
let recs = await this.deps.nativeItems.list(catalogId, { ...listOpts, filterEq: withGlobal });
|
|
684
|
-
if (recs.length === 0 && Object.keys(userFe).length === 0) {
|
|
685
|
-
const broad = await this.deps.nativeItems.list(catalogId, { ...listOpts, filterEq: {} });
|
|
686
|
-
recs = broad.filter((r) => isGlobalPhysicalRow(r));
|
|
669
|
+
filterNativeCatalogRows(context, records, fetchOpts) {
|
|
670
|
+
const wantsSuperList = fetchOpts?.superAdmin === true && context.superAdmin === true;
|
|
671
|
+
if (wantsSuperList) {
|
|
672
|
+
return {
|
|
673
|
+
rows: records,
|
|
674
|
+
applied: {
|
|
675
|
+
effectiveScope: {},
|
|
676
|
+
includeGeneric: true,
|
|
677
|
+
genericOnly: false,
|
|
678
|
+
scopedOnly: false,
|
|
679
|
+
scopedAccessAllowed: true,
|
|
680
|
+
superAdmin: true,
|
|
681
|
+
},
|
|
682
|
+
};
|
|
687
683
|
}
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
const
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
const
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
684
|
+
const fetchMode = resolveCatalogScopeFetchMode(fetchOpts);
|
|
685
|
+
const unrestricted = isUnrestrictedScopeContext(context);
|
|
686
|
+
const token = tokenScopeFromContext(context);
|
|
687
|
+
const { effectiveScope, scopedAccessAllowed } = mergeEffectiveScope(token, fetchOpts?.scope, {
|
|
688
|
+
unrestricted,
|
|
689
|
+
});
|
|
690
|
+
const filtered = records.filter((r) => shouldIncludeItemInList({
|
|
691
|
+
itemScope: scopeFromRecordField(r.scope),
|
|
692
|
+
effectiveScope,
|
|
693
|
+
scopedAccessAllowed: unrestricted || scopedAccessAllowed,
|
|
694
|
+
fetchMode,
|
|
695
|
+
}));
|
|
696
|
+
return {
|
|
697
|
+
rows: filtered,
|
|
698
|
+
applied: {
|
|
699
|
+
effectiveScope,
|
|
700
|
+
includeGeneric: fetchMode.includeGeneric,
|
|
701
|
+
genericOnly: fetchMode.genericOnly,
|
|
702
|
+
scopedOnly: fetchMode.scopedOnly,
|
|
703
|
+
scopedAccessAllowed: unrestricted || scopedAccessAllowed,
|
|
704
|
+
superAdmin: false,
|
|
705
|
+
},
|
|
703
706
|
};
|
|
704
|
-
take(await this.deps.nativeItems.list(catalogId, { ...cap, filterEq: { ...userFe, scopeLayer: "global" } }));
|
|
705
|
-
// Legacy v1 scopes (still queried for back-compat)
|
|
706
|
-
if (fetch.accountId) {
|
|
707
|
-
take(await this.deps.nativeItems.list(catalogId, { ...cap, filterEq: { ...userFe, scopeLayer: "account", scopeAccountId: fetch.accountId } }));
|
|
708
|
-
if (fetch.agentId) {
|
|
709
|
-
take(await this.deps.nativeItems.list(catalogId, { ...cap, filterEq: { ...userFe, scopeLayer: "agent", scopeAccountId: fetch.accountId, scopeAgentId: fetch.agentId } }));
|
|
710
|
-
}
|
|
711
|
-
if (fetch.userId) {
|
|
712
|
-
take(await this.deps.nativeItems.list(catalogId, { ...cap, filterEq: { ...userFe, scopeLayer: "user", scopeAccountId: fetch.accountId, scopeUserId: fetch.userId } }));
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
// v2 scoped rows: best-effort prefilter by available indexed coordinates.
|
|
716
|
-
const scopedQueries = [];
|
|
717
|
-
if (fetch.accountId)
|
|
718
|
-
scopedQueries.push({ scopeLayer: "scoped", scopeAccountId: fetch.accountId });
|
|
719
|
-
if (fetch.userId)
|
|
720
|
-
scopedQueries.push({ scopeLayer: "scoped", scopeUserId: fetch.userId });
|
|
721
|
-
if (fetch.channelId)
|
|
722
|
-
scopedQueries.push({ scopeLayer: "scoped", scopeChannelId: fetch.channelId });
|
|
723
|
-
if (fetch.visitorId)
|
|
724
|
-
scopedQueries.push({ scopeLayer: "scoped", scopeVisitorId: fetch.visitorId });
|
|
725
|
-
if (fetch.domainId)
|
|
726
|
-
scopedQueries.push({ scopeLayer: "scoped", scopeDomainId: fetch.domainId });
|
|
727
|
-
if (fetch.agentId)
|
|
728
|
-
scopedQueries.push({ scopeLayer: "scoped", scopeAgentId: fetch.agentId });
|
|
729
|
-
if (fetch.groupId)
|
|
730
|
-
scopedQueries.push({ scopeLayer: "scoped", scopeGroupId: fetch.groupId });
|
|
731
|
-
if (scopedQueries.length) {
|
|
732
|
-
for (const q of scopedQueries) {
|
|
733
|
-
take(await this.deps.nativeItems.list(catalogId, { ...cap, filterEq: { ...userFe, ...q } }));
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
else {
|
|
737
|
-
// Fallback: if caller provided coordinates but none mapped to a queryable indexed field, list scoped broadly.
|
|
738
|
-
take(await this.deps.nativeItems.list(catalogId, { ...cap, filterEq: { ...userFe, scopeLayer: "scoped" } }));
|
|
739
|
-
}
|
|
740
|
-
return filterPhysicalForTenantFetch(out, fetch);
|
|
741
707
|
}
|
|
742
708
|
async decorateItem(catalogId, item) {
|
|
743
709
|
const descriptor = await this.deps.descriptors.get(catalogId);
|
|
@@ -978,19 +944,16 @@ export class Catalox {
|
|
|
978
944
|
...(Object.keys(filterEq).length ? { filterEq } : {}),
|
|
979
945
|
...(listQueryOptions?.sort ? { sort: listQueryOptions.sort } : {}),
|
|
980
946
|
};
|
|
981
|
-
const
|
|
982
|
-
const
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
physical = await this.fetchScopedPhysicalRows(catalogId, rawScope, baseListOpts);
|
|
992
|
-
}
|
|
993
|
-
const merged = mergeNativePhysicalRows(physical, descRec.descriptor.identity, String(appId), wantsSuperList);
|
|
947
|
+
const fetchOpts = listQueryOptions?.scope;
|
|
948
|
+
const includeScopeInResponse = fetchOpts?.includeScopeInResponse !== false;
|
|
949
|
+
const poolLimit = Math.min(5000, Math.max((listQueryOptions?.limit ?? 100) + (listQueryOptions?.offset ?? 0), 500));
|
|
950
|
+
const physical = await this.deps.nativeItems.list(catalogId, {
|
|
951
|
+
...baseListOpts,
|
|
952
|
+
limit: poolLimit,
|
|
953
|
+
offset: 0,
|
|
954
|
+
});
|
|
955
|
+
const { rows: filtered, applied } = this.filterNativeCatalogRows(context, physical, fetchOpts);
|
|
956
|
+
const merged = mergeNativePhysicalRows(filtered, descRec.descriptor.identity, String(appId), includeScopeInResponse);
|
|
994
957
|
const off = listQueryOptions?.offset ?? 0;
|
|
995
958
|
const lim = listQueryOptions?.limit ?? 100;
|
|
996
959
|
const sliced = merged.slice(off, off + lim);
|
|
@@ -998,7 +961,7 @@ export class Catalox {
|
|
|
998
961
|
return {
|
|
999
962
|
listOutcome: "ok",
|
|
1000
963
|
items: decorated,
|
|
1001
|
-
appliedScope:
|
|
964
|
+
appliedScope: applied,
|
|
1002
965
|
};
|
|
1003
966
|
}
|
|
1004
967
|
const def = await this.deps.definitions.get(catalogId);
|
|
@@ -1088,24 +1051,13 @@ export class Catalox {
|
|
|
1088
1051
|
const rec = await this.deps.nativeItems.get(catalogId, options.storageDocId);
|
|
1089
1052
|
if (!rec)
|
|
1090
1053
|
return { outcome: "not_found" };
|
|
1091
|
-
const
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
const base =
|
|
1096
|
-
itemId: rec.itemId,
|
|
1097
|
-
catalogId: rec.catalogId,
|
|
1098
|
-
appId,
|
|
1099
|
-
sourceMode: "native",
|
|
1100
|
-
sourceType: "firebase",
|
|
1101
|
-
data: rec.data,
|
|
1102
|
-
metadata: meta,
|
|
1103
|
-
};
|
|
1054
|
+
const { rows } = this.filterNativeCatalogRows(context, [rec], options?.scope);
|
|
1055
|
+
if (!rows.length)
|
|
1056
|
+
return { outcome: "not_found" };
|
|
1057
|
+
const includeScope = options?.scope?.includeScopeInResponse !== false;
|
|
1058
|
+
const base = toUnifiedNativeItem(rows[0], String(appId), includeScope);
|
|
1104
1059
|
return { outcome: "found", item: await this.decorateItem(catalogId, base) };
|
|
1105
1060
|
}
|
|
1106
|
-
const descRec = await this.deps.descriptors.get(catalogId);
|
|
1107
|
-
if (!descRec)
|
|
1108
|
-
throw new CatalogAdapterError({ catalogId, reason: "missing_descriptor" });
|
|
1109
1061
|
let rows = await this.deps.nativeItems.findByLogicalItemId(catalogId, itemId);
|
|
1110
1062
|
if (!rows.length) {
|
|
1111
1063
|
const legacy = await this.deps.nativeItems.get(catalogId, itemId);
|
|
@@ -1114,15 +1066,12 @@ export class Catalox {
|
|
|
1114
1066
|
}
|
|
1115
1067
|
if (!rows.length)
|
|
1116
1068
|
return { outcome: "not_found" };
|
|
1117
|
-
const
|
|
1118
|
-
|
|
1119
|
-
? filterPhysicalForTenantFetch(rows, scope)
|
|
1120
|
-
: rows.filter((r) => isGlobalPhysicalRow(r));
|
|
1121
|
-
const merged = mergeNativePhysicalRows(filtered, descRec.descriptor.identity, String(appId), false);
|
|
1122
|
-
const hit = merged.find((m) => m.itemId === itemId) ?? merged[0];
|
|
1123
|
-
if (!hit)
|
|
1069
|
+
const { rows: filtered } = this.filterNativeCatalogRows(context, rows, options?.scope);
|
|
1070
|
+
if (!filtered.length)
|
|
1124
1071
|
return { outcome: "not_found" };
|
|
1125
|
-
|
|
1072
|
+
const includeScope = options?.scope?.includeScopeInResponse !== false;
|
|
1073
|
+
const base = toUnifiedNativeItem(filtered[0], String(appId), includeScope);
|
|
1074
|
+
return { outcome: "found", item: await this.decorateItem(catalogId, base) };
|
|
1126
1075
|
}
|
|
1127
1076
|
// For mapped catalogs, use list path with filter if adapter supports.
|
|
1128
1077
|
const result = await this.listCatalogItems(context, catalogId, {
|
|
@@ -1241,7 +1190,7 @@ export class Catalox {
|
|
|
1241
1190
|
}
|
|
1242
1191
|
return { isValid: !issues.some((i) => i.severity === "error"), issues };
|
|
1243
1192
|
}
|
|
1244
|
-
async validateCatalogItem(context, catalogId, itemId) {
|
|
1193
|
+
async validateCatalogItem(context, catalogId, itemId, options) {
|
|
1245
1194
|
const issues = [];
|
|
1246
1195
|
const descriptor = await this.getCatalogDescriptor(context, catalogId);
|
|
1247
1196
|
if (!descriptor) {
|
|
@@ -1271,6 +1220,10 @@ export class Catalox {
|
|
|
1271
1220
|
const typeRegistry = await this.resolveCatalogTypeRegistry(context);
|
|
1272
1221
|
const payloadReport = this.validateNativeItemData(got.item.data, catalog, typeRegistry);
|
|
1273
1222
|
issues.push(...payloadReport.issues);
|
|
1223
|
+
if (descriptor.smartProperties?.length) {
|
|
1224
|
+
const spReport = await validateSmartPropertiesForItem(this.smartPropertyDeps(), context, catalogId, descriptor, got.item.data, { autoCreate: options?.noAutoCreate ? false : true });
|
|
1225
|
+
issues.push(...spReport.issues);
|
|
1226
|
+
}
|
|
1274
1227
|
}
|
|
1275
1228
|
}
|
|
1276
1229
|
return {
|
|
@@ -1743,15 +1696,15 @@ export class Catalox {
|
|
|
1743
1696
|
if (legacy)
|
|
1744
1697
|
rows = [legacy];
|
|
1745
1698
|
}
|
|
1746
|
-
existing = pickWinningPhysicalRow(rows
|
|
1699
|
+
existing = pickWinningPhysicalRow(rows);
|
|
1747
1700
|
}
|
|
1748
1701
|
if (!existing)
|
|
1749
1702
|
throw new CatalogNotFoundError({ catalogId: _catalogId, itemId: _itemId });
|
|
1750
1703
|
const { data: patchData, indexed: patchIndexed, scope: patchScope, relations: patchRelations } = this.stripReservedWriteFields(_patch);
|
|
1751
|
-
let nextScope =
|
|
1704
|
+
let nextScope = scopeFromRecordField(existing.scope);
|
|
1752
1705
|
if (patchScope != null) {
|
|
1753
|
-
nextScope =
|
|
1754
|
-
|
|
1706
|
+
nextScope = normalizeItemScope(patchScope);
|
|
1707
|
+
assertWriteScopeAllowed(_context, nextScope);
|
|
1755
1708
|
}
|
|
1756
1709
|
// Validate relation rules using existing + provided relations (if any).
|
|
1757
1710
|
const descriptorRec = await this.deps.descriptors.get(_catalogId);
|
|
@@ -1786,24 +1739,40 @@ export class Catalox {
|
|
|
1786
1739
|
}
|
|
1787
1740
|
const updatedAt = new Date().toISOString();
|
|
1788
1741
|
const mergedData = { ...(existing.data ?? {}), ...patchData };
|
|
1742
|
+
const desc = descriptorRec?.descriptor;
|
|
1743
|
+
if (desc?.smartProperties?.length) {
|
|
1744
|
+
const rulesOnly = validateSmartPropertyRules(desc, mergedData);
|
|
1745
|
+
if (!rulesOnly.isValid) {
|
|
1746
|
+
throw new CatalogValidationError({
|
|
1747
|
+
reason: "smart_properties",
|
|
1748
|
+
catalogId: String(_catalogId),
|
|
1749
|
+
itemId: String(_itemId),
|
|
1750
|
+
report: rulesOnly,
|
|
1751
|
+
});
|
|
1752
|
+
}
|
|
1753
|
+
await resolveSmartPropertiesOnWrite(this.smartPropertyDeps(), _context, _catalogId, desc, mergedData, nextScope);
|
|
1754
|
+
}
|
|
1755
|
+
const derived = desc ? this.deriveIndexed(desc, mergedData) : undefined;
|
|
1789
1756
|
const idx = {
|
|
1790
1757
|
...(existing.indexed ?? {}),
|
|
1791
1758
|
...(patchIndexed ?? {}),
|
|
1759
|
+
...(derived ?? {}),
|
|
1792
1760
|
...indexedScopeFields(nextScope),
|
|
1793
1761
|
};
|
|
1794
1762
|
const oldDocId = storageDocIdForNativeRecord(existing);
|
|
1795
1763
|
const { scope: _dropScope, ...existingRest } = existing;
|
|
1764
|
+
const scopeMeta = metadataFromScope(nextScope);
|
|
1796
1765
|
const nextRec = {
|
|
1797
1766
|
...existingRest,
|
|
1798
1767
|
data: mergedData,
|
|
1799
1768
|
indexed: idx,
|
|
1800
|
-
|
|
1769
|
+
scope: nextScope,
|
|
1801
1770
|
metadata: {
|
|
1802
1771
|
...(existing.metadata ?? {}),
|
|
1803
1772
|
createdAt: String(existing.metadata?.createdAt ?? updatedAt),
|
|
1804
1773
|
lastUpdate: updatedAt,
|
|
1805
|
-
domainIds:
|
|
1806
|
-
agentIds:
|
|
1774
|
+
domainIds: scopeMeta.domainIds,
|
|
1775
|
+
agentIds: scopeMeta.agentIds,
|
|
1807
1776
|
},
|
|
1808
1777
|
version: (existing.version ?? 0) + 1,
|
|
1809
1778
|
...(this.resolveActorId(_context) ? { updatedBy: this.resolveActorId(_context) } : {}),
|
|
@@ -1828,11 +1797,12 @@ export class Catalox {
|
|
|
1828
1797
|
sourceMode: "native",
|
|
1829
1798
|
sourceType: "firebase",
|
|
1830
1799
|
data: mergedData,
|
|
1800
|
+
scope: nextScope,
|
|
1831
1801
|
metadata: {
|
|
1832
1802
|
createdAt: String(existing.metadata?.createdAt ?? updatedAt),
|
|
1833
1803
|
lastUpdate: updatedAt,
|
|
1834
|
-
domainIds:
|
|
1835
|
-
agentIds:
|
|
1804
|
+
domainIds: scopeMeta.domainIds,
|
|
1805
|
+
agentIds: scopeMeta.agentIds,
|
|
1836
1806
|
},
|
|
1837
1807
|
};
|
|
1838
1808
|
// Upsert any provided relations after the item is persisted.
|
|
@@ -1860,7 +1830,7 @@ export class Catalox {
|
|
|
1860
1830
|
if (legacy)
|
|
1861
1831
|
rows = [legacy];
|
|
1862
1832
|
}
|
|
1863
|
-
const winner = pickWinningPhysicalRow(rows
|
|
1833
|
+
const winner = pickWinningPhysicalRow(rows);
|
|
1864
1834
|
if (!winner)
|
|
1865
1835
|
throw new CatalogNotFoundError({ catalogId: _catalogId, itemId: _itemId });
|
|
1866
1836
|
docId = storageDocIdForNativeRecord(winner);
|
|
@@ -1884,18 +1854,23 @@ export class Catalox {
|
|
|
1884
1854
|
}
|
|
1885
1855
|
const appId = await this.resolveAppIdForCatalogAccess({ context, catalogId, required: "write" });
|
|
1886
1856
|
await this.deps.authz.requireBindingAccess(context, appId, catalogId, "write");
|
|
1887
|
-
const fromDoc = input.fromStorageDocId ??
|
|
1888
|
-
encodeNativeItemStorageDocId(String(itemId), normalizeStoredScope(input.fromScope ?? { kind: "global" }));
|
|
1857
|
+
const fromDoc = input.fromStorageDocId ?? storageDocIdForNativeRecord({ itemId: String(itemId) });
|
|
1889
1858
|
const rec = await this.deps.nativeItems.get(catalogId, fromDoc);
|
|
1890
1859
|
if (!rec)
|
|
1891
1860
|
throw new CatalogNotFoundError({ catalogId, itemId });
|
|
1892
|
-
const toScope =
|
|
1893
|
-
|
|
1861
|
+
const toScope = normalizeItemScope(input.toScope);
|
|
1862
|
+
assertWriteScopeAllowed(context, toScope);
|
|
1863
|
+
const scopeMeta = metadataFromScope(toScope);
|
|
1894
1864
|
const nextRec = {
|
|
1895
|
-
...
|
|
1896
|
-
|
|
1865
|
+
...rec,
|
|
1866
|
+
scope: toScope,
|
|
1897
1867
|
indexed: { ...(rec.indexed ?? {}), ...indexedScopeFields(toScope) },
|
|
1898
|
-
|
|
1868
|
+
metadata: {
|
|
1869
|
+
...(rec.metadata ?? {}),
|
|
1870
|
+
domainIds: scopeMeta.domainIds,
|
|
1871
|
+
agentIds: scopeMeta.agentIds,
|
|
1872
|
+
lastUpdate: new Date().toISOString(),
|
|
1873
|
+
},
|
|
1899
1874
|
};
|
|
1900
1875
|
const toDoc = storageDocIdForNativeRecord(nextRec);
|
|
1901
1876
|
await this.deps.nativeItems.upsert(catalogId, nextRec);
|
|
@@ -1918,15 +1893,26 @@ export class Catalox {
|
|
|
1918
1893
|
if (!descriptor)
|
|
1919
1894
|
throw new CatalogAdapterError({ catalogId, reason: "missing_descriptor" });
|
|
1920
1895
|
const { data, indexed: callerIndexed, scope: inputScope, relations: inputRelations } = this.stripReservedWriteFields(input);
|
|
1921
|
-
const storedScope =
|
|
1922
|
-
|
|
1896
|
+
const storedScope = normalizeItemScope(inputScope ?? {});
|
|
1897
|
+
assertWriteScopeAllowed(context, storedScope);
|
|
1923
1898
|
const logicalItemIdEarly = resolveCatalogItemId({ identity: descriptor.descriptor.identity, data });
|
|
1924
1899
|
await this.assertNativeItemPayloadValid(context, catalogId, data, String(logicalItemIdEarly));
|
|
1900
|
+
const rulesOnly = validateSmartPropertyRules(descriptor.descriptor, data);
|
|
1901
|
+
if (!rulesOnly.isValid) {
|
|
1902
|
+
throw new CatalogValidationError({
|
|
1903
|
+
reason: "smart_properties",
|
|
1904
|
+
catalogId: String(catalogId),
|
|
1905
|
+
itemId: String(logicalItemIdEarly),
|
|
1906
|
+
report: rulesOnly,
|
|
1907
|
+
});
|
|
1908
|
+
}
|
|
1909
|
+
await resolveSmartPropertiesOnWrite(this.smartPropertyDeps(), context, catalogId, descriptor.descriptor, data, storedScope);
|
|
1925
1910
|
const derived = this.deriveIndexed(descriptor.descriptor, data);
|
|
1926
1911
|
const scopeIdx = indexedScopeFields(storedScope);
|
|
1927
1912
|
const indexed = derived != null ? { ...derived, ...scopeIdx } : (Object.keys(scopeIdx).length ? scopeIdx : undefined);
|
|
1928
1913
|
const logicalItemId = resolveCatalogItemId({ identity: descriptor.descriptor.identity, data });
|
|
1929
|
-
const storageDocId =
|
|
1914
|
+
const storageDocId = storageDocIdForNativeRecord({ itemId: String(logicalItemId) });
|
|
1915
|
+
const scopeMeta = metadataFromScope(storedScope);
|
|
1930
1916
|
const now = new Date().toISOString();
|
|
1931
1917
|
const existing = await this.deps.nativeItems.get(catalogId, storageDocId);
|
|
1932
1918
|
const actorId = this.resolveActorId(context);
|
|
@@ -1963,15 +1949,15 @@ export class Catalox {
|
|
|
1963
1949
|
await this.deps.nativeItems.upsert(catalogId, {
|
|
1964
1950
|
itemId: logicalItemId,
|
|
1965
1951
|
catalogId,
|
|
1966
|
-
|
|
1952
|
+
scope: storedScope,
|
|
1967
1953
|
appScopedOwnerId: appId,
|
|
1968
1954
|
...(indexed != null ? { indexed } : {}),
|
|
1969
1955
|
data,
|
|
1970
1956
|
metadata: {
|
|
1971
1957
|
createdAt: String(existing?.metadata?.createdAt ?? now),
|
|
1972
1958
|
lastUpdate: now,
|
|
1973
|
-
domainIds:
|
|
1974
|
-
agentIds:
|
|
1959
|
+
domainIds: scopeMeta.domainIds,
|
|
1960
|
+
agentIds: scopeMeta.agentIds,
|
|
1975
1961
|
},
|
|
1976
1962
|
version: (existing?.version ?? 0) + 1,
|
|
1977
1963
|
...(existing?.createdBy ? { createdBy: existing.createdBy } : actorId ? { createdBy: actorId } : {}),
|
|
@@ -2005,11 +1991,12 @@ export class Catalox {
|
|
|
2005
1991
|
sourceMode: "native",
|
|
2006
1992
|
sourceType: "firebase",
|
|
2007
1993
|
data,
|
|
1994
|
+
scope: storedScope,
|
|
2008
1995
|
metadata: {
|
|
2009
1996
|
createdAt: String(existing?.metadata?.createdAt ?? now),
|
|
2010
1997
|
lastUpdate: now,
|
|
2011
|
-
domainIds:
|
|
2012
|
-
agentIds:
|
|
1998
|
+
domainIds: scopeMeta.domainIds,
|
|
1999
|
+
agentIds: scopeMeta.agentIds,
|
|
2013
2000
|
},
|
|
2014
2001
|
};
|
|
2015
2002
|
}
|
|
@@ -2025,8 +2012,9 @@ export class Catalox {
|
|
|
2025
2012
|
const typeRegistry = catalog ? await this.resolveCatalogTypeRegistry(context) : null;
|
|
2026
2013
|
const records = items.map((row) => {
|
|
2027
2014
|
const stripped = this.stripReservedWriteFields(row);
|
|
2028
|
-
const storedScope =
|
|
2029
|
-
|
|
2015
|
+
const storedScope = normalizeItemScope(stripped.scope ?? {});
|
|
2016
|
+
assertWriteScopeAllowed(context, storedScope);
|
|
2017
|
+
const scopeMeta = metadataFromScope(storedScope);
|
|
2030
2018
|
if (catalog) {
|
|
2031
2019
|
const report = this.validateNativeItemData(stripped.data, catalog, typeRegistry);
|
|
2032
2020
|
if (!report.isValid) {
|
|
@@ -2049,11 +2037,11 @@ export class Catalox {
|
|
|
2049
2037
|
return {
|
|
2050
2038
|
itemId: logicalItemId,
|
|
2051
2039
|
catalogId,
|
|
2052
|
-
|
|
2040
|
+
scope: storedScope,
|
|
2053
2041
|
appScopedOwnerId: appId,
|
|
2054
2042
|
...(indexed != null ? { indexed } : {}),
|
|
2055
2043
|
data: stripped.data,
|
|
2056
|
-
metadata: { createdAt: now, lastUpdate: now, domainIds:
|
|
2044
|
+
metadata: { createdAt: now, lastUpdate: now, domainIds: scopeMeta.domainIds, agentIds: scopeMeta.agentIds },
|
|
2057
2045
|
...(actorId ? { updatedBy: actorId } : {}),
|
|
2058
2046
|
};
|
|
2059
2047
|
});
|
|
@@ -2428,6 +2416,14 @@ export class Catalox {
|
|
|
2428
2416
|
definitions: this.deps.definitions,
|
|
2429
2417
|
}, input);
|
|
2430
2418
|
}
|
|
2419
|
+
async migrateNativeItemScope(_context, input) {
|
|
2420
|
+
return runMigrateNativeItemScope({
|
|
2421
|
+
firestoreStore: this.deps.firestoreStore,
|
|
2422
|
+
catalogs: this.deps.catalogs,
|
|
2423
|
+
bindings: this.deps.bindings,
|
|
2424
|
+
nativeItems: this.deps.nativeItems,
|
|
2425
|
+
}, input);
|
|
2426
|
+
}
|
|
2431
2427
|
/**
|
|
2432
2428
|
* Export one Firestore collection (slash path) to a GCS object as NDJSON (`{ id, data }` per line).
|
|
2433
2429
|
* Auth: uses Admin Firestore only; bucket IAM must allow the runtime credential.
|