@open-mercato/core 0.8.1-develop.7228.1.b7ea09d2d3 → 0.8.1-develop.7238.1.3abe669ea5
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/.turbo/turbo-build.log +2 -2
- package/dist/generated/entities/availability_policy/index.js +41 -0
- package/dist/generated/entities/availability_policy/index.js.map +7 -0
- package/dist/generated/entities.ids.generated.js +4 -0
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +21 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/attachments/data/entities.js +3 -3
- package/dist/modules/attachments/data/entities.js.map +2 -2
- package/dist/modules/availability/acl.js +16 -0
- package/dist/modules/availability/acl.js.map +7 -0
- package/dist/modules/availability/api/check/route.js +95 -0
- package/dist/modules/availability/api/check/route.js.map +7 -0
- package/dist/modules/availability/api/openapi.js +27 -0
- package/dist/modules/availability/api/openapi.js.map +7 -0
- package/dist/modules/availability/api/policies/resolve-preview/route.js +58 -0
- package/dist/modules/availability/api/policies/resolve-preview/route.js.map +7 -0
- package/dist/modules/availability/api/policies/route.js +188 -0
- package/dist/modules/availability/api/policies/route.js.map +7 -0
- package/dist/modules/availability/backend/availability/check/page.js +185 -0
- package/dist/modules/availability/backend/availability/check/page.js.map +7 -0
- package/dist/modules/availability/backend/availability/check/page.meta.js +16 -0
- package/dist/modules/availability/backend/availability/check/page.meta.js.map +7 -0
- package/dist/modules/availability/backend/availability/policies/ResolutionPreviewPanel.js +79 -0
- package/dist/modules/availability/backend/availability/policies/ResolutionPreviewPanel.js.map +7 -0
- package/dist/modules/availability/backend/availability/policies/ScopeFields.js +53 -0
- package/dist/modules/availability/backend/availability/policies/ScopeFields.js.map +7 -0
- package/dist/modules/availability/backend/availability/policies/[id]/page.js +152 -0
- package/dist/modules/availability/backend/availability/policies/[id]/page.js.map +7 -0
- package/dist/modules/availability/backend/availability/policies/[id]/page.meta.js +12 -0
- package/dist/modules/availability/backend/availability/policies/[id]/page.meta.js.map +7 -0
- package/dist/modules/availability/backend/availability/policies/create/page.js +94 -0
- package/dist/modules/availability/backend/availability/policies/create/page.js.map +7 -0
- package/dist/modules/availability/backend/availability/policies/create/page.meta.js +12 -0
- package/dist/modules/availability/backend/availability/policies/create/page.meta.js.map +7 -0
- package/dist/modules/availability/backend/availability/policies/formGroups.js +75 -0
- package/dist/modules/availability/backend/availability/policies/formGroups.js.map +7 -0
- package/dist/modules/availability/backend/availability/policies/page.js +176 -0
- package/dist/modules/availability/backend/availability/policies/page.js.map +7 -0
- package/dist/modules/availability/backend/availability/policies/page.meta.js +16 -0
- package/dist/modules/availability/backend/availability/policies/page.meta.js.map +7 -0
- package/dist/modules/availability/commands/policies.js +312 -0
- package/dist/modules/availability/commands/policies.js.map +7 -0
- package/dist/modules/availability/commands/scope.js +19 -0
- package/dist/modules/availability/commands/scope.js.map +7 -0
- package/dist/modules/availability/data/entities.js +95 -0
- package/dist/modules/availability/data/entities.js.map +7 -0
- package/dist/modules/availability/data/validators.js +53 -0
- package/dist/modules/availability/data/validators.js.map +7 -0
- package/dist/modules/availability/di.js +41 -0
- package/dist/modules/availability/di.js.map +7 -0
- package/dist/modules/availability/events.js +18 -0
- package/dist/modules/availability/events.js.map +7 -0
- package/dist/modules/availability/index.js +15 -0
- package/dist/modules/availability/index.js.map +7 -0
- package/dist/modules/availability/lib/policyResolution.js +155 -0
- package/dist/modules/availability/lib/policyResolution.js.map +7 -0
- package/dist/modules/availability/lib/tryResolve.js +11 -0
- package/dist/modules/availability/lib/tryResolve.js.map +7 -0
- package/dist/modules/availability/migrations/Migration20260922095844_availability.js +16 -0
- package/dist/modules/availability/migrations/Migration20260922095844_availability.js.map +7 -0
- package/dist/modules/availability/setup.js +12 -0
- package/dist/modules/availability/setup.js.map +7 -0
- package/dist/modules/entities/cli.js +33 -12
- package/dist/modules/entities/cli.js.map +2 -2
- package/dist/modules/inbox_ops/api/settings/route.js +7 -32
- package/dist/modules/inbox_ops/api/settings/route.js.map +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/settings/page.js +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/settings/page.js.map +2 -2
- package/dist/modules/inbox_ops/components/proposals/ActionCard.js +1 -1
- package/dist/modules/inbox_ops/components/proposals/ActionCard.js.map +2 -2
- package/dist/modules/inbox_ops/lib/ensure-settings.js +47 -0
- package/dist/modules/inbox_ops/lib/ensure-settings.js.map +7 -0
- package/dist/modules/inbox_ops/lib/executionEngine.js.map +1 -1
- package/dist/modules/inbox_ops/lib/executionHelpers.js +0 -23
- package/dist/modules/inbox_ops/lib/executionHelpers.js.map +2 -2
- package/dist/modules/inbox_ops/lib/payloadEnrichment.js +0 -3
- package/dist/modules/inbox_ops/lib/payloadEnrichment.js.map +2 -2
- package/dist/modules/inbox_ops/setup.js +2 -21
- package/dist/modules/inbox_ops/setup.js.map +2 -2
- package/dist/modules/inbox_ops/subscribers/extractionWorker.js.map +1 -1
- package/dist/modules/sales/components/documents/SalesDocumentForm.js +7 -0
- package/dist/modules/sales/components/documents/SalesDocumentForm.js.map +2 -2
- package/dist/modules/sales/data/entities.js +3 -1
- package/dist/modules/sales/data/entities.js.map +2 -2
- package/dist/modules/sales/inbox-actions.js +0 -12
- package/dist/modules/sales/inbox-actions.js.map +2 -2
- package/dist/modules/sales/migrations/Migration20260916120000_sales_payment_allocation_order_idx.js +20 -0
- package/dist/modules/sales/migrations/Migration20260916120000_sales_payment_allocation_order_idx.js.map +7 -0
- package/dist/modules/wms/di.js +3 -0
- package/dist/modules/wms/di.js.map +2 -2
- package/dist/modules/wms/lib/availabilityCache.js +85 -0
- package/dist/modules/wms/lib/availabilityCache.js.map +7 -0
- package/dist/modules/wms/lib/availabilityCalculation.js +178 -0
- package/dist/modules/wms/lib/availabilityCalculation.js.map +7 -0
- package/dist/modules/wms/lib/availabilityProvider.js +14 -0
- package/dist/modules/wms/lib/availabilityProvider.js.map +7 -0
- package/dist/modules/wms/lib/sqlInClause.js +9 -0
- package/dist/modules/wms/lib/sqlInClause.js.map +7 -0
- package/dist/modules/wms/lib/tryResolve.js +11 -0
- package/dist/modules/wms/lib/tryResolve.js.map +7 -0
- package/dist/modules/wms/subscribers/invalidate-availability-cache.js +16 -0
- package/dist/modules/wms/subscribers/invalidate-availability-cache.js.map +7 -0
- package/generated/entities/availability_policy/index.ts +19 -0
- package/generated/entities.ids.generated.ts +4 -0
- package/generated/entity-fields-registry.ts +21 -0
- package/package.json +7 -7
- package/src/modules/attachments/data/entities.ts +5 -3
- package/src/modules/auth/i18n/de.json +3 -0
- package/src/modules/auth/i18n/en.json +3 -0
- package/src/modules/auth/i18n/es.json +3 -0
- package/src/modules/auth/i18n/ko.json +3 -0
- package/src/modules/auth/i18n/pl.json +3 -0
- package/src/modules/availability/acl.ts +12 -0
- package/src/modules/availability/api/check/route.ts +114 -0
- package/src/modules/availability/api/openapi.ts +28 -0
- package/src/modules/availability/api/policies/resolve-preview/route.ts +67 -0
- package/src/modules/availability/api/policies/route.ts +224 -0
- package/src/modules/availability/backend/availability/check/page.meta.ts +12 -0
- package/src/modules/availability/backend/availability/check/page.tsx +223 -0
- package/src/modules/availability/backend/availability/policies/ResolutionPreviewPanel.tsx +106 -0
- package/src/modules/availability/backend/availability/policies/ScopeFields.tsx +55 -0
- package/src/modules/availability/backend/availability/policies/[id]/page.meta.ts +8 -0
- package/src/modules/availability/backend/availability/policies/[id]/page.tsx +182 -0
- package/src/modules/availability/backend/availability/policies/create/page.meta.ts +8 -0
- package/src/modules/availability/backend/availability/policies/create/page.tsx +105 -0
- package/src/modules/availability/backend/availability/policies/formGroups.ts +77 -0
- package/src/modules/availability/backend/availability/policies/page.meta.ts +12 -0
- package/src/modules/availability/backend/availability/policies/page.tsx +196 -0
- package/src/modules/availability/commands/policies.ts +358 -0
- package/src/modules/availability/commands/scope.ts +26 -0
- package/src/modules/availability/data/entities.ts +92 -0
- package/src/modules/availability/data/validators.ts +70 -0
- package/src/modules/availability/di.ts +54 -0
- package/src/modules/availability/events.ts +17 -0
- package/src/modules/availability/i18n/de.json +81 -0
- package/src/modules/availability/i18n/en.json +81 -0
- package/src/modules/availability/i18n/es.json +81 -0
- package/src/modules/availability/i18n/ko.json +81 -0
- package/src/modules/availability/i18n/pl.json +81 -0
- package/src/modules/availability/index.ts +13 -0
- package/src/modules/availability/lib/policyResolution.ts +264 -0
- package/src/modules/availability/lib/tryResolve.ts +8 -0
- package/src/modules/availability/migrations/.snapshot-open-mercato.json +375 -0
- package/src/modules/availability/migrations/Migration20260922095844_availability.ts +13 -0
- package/src/modules/availability/setup.ts +10 -0
- package/src/modules/entities/cli.ts +53 -13
- package/src/modules/inbox_ops/api/settings/route.ts +9 -32
- package/src/modules/inbox_ops/backend/inbox-ops/settings/page.tsx +2 -2
- package/src/modules/inbox_ops/components/proposals/ActionCard.tsx +1 -1
- package/src/modules/inbox_ops/i18n/de.json +1 -1
- package/src/modules/inbox_ops/i18n/en.json +1 -1
- package/src/modules/inbox_ops/i18n/es.json +1 -1
- package/src/modules/inbox_ops/i18n/ko.json +1 -1
- package/src/modules/inbox_ops/i18n/pl.json +1 -1
- package/src/modules/inbox_ops/lib/ensure-settings.ts +77 -0
- package/src/modules/inbox_ops/lib/executionEngine.ts +1 -1
- package/src/modules/inbox_ops/lib/executionHelpers.ts +0 -27
- package/src/modules/inbox_ops/lib/payloadEnrichment.ts +1 -5
- package/src/modules/inbox_ops/setup.ts +2 -21
- package/src/modules/inbox_ops/subscribers/extractionWorker.ts +1 -1
- package/src/modules/sales/components/documents/SalesDocumentForm.tsx +13 -0
- package/src/modules/sales/data/entities.ts +2 -0
- package/src/modules/sales/inbox-actions.ts +0 -20
- package/src/modules/sales/migrations/.snapshot-open-mercato.json +20 -0
- package/src/modules/sales/migrations/Migration20260916120000_sales_payment_allocation_order_idx.ts +74 -0
- package/src/modules/wms/di.ts +6 -0
- package/src/modules/wms/lib/availabilityCache.ts +136 -0
- package/src/modules/wms/lib/availabilityCalculation.ts +289 -0
- package/src/modules/wms/lib/availabilityProvider.ts +29 -0
- package/src/modules/wms/lib/sqlInClause.ts +21 -0
- package/src/modules/wms/lib/tryResolve.ts +8 -0
- package/src/modules/wms/subscribers/invalidate-availability-cache.ts +30 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createModuleEvents } from "@open-mercato/shared/modules/events";
|
|
2
|
+
const events = [
|
|
3
|
+
{ id: "availability.policy.created", label: "Availability Policy Created", entity: "policy", category: "crud" },
|
|
4
|
+
{ id: "availability.policy.updated", label: "Availability Policy Updated", entity: "policy", category: "crud" },
|
|
5
|
+
{ id: "availability.policy.deleted", label: "Availability Policy Deleted", entity: "policy", category: "crud" }
|
|
6
|
+
];
|
|
7
|
+
const eventsConfig = createModuleEvents({
|
|
8
|
+
moduleId: "availability",
|
|
9
|
+
events
|
|
10
|
+
});
|
|
11
|
+
const emitAvailabilityEvent = eventsConfig.emit;
|
|
12
|
+
var events_default = eventsConfig;
|
|
13
|
+
export {
|
|
14
|
+
events_default as default,
|
|
15
|
+
emitAvailabilityEvent,
|
|
16
|
+
eventsConfig
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=events.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/modules/availability/events.ts"],
|
|
4
|
+
"sourcesContent": ["import { createModuleEvents } from '@open-mercato/shared/modules/events'\n\nconst events = [\n { id: 'availability.policy.created', label: 'Availability Policy Created', entity: 'policy', category: 'crud' },\n { id: 'availability.policy.updated', label: 'Availability Policy Updated', entity: 'policy', category: 'crud' },\n { id: 'availability.policy.deleted', label: 'Availability Policy Deleted', entity: 'policy', category: 'crud' },\n] as const\n\nexport const eventsConfig = createModuleEvents({\n moduleId: 'availability',\n events,\n})\n\nexport const emitAvailabilityEvent = eventsConfig.emit\nexport type AvailabilityEventId = typeof events[number]['id']\n\nexport default eventsConfig\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,0BAA0B;AAEnC,MAAM,SAAS;AAAA,EACb,EAAE,IAAI,+BAA+B,OAAO,+BAA+B,QAAQ,UAAU,UAAU,OAAO;AAAA,EAC9G,EAAE,IAAI,+BAA+B,OAAO,+BAA+B,QAAQ,UAAU,UAAU,OAAO;AAAA,EAC9G,EAAE,IAAI,+BAA+B,OAAO,+BAA+B,QAAQ,UAAU,UAAU,OAAO;AAChH;AAEO,MAAM,eAAe,mBAAmB;AAAA,EAC7C,UAAU;AAAA,EACV;AACF,CAAC;AAEM,MAAM,wBAAwB,aAAa;AAGlD,IAAO,iBAAQ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const metadata = {
|
|
2
|
+
name: "availability",
|
|
3
|
+
title: "Availability",
|
|
4
|
+
version: "0.1.0",
|
|
5
|
+
description: "Sell-policy layer (stock-managed, backorder, preorder, thresholds) on top of the shared availability contract.",
|
|
6
|
+
author: "Open Mercato Team",
|
|
7
|
+
license: "MIT",
|
|
8
|
+
ejectable: true
|
|
9
|
+
};
|
|
10
|
+
import { features } from "./acl.js";
|
|
11
|
+
export {
|
|
12
|
+
features,
|
|
13
|
+
metadata
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/modules/availability/index.ts"],
|
|
4
|
+
"sourcesContent": ["import type { ModuleInfo } from '@open-mercato/shared/modules/registry'\n\nexport const metadata: ModuleInfo = {\n name: 'availability',\n title: 'Availability',\n version: '0.1.0',\n description: 'Sell-policy layer (stock-managed, backorder, preorder, thresholds) on top of the shared availability contract.',\n author: 'Open Mercato Team',\n license: 'MIT',\n ejectable: true,\n}\n\nexport { features } from './acl'\n"],
|
|
5
|
+
"mappings": "AAEO,MAAM,WAAuB;AAAA,EAClC,MAAM;AAAA,EACN,OAAO;AAAA,EACP,SAAS;AAAA,EACT,aAAa;AAAA,EACb,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,WAAW;AACb;AAEA,SAAS,gBAAgB;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { AvailabilityPolicy } from "../data/entities.js";
|
|
2
|
+
import { tryResolve } from "./tryResolve.js";
|
|
3
|
+
const POLICY_CHAIN_LEVELS = [
|
|
4
|
+
"variant_store",
|
|
5
|
+
"variant",
|
|
6
|
+
"product_store",
|
|
7
|
+
"product",
|
|
8
|
+
"store_default",
|
|
9
|
+
"module_default"
|
|
10
|
+
];
|
|
11
|
+
const NULLABLE_FIELDS = [
|
|
12
|
+
"backorderLeadTimeDays",
|
|
13
|
+
"preorderReleaseAt",
|
|
14
|
+
"lowStockThreshold",
|
|
15
|
+
"minOrderQuantity",
|
|
16
|
+
"maxOrderQuantity",
|
|
17
|
+
"quantityIncrement"
|
|
18
|
+
];
|
|
19
|
+
const BOOLEAN_FIELDS = ["isStockManaged", "allowBackorder", "hideWhenOutOfStock", "isActive"];
|
|
20
|
+
function candidateFilters(scope) {
|
|
21
|
+
const storeId = scope.storeId ?? null;
|
|
22
|
+
const variantId = scope.variantId ?? null;
|
|
23
|
+
const filters = [];
|
|
24
|
+
if (variantId) {
|
|
25
|
+
filters.push({ variantId, storeId });
|
|
26
|
+
filters.push({ variantId, storeId: null });
|
|
27
|
+
}
|
|
28
|
+
filters.push({ productId: scope.productId, variantId: null, storeId });
|
|
29
|
+
filters.push({ productId: scope.productId, variantId: null, storeId: null });
|
|
30
|
+
filters.push({ productId: null, variantId: null, storeId });
|
|
31
|
+
if (storeId) filters.push({ productId: null, variantId: null, storeId: null });
|
|
32
|
+
return filters;
|
|
33
|
+
}
|
|
34
|
+
function matchChainRows(scope, pool) {
|
|
35
|
+
const storeId = scope.storeId ?? null;
|
|
36
|
+
const variantId = scope.variantId ?? null;
|
|
37
|
+
const findMatch = (predicate) => pool.find(predicate) ?? null;
|
|
38
|
+
const rows = [];
|
|
39
|
+
if (variantId) {
|
|
40
|
+
rows.push(findMatch((r) => r.variantId === variantId && (r.storeId ?? null) === storeId));
|
|
41
|
+
rows.push(findMatch((r) => r.variantId === variantId && (r.storeId ?? null) === null));
|
|
42
|
+
}
|
|
43
|
+
rows.push(findMatch((r) => r.productId === scope.productId && !r.variantId && (r.storeId ?? null) === storeId));
|
|
44
|
+
rows.push(findMatch((r) => r.productId === scope.productId && !r.variantId && (r.storeId ?? null) === null));
|
|
45
|
+
rows.push(findMatch((r) => !r.productId && !r.variantId && (r.storeId ?? null) === storeId));
|
|
46
|
+
if (storeId) rows.push(findMatch((r) => !r.productId && !r.variantId && (r.storeId ?? null) === null));
|
|
47
|
+
return rows;
|
|
48
|
+
}
|
|
49
|
+
function resolveFromRows(rows, isStockManagedModuleDefault) {
|
|
50
|
+
const firstRow = rows.find((row) => row != null) ?? null;
|
|
51
|
+
const result = {};
|
|
52
|
+
for (const field of BOOLEAN_FIELDS) {
|
|
53
|
+
if (firstRow) {
|
|
54
|
+
;
|
|
55
|
+
result[field] = { value: firstRow[field], policySourceId: firstRow.id };
|
|
56
|
+
} else {
|
|
57
|
+
;
|
|
58
|
+
result[field] = { value: false, policySourceId: null };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (!firstRow) result.isStockManaged = { value: isStockManagedModuleDefault, policySourceId: null };
|
|
62
|
+
if (!firstRow) result.isActive = { value: true, policySourceId: null };
|
|
63
|
+
for (const field of NULLABLE_FIELDS) {
|
|
64
|
+
let resolved = { value: null, policySourceId: null };
|
|
65
|
+
for (const row of rows) {
|
|
66
|
+
if (row && row[field] != null) {
|
|
67
|
+
resolved = { value: row[field], policySourceId: row.id };
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
;
|
|
72
|
+
result[field] = resolved;
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
async function resolveIsStockManagedModuleDefault(em, container, scope) {
|
|
77
|
+
const ProductInventoryProfile = tryResolve(container, "ProductInventoryProfile");
|
|
78
|
+
if (!ProductInventoryProfile) return false;
|
|
79
|
+
const baseFilter = {
|
|
80
|
+
tenantId: scope.tenantId,
|
|
81
|
+
organizationId: scope.organizationId,
|
|
82
|
+
catalogProductId: scope.productId,
|
|
83
|
+
deletedAt: null
|
|
84
|
+
};
|
|
85
|
+
if (scope.variantId) {
|
|
86
|
+
const variantRow = await em.findOne(ProductInventoryProfile, { ...baseFilter, catalogVariantId: scope.variantId });
|
|
87
|
+
if (variantRow) return true;
|
|
88
|
+
}
|
|
89
|
+
const productRow = await em.findOne(ProductInventoryProfile, { ...baseFilter, catalogVariantId: null });
|
|
90
|
+
return !!productRow;
|
|
91
|
+
}
|
|
92
|
+
async function resolveIsStockManagedModuleDefaultMany(em, container, scopes) {
|
|
93
|
+
const ProductInventoryProfile = tryResolve(container, "ProductInventoryProfile");
|
|
94
|
+
const results = /* @__PURE__ */ new Map();
|
|
95
|
+
if (!ProductInventoryProfile || scopes.length === 0) return results;
|
|
96
|
+
const first = scopes[0];
|
|
97
|
+
const productIds = Array.from(new Set(scopes.map((s) => s.productId)));
|
|
98
|
+
const rows = await em.find(ProductInventoryProfile, {
|
|
99
|
+
tenantId: first.tenantId,
|
|
100
|
+
organizationId: first.organizationId,
|
|
101
|
+
deletedAt: null,
|
|
102
|
+
catalogProductId: { $in: productIds }
|
|
103
|
+
});
|
|
104
|
+
const exists = new Set(rows.map((row) => `${row.catalogProductId}:${row.catalogVariantId ?? ""}`));
|
|
105
|
+
for (const scope of scopes) {
|
|
106
|
+
const variantHit = scope.variantId ? exists.has(`${scope.productId}:${scope.variantId}`) : false;
|
|
107
|
+
const productHit = exists.has(`${scope.productId}:`);
|
|
108
|
+
results.set(scopeKey(scope), variantHit || productHit);
|
|
109
|
+
}
|
|
110
|
+
return results;
|
|
111
|
+
}
|
|
112
|
+
function scopeKey(scope) {
|
|
113
|
+
return `${scope.productId}:${scope.variantId ?? ""}:${scope.storeId ?? ""}`;
|
|
114
|
+
}
|
|
115
|
+
function createPolicyResolutionService(container) {
|
|
116
|
+
async function resolveMany(em, scopes) {
|
|
117
|
+
if (scopes.length === 0) return [];
|
|
118
|
+
const first = scopes[0];
|
|
119
|
+
const seen = /* @__PURE__ */ new Set();
|
|
120
|
+
const orFilters = [];
|
|
121
|
+
for (const scope of scopes) {
|
|
122
|
+
for (const filter of candidateFilters(scope)) {
|
|
123
|
+
const key = JSON.stringify(filter);
|
|
124
|
+
if (seen.has(key)) continue;
|
|
125
|
+
seen.add(key);
|
|
126
|
+
orFilters.push(filter);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const pool = orFilters.length ? await em.find(AvailabilityPolicy, {
|
|
130
|
+
tenantId: first.tenantId,
|
|
131
|
+
organizationId: first.organizationId,
|
|
132
|
+
deletedAt: null,
|
|
133
|
+
$or: orFilters
|
|
134
|
+
}) : [];
|
|
135
|
+
const rowsByScope = scopes.map((scope) => matchChainRows(scope, pool));
|
|
136
|
+
const needsModuleDefault = scopes.filter((scope, index) => !rowsByScope[index].some((row) => row != null));
|
|
137
|
+
const moduleDefaults = await resolveIsStockManagedModuleDefaultMany(em, container, needsModuleDefault);
|
|
138
|
+
return scopes.map(
|
|
139
|
+
(scope, index) => resolveFromRows(rowsByScope[index], moduleDefaults.get(scopeKey(scope)) ?? false)
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
async resolve(em, scope) {
|
|
144
|
+
const [result] = await resolveMany(em, [scope]);
|
|
145
|
+
return result;
|
|
146
|
+
},
|
|
147
|
+
resolveMany
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
export {
|
|
151
|
+
POLICY_CHAIN_LEVELS,
|
|
152
|
+
createPolicyResolutionService,
|
|
153
|
+
resolveIsStockManagedModuleDefault
|
|
154
|
+
};
|
|
155
|
+
//# sourceMappingURL=policyResolution.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/availability/lib/policyResolution.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * `AvailabilityPolicy` resolution chain \u2014 six levels, most specific wins.\n *\n * @see .ai/specs/2026-08-14-availability-contract.md \u00A75.2\n *\n * Resolution design (documented here because \u00A75.2 does not fully spell it\n * out \u2014 see PLAN.md \u00A7 Key design decisions):\n *\n * - Boolean columns (`isStockManaged`, `allowBackorder`, `hideWhenOutOfStock`,\n * `isActive`) are NOT NULL, so a matched row's own value is always\n * concrete \u2014 there is no \"unset\" to fall through. The single most specific\n * EXISTING row decides every boolean field.\n * - Nullable columns (`backorderLeadTimeDays`, `preorderReleaseAt`,\n * `lowStockThreshold`, `minOrderQuantity`, `maxOrderQuantity`,\n * `quantityIncrement`) genuinely cascade: a matched row that leaves the\n * field `null` defers to the next-less-specific row, per US-A2's \"which\n * level currently decides each field\" requirement.\n *\n * `resolveMany()` exists so `wms`'s batched `check()` (\u00A74.2/R4 \u2014 one policy\n * lookup regardless of item count) can soft-resolve this service and still\n * issue exactly one query for the whole batch, rather than one per item.\n * `resolve()` is `resolveMany([scope])[0]`, so single-item callers (the admin\n * check tool, the resolve-preview endpoint) are unaffected.\n */\n\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { AvailabilityPolicy } from '../data/entities'\nimport { tryResolve } from './tryResolve'\n\nexport type PolicyResolutionScope = {\n tenantId: string\n organizationId: string\n storeId?: string | null\n productId: string\n variantId?: string | null\n}\n\nexport type ResolvedField<T> = {\n value: T\n /** The `AvailabilityPolicy` row id that decided, or `null` for the module default. */\n policySourceId: string | null\n}\n\nexport type ResolvedAvailabilityPolicy = {\n isStockManaged: ResolvedField<boolean>\n allowBackorder: ResolvedField<boolean>\n backorderLeadTimeDays: ResolvedField<number | null>\n preorderReleaseAt: ResolvedField<Date | null>\n lowStockThreshold: ResolvedField<number | null>\n minOrderQuantity: ResolvedField<number | null>\n maxOrderQuantity: ResolvedField<number | null>\n quantityIncrement: ResolvedField<number | null>\n hideWhenOutOfStock: ResolvedField<boolean>\n isActive: ResolvedField<boolean>\n}\n\n/** The six chain levels, most specific first. `null` = \"no row matched this level\". */\nexport type PolicyChainLevel =\n | 'variant_store'\n | 'variant'\n | 'product_store'\n | 'product'\n | 'store_default'\n | 'module_default'\n\nexport const POLICY_CHAIN_LEVELS: PolicyChainLevel[] = [\n 'variant_store',\n 'variant',\n 'product_store',\n 'product',\n 'store_default',\n 'module_default',\n]\n\nconst NULLABLE_FIELDS = [\n 'backorderLeadTimeDays',\n 'preorderReleaseAt',\n 'lowStockThreshold',\n 'minOrderQuantity',\n 'maxOrderQuantity',\n 'quantityIncrement',\n] as const\n\nconst BOOLEAN_FIELDS = ['isStockManaged', 'allowBackorder', 'hideWhenOutOfStock', 'isActive'] as const\n\nfunction candidateFilters(scope: PolicyResolutionScope): Array<Record<string, unknown>> {\n const storeId = scope.storeId ?? null\n const variantId = scope.variantId ?? null\n const filters: Array<Record<string, unknown>> = []\n if (variantId) {\n filters.push({ variantId, storeId })\n filters.push({ variantId, storeId: null })\n }\n filters.push({ productId: scope.productId, variantId: null, storeId })\n filters.push({ productId: scope.productId, variantId: null, storeId: null })\n filters.push({ productId: null, variantId: null, storeId })\n if (storeId) filters.push({ productId: null, variantId: null, storeId: null })\n return filters\n}\n\n/** Picks the (up to) five candidate rows for one scope out of a shared, already-loaded candidate pool. */\nfunction matchChainRows(scope: PolicyResolutionScope, pool: AvailabilityPolicy[]): Array<AvailabilityPolicy | null> {\n const storeId = scope.storeId ?? null\n const variantId = scope.variantId ?? null\n const findMatch = (predicate: (row: AvailabilityPolicy) => boolean) => pool.find(predicate) ?? null\n\n const rows: Array<AvailabilityPolicy | null> = []\n if (variantId) {\n rows.push(findMatch((r) => r.variantId === variantId && (r.storeId ?? null) === storeId))\n rows.push(findMatch((r) => r.variantId === variantId && (r.storeId ?? null) === null))\n }\n rows.push(findMatch((r) => r.productId === scope.productId && !r.variantId && (r.storeId ?? null) === storeId))\n rows.push(findMatch((r) => r.productId === scope.productId && !r.variantId && (r.storeId ?? null) === null))\n rows.push(findMatch((r) => !r.productId && !r.variantId && (r.storeId ?? null) === storeId))\n // The store-default level cascades store-specific \u2192 all-stores, same as\n // the variant/product levels above it (\"store_id null = applies to all\n // stores in the organization\", \u00A75.1) \u2014 otherwise an org-wide default row\n // is invisible to any query that names a concrete store.\n if (storeId) rows.push(findMatch((r) => !r.productId && !r.variantId && (r.storeId ?? null) === null))\n return rows\n}\n\nfunction resolveFromRows(\n rows: Array<AvailabilityPolicy | null>,\n isStockManagedModuleDefault: boolean,\n): ResolvedAvailabilityPolicy {\n const firstRow = rows.find((row): row is AvailabilityPolicy => row != null) ?? null\n const result = {} as ResolvedAvailabilityPolicy\n\n for (const field of BOOLEAN_FIELDS) {\n if (firstRow) {\n ;(result as any)[field] = { value: (firstRow as any)[field], policySourceId: firstRow.id }\n } else {\n ;(result as any)[field] = { value: false, policySourceId: null }\n }\n }\n\n // is_stock_managed's module default is dynamic (wms + profile existence), not a flat `false`.\n if (!firstRow) result.isStockManaged = { value: isStockManagedModuleDefault, policySourceId: null }\n // is_active's module default is \"active\" (nothing to deactivate).\n if (!firstRow) result.isActive = { value: true, policySourceId: null }\n\n for (const field of NULLABLE_FIELDS) {\n let resolved: ResolvedField<unknown> = { value: null, policySourceId: null }\n for (const row of rows) {\n if (row && (row as any)[field] != null) {\n resolved = { value: (row as any)[field], policySourceId: row.id }\n break\n }\n }\n ;(result as any)[field] = resolved\n }\n\n return result\n}\n\n/** `is_stock_managed`'s module default: true when `wms` is enabled and a `ProductInventoryProfile` exists for the item. */\nexport async function resolveIsStockManagedModuleDefault(\n em: EntityManager,\n container: { resolve: <T = unknown>(name: string) => T },\n scope: { tenantId: string; organizationId: string; productId: string; variantId?: string | null },\n): Promise<boolean> {\n const ProductInventoryProfile = tryResolve<new () => unknown>(container, 'ProductInventoryProfile')\n if (!ProductInventoryProfile) return false\n\n const baseFilter = {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n catalogProductId: scope.productId,\n deletedAt: null,\n }\n\n if (scope.variantId) {\n const variantRow = await em.findOne(ProductInventoryProfile as any, { ...baseFilter, catalogVariantId: scope.variantId })\n if (variantRow) return true\n }\n\n const productRow = await em.findOne(ProductInventoryProfile as any, { ...baseFilter, catalogVariantId: null })\n return !!productRow\n}\n\n/** Batched module-default check for `resolveMany` \u2014 one query for every scope that might need it. */\nasync function resolveIsStockManagedModuleDefaultMany(\n em: EntityManager,\n container: { resolve: <T = unknown>(name: string) => T },\n scopes: PolicyResolutionScope[],\n): Promise<Map<string, boolean>> {\n const ProductInventoryProfile = tryResolve<new () => unknown>(container, 'ProductInventoryProfile')\n const results = new Map<string, boolean>()\n if (!ProductInventoryProfile || scopes.length === 0) return results\n\n const first = scopes[0]\n const productIds = Array.from(new Set(scopes.map((s) => s.productId)))\n const rows = (await em.find(ProductInventoryProfile as any, {\n tenantId: first.tenantId,\n organizationId: first.organizationId,\n deletedAt: null,\n catalogProductId: { $in: productIds },\n } as any)) as Array<{ catalogProductId: string; catalogVariantId: string | null }>\n\n const exists = new Set(rows.map((row) => `${row.catalogProductId}:${row.catalogVariantId ?? ''}`))\n for (const scope of scopes) {\n const variantHit = scope.variantId ? exists.has(`${scope.productId}:${scope.variantId}`) : false\n const productHit = exists.has(`${scope.productId}:`)\n results.set(scopeKey(scope), variantHit || productHit)\n }\n return results\n}\n\nfunction scopeKey(scope: PolicyResolutionScope): string {\n return `${scope.productId}:${scope.variantId ?? ''}:${scope.storeId ?? ''}`\n}\n\nexport interface PolicyResolutionService {\n resolve(em: EntityManager, scope: PolicyResolutionScope): Promise<ResolvedAvailabilityPolicy>\n resolveMany(em: EntityManager, scopes: PolicyResolutionScope[]): Promise<ResolvedAvailabilityPolicy[]>\n}\n\nexport function createPolicyResolutionService(container: {\n resolve: <T = unknown>(name: string) => T\n}): PolicyResolutionService {\n async function resolveMany(em: EntityManager, scopes: PolicyResolutionScope[]): Promise<ResolvedAvailabilityPolicy[]> {\n if (scopes.length === 0) return []\n\n const first = scopes[0]\n const seen = new Set<string>()\n const orFilters: Array<Record<string, unknown>> = []\n for (const scope of scopes) {\n for (const filter of candidateFilters(scope)) {\n const key = JSON.stringify(filter)\n if (seen.has(key)) continue\n seen.add(key)\n orFilters.push(filter)\n }\n }\n\n const pool = orFilters.length\n ? await em.find(AvailabilityPolicy, {\n tenantId: first.tenantId,\n organizationId: first.organizationId,\n deletedAt: null,\n $or: orFilters as any,\n })\n : []\n\n // Only scopes that resolve to no row at all need the dynamic is_stock_managed\n // module default \u2014 batch just those.\n const rowsByScope = scopes.map((scope) => matchChainRows(scope, pool))\n const needsModuleDefault = scopes.filter((scope, index) => !rowsByScope[index].some((row) => row != null))\n const moduleDefaults = await resolveIsStockManagedModuleDefaultMany(em, container, needsModuleDefault)\n\n return scopes.map((scope, index) =>\n resolveFromRows(rowsByScope[index], moduleDefaults.get(scopeKey(scope)) ?? false),\n )\n }\n\n return {\n async resolve(em, scope) {\n const [result] = await resolveMany(em, [scope])\n return result\n },\n resolveMany,\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/availability/lib/tryResolve.ts"],
|
|
4
|
+
"sourcesContent": ["/** Per-module local soft-resolve helper \u2014 see `packages/core/AGENTS.md` \u00A7 Cross-Module Coupling. */\nexport function tryResolve<T>(resolver: { resolve: <R = unknown>(name: string) => R }, name: string): T | undefined {\n try {\n return resolver.resolve<T>(name)\n } catch {\n return undefined\n }\n}\n"],
|
|
5
|
+
"mappings": "AACO,SAAS,WAAc,UAAyD,MAA6B;AAClH,MAAI;AACF,WAAO,SAAS,QAAW,IAAI;AAAA,EACjC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Migration } from "@mikro-orm/migrations";
|
|
2
|
+
class Migration20260922095844_availability extends Migration {
|
|
3
|
+
constructor() {
|
|
4
|
+
super(...arguments);
|
|
5
|
+
this.name = "Migration20260922095844";
|
|
6
|
+
}
|
|
7
|
+
up() {
|
|
8
|
+
this.addSql(`create table "availability_policies" ("id" uuid not null default gen_random_uuid(), "organization_id" uuid not null, "tenant_id" uuid not null, "store_id" uuid null, "product_id" uuid null, "variant_id" uuid null, "is_stock_managed" boolean not null default false, "allow_backorder" boolean not null default false, "backorder_lead_time_days" int null, "preorder_release_at" timestamptz null, "low_stock_threshold" int null, "min_order_quantity" int null, "max_order_quantity" int null, "quantity_increment" int null, "hide_when_out_of_stock" boolean not null default false, "is_active" boolean not null default true, "created_at" timestamptz not null, "updated_at" timestamptz not null, "deleted_at" timestamptz null, primary key ("id"));`);
|
|
9
|
+
this.addSql(`create unique index "availability_policies_scope_target_unique" on "availability_policies" ("tenant_id", "organization_id", "store_id", "product_id", "variant_id") nulls not distinct where deleted_at is null;`);
|
|
10
|
+
this.addSql(`create index "availability_policies_scope_idx" on "availability_policies" ("tenant_id", "organization_id");`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export {
|
|
14
|
+
Migration20260922095844_availability
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=Migration20260922095844_availability.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/availability/migrations/Migration20260922095844_availability.ts"],
|
|
4
|
+
"sourcesContent": ["import { Migration } from '@mikro-orm/migrations';\n\nexport class Migration20260922095844_availability extends Migration {\n\n override name = 'Migration20260922095844';\n\n override up(): void | Promise<void> {\n this.addSql(`create table \"availability_policies\" (\"id\" uuid not null default gen_random_uuid(), \"organization_id\" uuid not null, \"tenant_id\" uuid not null, \"store_id\" uuid null, \"product_id\" uuid null, \"variant_id\" uuid null, \"is_stock_managed\" boolean not null default false, \"allow_backorder\" boolean not null default false, \"backorder_lead_time_days\" int null, \"preorder_release_at\" timestamptz null, \"low_stock_threshold\" int null, \"min_order_quantity\" int null, \"max_order_quantity\" int null, \"quantity_increment\" int null, \"hide_when_out_of_stock\" boolean not null default false, \"is_active\" boolean not null default true, \"created_at\" timestamptz not null, \"updated_at\" timestamptz not null, \"deleted_at\" timestamptz null, primary key (\"id\"));`);\n this.addSql(`create unique index \"availability_policies_scope_target_unique\" on \"availability_policies\" (\"tenant_id\", \"organization_id\", \"store_id\", \"product_id\", \"variant_id\") nulls not distinct where deleted_at is null;`);\n this.addSql(`create index \"availability_policies_scope_idx\" on \"availability_policies\" (\"tenant_id\", \"organization_id\");`);\n }\n\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,iBAAiB;AAEnB,MAAM,6CAA6C,UAAU;AAAA,EAA7D;AAAA;AAEL,SAAS,OAAO;AAAA;AAAA,EAEP,KAA2B;AAClC,SAAK,OAAO,ouBAAouB;AAChvB,SAAK,OAAO,kNAAkN;AAC9N,SAAK,OAAO,6GAA6G;AAAA,EAC3H;AAEF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/modules/availability/setup.ts"],
|
|
4
|
+
"sourcesContent": ["import type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'\n\nexport const setup: ModuleSetupConfig = {\n defaultRoleFeatures: {\n admin: ['availability.*'],\n employee: ['availability.policies.view'],\n },\n}\n\nexport default setup\n"],
|
|
5
|
+
"mappings": "AAEO,MAAM,QAA2B;AAAA,EACtC,qBAAqB;AAAA,IACnB,OAAO,CAAC,gBAAgB;AAAA,IACxB,UAAU,CAAC,4BAA4B;AAAA,EACzC;AACF;AAEA,IAAO,gBAAQ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -14,6 +14,7 @@ import { createKmsService } from "@open-mercato/shared/lib/encryption/kms";
|
|
|
14
14
|
import {
|
|
15
15
|
decryptWithAesGcm,
|
|
16
16
|
decryptWithAesGcmStrict,
|
|
17
|
+
isEncryptedPayloadShape,
|
|
17
18
|
TenantDataEncryptionError,
|
|
18
19
|
TenantDataEncryptionErrorCode
|
|
19
20
|
} from "@open-mercato/shared/lib/encryption/aes";
|
|
@@ -381,11 +382,6 @@ function resolveMapMeta(map, metaByEntityId, warn = () => {
|
|
|
381
382
|
if (!tenantId) return null;
|
|
382
383
|
return { entityId, meta, fields, tenantId };
|
|
383
384
|
}
|
|
384
|
-
function isEncryptedPayload(value) {
|
|
385
|
-
if (typeof value !== "string") return false;
|
|
386
|
-
const parts = value.split(":");
|
|
387
|
-
return parts.length === 4 && parts[3] === "v1";
|
|
388
|
-
}
|
|
389
385
|
function formatValueForColumn(prop, value) {
|
|
390
386
|
if (value === null || value === void 0) return value;
|
|
391
387
|
const types = Array.isArray(prop?.columnTypes) ? prop.columnTypes : [];
|
|
@@ -474,6 +470,7 @@ const rotateEncryptionKey = {
|
|
|
474
470
|
return scopes;
|
|
475
471
|
};
|
|
476
472
|
const oldDekCache = /* @__PURE__ */ new Map();
|
|
473
|
+
const unrecoverableRows = [];
|
|
477
474
|
const dekAvailability = /* @__PURE__ */ new Map();
|
|
478
475
|
const hasExistingDek = async (tenantId) => {
|
|
479
476
|
const cached = dekAvailability.get(tenantId);
|
|
@@ -510,6 +507,7 @@ const rotateEncryptionKey = {
|
|
|
510
507
|
const list = Array.isArray(rows) ? rows : [];
|
|
511
508
|
if (!list.length) return 0;
|
|
512
509
|
const dekAvailable = dryRun ? await hasExistingDek(scope.tenantId) : true;
|
|
510
|
+
const currentDek = rotate && oldKms ? await encryptionService.getDek(scope.tenantId) : null;
|
|
513
511
|
let updated = 0;
|
|
514
512
|
for (const row of list) {
|
|
515
513
|
const payload = {};
|
|
@@ -518,7 +516,7 @@ const rotateEncryptionKey = {
|
|
|
518
516
|
const col = resolved?.columnName;
|
|
519
517
|
if (!col) continue;
|
|
520
518
|
const rawValue = row[col];
|
|
521
|
-
if (rotate
|
|
519
|
+
if (isEncryptedPayloadShape(rawValue) ? !rotate : rotate) {
|
|
522
520
|
continue;
|
|
523
521
|
}
|
|
524
522
|
payload[rule.field] = rawValue;
|
|
@@ -539,10 +537,19 @@ const rotateEncryptionKey = {
|
|
|
539
537
|
}
|
|
540
538
|
for (const rule of fields) {
|
|
541
539
|
const value = payload[rule.field];
|
|
542
|
-
if (typeof value !== "string" || !
|
|
540
|
+
if (typeof value !== "string" || !isEncryptedPayloadShape(value)) continue;
|
|
543
541
|
const decrypted = decryptWithOldKey(value, oldDek);
|
|
544
|
-
if (decrypted
|
|
545
|
-
|
|
542
|
+
if (decrypted !== null) {
|
|
543
|
+
payload[rule.field] = parseDecryptedFieldValue(decrypted);
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
if (currentDek && decryptWithAesGcm(value, currentDek.key) !== null) continue;
|
|
547
|
+
delete payload[rule.field];
|
|
548
|
+
if (rule.hashField) delete payload[rule.hashField];
|
|
549
|
+
unrecoverableRows.push({ entityId, field: rule.field, rowId: row[pk] });
|
|
550
|
+
console.warn(
|
|
551
|
+
`Skipping ${entityId}.${rule.field} for row ${row[pk]}: its ciphertext opens under neither --old-key nor the current tenant key. Re-run with the key that sealed it.`
|
|
552
|
+
);
|
|
546
553
|
}
|
|
547
554
|
}
|
|
548
555
|
if (!dekAvailable) {
|
|
@@ -551,7 +558,7 @@ const rotateEncryptionKey = {
|
|
|
551
558
|
if (!col) return false;
|
|
552
559
|
const value = row[col];
|
|
553
560
|
if (value === null || value === void 0) return false;
|
|
554
|
-
return rotate ?
|
|
561
|
+
return rotate ? isEncryptedPayloadShape(value) : !isEncryptedPayloadShape(value);
|
|
555
562
|
});
|
|
556
563
|
if (wouldChange) updated += 1;
|
|
557
564
|
continue;
|
|
@@ -570,7 +577,7 @@ const rotateEncryptionKey = {
|
|
|
570
577
|
if (!col) continue;
|
|
571
578
|
const nextValue = encrypted[rule.field];
|
|
572
579
|
if (nextValue !== void 0 && nextValue !== row[col]) {
|
|
573
|
-
if (!rotate &&
|
|
580
|
+
if (!rotate && isEncryptedPayloadShape(row[col])) continue;
|
|
574
581
|
updates[col] = formatValueForColumn(resolved?.prop, nextValue);
|
|
575
582
|
}
|
|
576
583
|
if (rule.hashField) {
|
|
@@ -615,6 +622,19 @@ const rotateEncryptionKey = {
|
|
|
615
622
|
} else {
|
|
616
623
|
console.log("All mapped entity fields already encrypted for the selected scope.");
|
|
617
624
|
}
|
|
625
|
+
if (unrecoverableRows.length) {
|
|
626
|
+
const maxListed = 20;
|
|
627
|
+
console.log(
|
|
628
|
+
`
|
|
629
|
+
\u26A0\uFE0F ${unrecoverableRows.length} field(s) could not be rotated \u2014 their ciphertext opens under neither --old-key nor the current tenant key, so they were left untouched. See "Key management" in apps/docs/docs/architecture/data-encryption.mdx for how to resolve these manually.`
|
|
630
|
+
);
|
|
631
|
+
for (const row of unrecoverableRows.slice(0, maxListed)) {
|
|
632
|
+
console.log(` - ${row.entityId}.${row.field} row=${String(row.rowId)}`);
|
|
633
|
+
}
|
|
634
|
+
if (unrecoverableRows.length > maxListed) {
|
|
635
|
+
console.log(` ... and ${unrecoverableRows.length - maxListed} more`);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
618
638
|
}
|
|
619
639
|
};
|
|
620
640
|
const decryptDatabase = {
|
|
@@ -1044,8 +1064,9 @@ const backfillSystemEncryption = {
|
|
|
1044
1064
|
const resolved = resolveProperty(meta, rule.field);
|
|
1045
1065
|
if (!resolved.columnName) continue;
|
|
1046
1066
|
const rawValue = row[resolved.columnName];
|
|
1067
|
+
if (rawValue !== null && rawValue !== void 0 && isEncryptedPayloadShape(rawValue)) continue;
|
|
1047
1068
|
payload[rule.field] = rawValue;
|
|
1048
|
-
if (rawValue !== null && rawValue !== void 0
|
|
1069
|
+
if (rawValue !== null && rawValue !== void 0) hasPlaintext = true;
|
|
1049
1070
|
if (rule.hashField) {
|
|
1050
1071
|
const resolvedHash = resolveProperty(meta, rule.hashField);
|
|
1051
1072
|
if (resolvedHash.columnName) payload[rule.hashField] = row[resolvedHash.columnName];
|