@open-mercato/core 0.6.7-develop.6696.1.4236709f7e → 0.6.7-develop.6725.1.07a748cfc2
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 +1 -1
- package/dist/modules/auth/api/admin/nav.js +14 -4
- package/dist/modules/auth/api/admin/nav.js.map +2 -2
- package/dist/modules/auth/api/users/route.js +8 -19
- package/dist/modules/auth/api/users/route.js.map +2 -2
- package/dist/modules/auth/frontend/login.js +4 -1
- package/dist/modules/auth/frontend/login.js.map +2 -2
- package/dist/modules/auth/lib/backendChrome.js +31 -14
- package/dist/modules/auth/lib/backendChrome.js.map +2 -2
- package/dist/modules/customers/api/companies/[id]/route.js +3 -4
- package/dist/modules/customers/api/companies/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/companies/enriched/route.js +3 -2
- package/dist/modules/customers/api/people/[id]/companies/enriched/route.js.map +2 -2
- package/dist/modules/customers/api/utils.js +12 -18
- package/dist/modules/customers/api/utils.js.map +2 -2
- package/dist/modules/customers/cli.js +11 -4
- package/dist/modules/customers/cli.js.map +2 -2
- package/dist/modules/customers/components/detail/ActiveDealCard.js +2 -1
- package/dist/modules/customers/components/detail/ActiveDealCard.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyKpiBar.js +6 -5
- package/dist/modules/customers/components/detail/CompanyKpiBar.js.map +2 -2
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +4 -2
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/dashboard/helpers.js +4 -3
- package/dist/modules/customers/components/detail/dashboard/helpers.js.map +2 -2
- package/dist/modules/customers/components/formConfig.js +14 -10
- package/dist/modules/customers/components/formConfig.js.map +2 -2
- package/dist/modules/customers/lib/dealStatus.js +38 -0
- package/dist/modules/customers/lib/dealStatus.js.map +7 -0
- package/dist/modules/dashboards/api/widgets/data/batch/route.js +4 -2
- package/dist/modules/dashboards/api/widgets/data/batch/route.js.map +2 -2
- package/dist/modules/dashboards/api/widgets/data/route.js +20 -2
- package/dist/modules/dashboards/api/widgets/data/route.js.map +2 -2
- package/dist/modules/dashboards/lib/aggregations.js +76 -40
- package/dist/modules/dashboards/lib/aggregations.js.map +2 -2
- package/dist/modules/dashboards/lib/exactDecimal.js +69 -0
- package/dist/modules/dashboards/lib/exactDecimal.js.map +7 -0
- package/dist/modules/dashboards/services/widgetDataService.js +228 -5
- package/dist/modules/dashboards/services/widgetDataService.js.map +2 -2
- package/dist/modules/feature_toggles/components/FeatureGuard.js +3 -2
- package/dist/modules/feature_toggles/components/FeatureGuard.js.map +2 -2
- package/dist/modules/feature_toggles/components/hooks/staleTime.js +5 -0
- package/dist/modules/feature_toggles/components/hooks/staleTime.js.map +7 -0
- package/dist/modules/feature_toggles/components/hooks/useFeatureFlagBoolean.js +5 -2
- package/dist/modules/feature_toggles/components/hooks/useFeatureFlagBoolean.js.map +2 -2
- package/dist/modules/feature_toggles/components/hooks/useFeatureFlagJson.js +5 -2
- package/dist/modules/feature_toggles/components/hooks/useFeatureFlagJson.js.map +2 -2
- package/dist/modules/feature_toggles/components/hooks/useFeatureFlagNumber.js +5 -2
- package/dist/modules/feature_toggles/components/hooks/useFeatureFlagNumber.js.map +2 -2
- package/dist/modules/feature_toggles/components/hooks/useFeatureFlagString.js +5 -2
- package/dist/modules/feature_toggles/components/hooks/useFeatureFlagString.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/route.js +16 -1
- package/dist/modules/inbox_ops/api/proposals/route.js.map +2 -2
- package/dist/modules/inbox_ops/subscribers/search-reindex-proposal-created.js +32 -0
- package/dist/modules/inbox_ops/subscribers/search-reindex-proposal-created.js.map +7 -0
- package/dist/modules/integrations/widgets/injection/external-ids/widget.client.js +0 -5
- package/dist/modules/integrations/widgets/injection/external-ids/widget.client.js.map +2 -2
- package/dist/modules/integrations/widgets/injection/external-ids/widget.js +14 -0
- package/dist/modules/integrations/widgets/injection/external-ids/widget.js.map +7 -0
- package/dist/modules/messages/lib/searchLookup.js +8 -16
- package/dist/modules/messages/lib/searchLookup.js.map +2 -2
- package/dist/modules/payment_gateways/api/sessions/route.js +5 -0
- package/dist/modules/payment_gateways/api/sessions/route.js.map +2 -2
- package/dist/modules/payment_gateways/di.js +17 -1
- package/dist/modules/payment_gateways/di.js.map +2 -2
- package/dist/modules/payment_gateways/lib/gateway-service.js +8 -0
- package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
- package/dist/modules/payment_gateways/lib/order-amount-reconciliation.js +58 -0
- package/dist/modules/payment_gateways/lib/order-amount-reconciliation.js.map +7 -0
- package/dist/modules/query_index/api/openapi.js +13 -1
- package/dist/modules/query_index/api/openapi.js.map +2 -2
- package/dist/modules/query_index/api/status.js +165 -105
- package/dist/modules/query_index/api/status.js.map +2 -2
- package/dist/modules/query_index/components/QueryIndexesTable.js +128 -41
- package/dist/modules/query_index/components/QueryIndexesTable.js.map +2 -2
- package/dist/modules/query_index/lib/batch.js +11 -4
- package/dist/modules/query_index/lib/batch.js.map +2 -2
- package/dist/modules/query_index/lib/document.js +12 -5
- package/dist/modules/query_index/lib/document.js.map +2 -2
- package/dist/modules/query_index/lib/engine.js +36 -4
- package/dist/modules/query_index/lib/engine.js.map +2 -2
- package/dist/modules/query_index/lib/indexer.js +1 -1
- package/dist/modules/query_index/lib/indexer.js.map +2 -2
- package/dist/modules/query_index/lib/search-tokens.js +7 -4
- package/dist/modules/query_index/lib/search-tokens.js.map +2 -2
- package/dist/modules/sales/api/documents/factory.js +14 -3
- package/dist/modules/sales/api/documents/factory.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +22 -1
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/components/useSalesChannelsEnabled.js +2 -35
- package/dist/modules/sales/components/useSalesChannelsEnabled.js.map +2 -2
- package/dist/modules/sales/data/validators.js +25 -4
- package/dist/modules/sales/data/validators.js.map +2 -2
- package/dist/modules/sales/di.js +7 -0
- package/dist/modules/sales/di.js.map +2 -2
- package/dist/modules/sales/services/paymentOrderTotalResolver.js +52 -0
- package/dist/modules/sales/services/paymentOrderTotalResolver.js.map +7 -0
- package/package.json +7 -7
- package/src/modules/auth/api/admin/nav.ts +24 -4
- package/src/modules/auth/api/users/route.ts +8 -28
- package/src/modules/auth/frontend/login.tsx +4 -1
- package/src/modules/auth/lib/backendChrome.tsx +51 -13
- package/src/modules/customers/api/companies/[id]/route.ts +3 -4
- package/src/modules/customers/api/people/[id]/companies/enriched/route.ts +3 -2
- package/src/modules/customers/api/utils.ts +12 -31
- package/src/modules/customers/cli.ts +11 -4
- package/src/modules/customers/components/detail/ActiveDealCard.tsx +2 -1
- package/src/modules/customers/components/detail/CompanyKpiBar.tsx +6 -5
- package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +4 -0
- package/src/modules/customers/components/detail/dashboard/helpers.ts +4 -3
- package/src/modules/customers/components/formConfig.tsx +12 -8
- package/src/modules/customers/lib/dealStatus.ts +48 -0
- package/src/modules/dashboards/api/widgets/data/batch/route.ts +4 -0
- package/src/modules/dashboards/api/widgets/data/route.ts +21 -0
- package/src/modules/dashboards/lib/aggregations.ts +122 -43
- package/src/modules/dashboards/lib/exactDecimal.ts +96 -0
- package/src/modules/dashboards/services/widgetDataService.ts +342 -4
- package/src/modules/feature_toggles/components/FeatureGuard.tsx +9 -2
- package/src/modules/feature_toggles/components/hooks/staleTime.ts +5 -0
- package/src/modules/feature_toggles/components/hooks/useFeatureFlagBoolean.ts +13 -1
- package/src/modules/feature_toggles/components/hooks/useFeatureFlagJson.ts +12 -3
- package/src/modules/feature_toggles/components/hooks/useFeatureFlagNumber.ts +10 -1
- package/src/modules/feature_toggles/components/hooks/useFeatureFlagString.ts +10 -1
- package/src/modules/inbox_ops/api/proposals/route.ts +20 -1
- package/src/modules/inbox_ops/subscribers/search-reindex-proposal-created.ts +39 -0
- package/src/modules/integrations/widgets/injection/external-ids/widget.client.tsx +0 -5
- package/src/modules/integrations/widgets/injection/external-ids/widget.ts +13 -0
- package/src/modules/messages/lib/searchLookup.ts +11 -34
- package/src/modules/payment_gateways/api/sessions/route.ts +5 -0
- package/src/modules/payment_gateways/di.ts +28 -2
- package/src/modules/payment_gateways/i18n/de.json +3 -0
- package/src/modules/payment_gateways/i18n/en.json +3 -0
- package/src/modules/payment_gateways/i18n/es.json +3 -0
- package/src/modules/payment_gateways/i18n/pl.json +3 -0
- package/src/modules/payment_gateways/lib/gateway-service.ts +14 -0
- package/src/modules/payment_gateways/lib/order-amount-reconciliation.ts +93 -0
- package/src/modules/query_index/api/openapi.ts +12 -0
- package/src/modules/query_index/api/status.ts +238 -136
- package/src/modules/query_index/components/QueryIndexesTable.tsx +192 -46
- package/src/modules/query_index/i18n/de.json +5 -3
- package/src/modules/query_index/i18n/en.json +5 -3
- package/src/modules/query_index/i18n/es.json +5 -3
- package/src/modules/query_index/i18n/pl.json +5 -3
- package/src/modules/query_index/lib/batch.ts +11 -4
- package/src/modules/query_index/lib/document.ts +35 -4
- package/src/modules/query_index/lib/engine.ts +53 -5
- package/src/modules/query_index/lib/indexer.ts +1 -1
- package/src/modules/query_index/lib/search-tokens.ts +13 -4
- package/src/modules/sales/api/documents/factory.ts +28 -3
- package/src/modules/sales/commands/documents.ts +26 -2
- package/src/modules/sales/components/useSalesChannelsEnabled.ts +2 -48
- package/src/modules/sales/data/validators.ts +50 -5
- package/src/modules/sales/di.ts +9 -0
- package/src/modules/sales/services/paymentOrderTotalResolver.ts +67 -0
|
@@ -10,24 +10,93 @@ import {
|
|
|
10
10
|
determineChangeDirection,
|
|
11
11
|
isValidDateRangePreset
|
|
12
12
|
} from "@open-mercato/ui/backend/date-range";
|
|
13
|
+
import { parseDecryptedFieldValue } from "@open-mercato/shared/lib/encryption/tenantDataEncryptionService";
|
|
14
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
15
|
+
import { isTenantDataEncryptionEnabled } from "@open-mercato/shared/lib/encryption/toggles";
|
|
13
16
|
import {
|
|
14
|
-
buildAggregationQuery
|
|
17
|
+
buildAggregationQuery,
|
|
18
|
+
buildGroupSourceRowsQuery,
|
|
19
|
+
resolveGroupExpression
|
|
15
20
|
} from "../lib/aggregations.js";
|
|
21
|
+
import {
|
|
22
|
+
EXACT_DECIMAL_ZERO,
|
|
23
|
+
addExactDecimal,
|
|
24
|
+
compareExactDecimal,
|
|
25
|
+
exactDecimalToNumber,
|
|
26
|
+
parseExactDecimal
|
|
27
|
+
} from "../lib/exactDecimal.js";
|
|
16
28
|
const WIDGET_DATA_CACHE_TTL = 12e4;
|
|
17
29
|
const WIDGET_DATA_SEGMENT_TTL = 864e5;
|
|
18
30
|
const WIDGET_DATA_SEGMENT_KEY = "widget-data:__segment__";
|
|
31
|
+
const ENCRYPTED_GROUP_SCAN_LIMIT = 2e4;
|
|
19
32
|
const SAFE_IDENTIFIER_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
33
|
+
const logger = createLogger("dashboards").child({ component: "widget-data" });
|
|
20
34
|
class WidgetDataValidationError extends Error {
|
|
21
35
|
constructor(message) {
|
|
22
36
|
super(message);
|
|
23
37
|
this.name = "WidgetDataValidationError";
|
|
24
38
|
}
|
|
25
39
|
}
|
|
40
|
+
class WidgetDataScanLimitError extends Error {
|
|
41
|
+
constructor(message) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.name = "WidgetDataScanLimitError";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
class WidgetDataEncryptionUnavailableError extends Error {
|
|
47
|
+
constructor(message) {
|
|
48
|
+
super(message);
|
|
49
|
+
this.name = "WidgetDataEncryptionUnavailableError";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
26
52
|
function assertSafeIdentifier(value, name) {
|
|
27
53
|
if (!SAFE_IDENTIFIER_PATTERN.test(value)) {
|
|
28
54
|
throw new Error(`Invalid ${name}: ${value}`);
|
|
29
55
|
}
|
|
30
56
|
}
|
|
57
|
+
function matchesColumn(fieldName, dbColumn) {
|
|
58
|
+
const normalize = (value) => value.replace(/_/g, "").toLowerCase();
|
|
59
|
+
return normalize(fieldName) === normalize(dbColumn);
|
|
60
|
+
}
|
|
61
|
+
function readJsonPath(value, path) {
|
|
62
|
+
let current = value;
|
|
63
|
+
for (const part of path.split(".")) {
|
|
64
|
+
if (current === null || typeof current !== "object") return null;
|
|
65
|
+
current = current[part];
|
|
66
|
+
}
|
|
67
|
+
return current;
|
|
68
|
+
}
|
|
69
|
+
function createGroupBucket() {
|
|
70
|
+
return { count: 0, numericCount: 0, sum: EXACT_DECIMAL_ZERO, min: null, max: null };
|
|
71
|
+
}
|
|
72
|
+
function foldMetricValue(bucket, value) {
|
|
73
|
+
bucket.numericCount += 1;
|
|
74
|
+
bucket.sum = addExactDecimal(bucket.sum, value);
|
|
75
|
+
if (bucket.min === null || compareExactDecimal(value, bucket.min) < 0) bucket.min = value;
|
|
76
|
+
if (bucket.max === null || compareExactDecimal(value, bucket.max) > 0) bucket.max = value;
|
|
77
|
+
}
|
|
78
|
+
function aggregateBucket(aggregate, bucket) {
|
|
79
|
+
switch (aggregate) {
|
|
80
|
+
case "count":
|
|
81
|
+
return bucket.count;
|
|
82
|
+
case "sum":
|
|
83
|
+
return bucket.numericCount === 0 ? 0 : exactDecimalToNumber(bucket.sum);
|
|
84
|
+
case "avg":
|
|
85
|
+
return bucket.numericCount === 0 ? 0 : exactDecimalToNumber(bucket.sum) / bucket.numericCount;
|
|
86
|
+
case "min":
|
|
87
|
+
return bucket.min === null ? null : exactDecimalToNumber(bucket.min);
|
|
88
|
+
case "max":
|
|
89
|
+
return bucket.max === null ? null : exactDecimalToNumber(bucket.max);
|
|
90
|
+
default:
|
|
91
|
+
return bucket.count;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function compareWidgetDataItemsByValueDesc(left, right) {
|
|
95
|
+
if (left.value === null && right.value === null) return 0;
|
|
96
|
+
if (left.value === null) return 1;
|
|
97
|
+
if (right.value === null) return -1;
|
|
98
|
+
return right.value - left.value;
|
|
99
|
+
}
|
|
31
100
|
class WidgetDataService {
|
|
32
101
|
constructor(options) {
|
|
33
102
|
this.em = options.em;
|
|
@@ -131,6 +200,12 @@ class WidgetDataService {
|
|
|
131
200
|
}
|
|
132
201
|
}
|
|
133
202
|
async executeQuery(request, dateRange) {
|
|
203
|
+
if (request.groupBy) {
|
|
204
|
+
const encryptedSource = await this.resolveEncryptedGroupSource(request.entityType, request.groupBy);
|
|
205
|
+
if (encryptedSource) {
|
|
206
|
+
return this.executeEncryptedGroupQuery(request, encryptedSource, dateRange);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
134
209
|
const query = buildAggregationQuery({
|
|
135
210
|
entityType: request.entityType,
|
|
136
211
|
metric: request.metric,
|
|
@@ -159,13 +234,150 @@ class WidgetDataService {
|
|
|
159
234
|
const singleValue = results[0]?.value !== void 0 ? Number(results[0].value) : null;
|
|
160
235
|
return { value: singleValue, data: [] };
|
|
161
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* Reports the encrypted column a groupBy field reads from, or null when the source is stored in
|
|
239
|
+
* plaintext and can be grouped by the database. Grouping over an encrypted column in SQL buckets
|
|
240
|
+
* ciphertext and renders it to the user (#4622).
|
|
241
|
+
*/
|
|
242
|
+
async resolveEncryptedGroupSource(entityType, groupBy) {
|
|
243
|
+
if (!isTenantDataEncryptionEnabled()) return null;
|
|
244
|
+
const resolved = resolveGroupExpression(this.registry, entityType, groupBy);
|
|
245
|
+
if (!resolved) return null;
|
|
246
|
+
const tableName = this.registry.getEntityTypeConfig(entityType)?.tableName;
|
|
247
|
+
if (!tableName) return null;
|
|
248
|
+
const entityId = this.resolveEncryptionEntityId(tableName);
|
|
249
|
+
if (!entityId) return null;
|
|
250
|
+
const encryptionService = resolveTenantEncryptionService(this.em);
|
|
251
|
+
if (!encryptionService) {
|
|
252
|
+
throw new WidgetDataEncryptionUnavailableError(
|
|
253
|
+
`Cannot determine whether ${groupBy.field} is encrypted: encryption service unavailable`
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
const organizationId = this.resolveOrganizationId();
|
|
257
|
+
let encryptedFields;
|
|
258
|
+
try {
|
|
259
|
+
encryptedFields = await encryptionService.getEncryptedFieldNames(
|
|
260
|
+
entityId,
|
|
261
|
+
this.scope.tenantId,
|
|
262
|
+
organizationId,
|
|
263
|
+
{ ignoreRuntimeHealth: true }
|
|
264
|
+
);
|
|
265
|
+
} catch (err) {
|
|
266
|
+
logger.error("Failed to resolve encrypted fields for widget grouping", { err, entityId, entityType });
|
|
267
|
+
throw new WidgetDataEncryptionUnavailableError(
|
|
268
|
+
`Cannot determine whether ${groupBy.field} is encrypted: encryption map lookup failed`
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
if (!encryptedFields.some((field) => matchesColumn(field, resolved.dbColumn))) return null;
|
|
272
|
+
if (resolved.jsonPath === null && groupBy.granularity) {
|
|
273
|
+
throw new WidgetDataValidationError(
|
|
274
|
+
`Cannot group encrypted field by granularity: ${groupBy.field}`
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
return { entityId, dbColumn: resolved.dbColumn, jsonPath: resolved.jsonPath };
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Aggregates an encrypted group source in application code: rows are scanned, the group column is
|
|
281
|
+
* decrypted, and only the resulting plaintext keys are grouped. Values that cannot be decrypted
|
|
282
|
+
* collapse into the null ("Unknown") bucket so ciphertext never reaches the response.
|
|
283
|
+
*/
|
|
284
|
+
async executeEncryptedGroupQuery(request, source, dateRange) {
|
|
285
|
+
const query = buildGroupSourceRowsQuery({
|
|
286
|
+
entityType: request.entityType,
|
|
287
|
+
metric: request.metric,
|
|
288
|
+
dateRange: dateRange && request.dateRange ? { field: request.dateRange.field, ...dateRange } : void 0,
|
|
289
|
+
filters: request.filters,
|
|
290
|
+
scope: this.scope,
|
|
291
|
+
registry: this.registry,
|
|
292
|
+
groupColumn: source.dbColumn,
|
|
293
|
+
rowLimit: ENCRYPTED_GROUP_SCAN_LIMIT
|
|
294
|
+
});
|
|
295
|
+
if (!query) {
|
|
296
|
+
throw new Error("Failed to build aggregation query");
|
|
297
|
+
}
|
|
298
|
+
const rows = await this.em.getConnection().execute(query.sql, query.params);
|
|
299
|
+
const results = Array.isArray(rows) ? rows : [];
|
|
300
|
+
if (results.length > ENCRYPTED_GROUP_SCAN_LIMIT) {
|
|
301
|
+
throw new WidgetDataScanLimitError(
|
|
302
|
+
`Too many rows to group encrypted field ${request.groupBy?.field} (limit ${ENCRYPTED_GROUP_SCAN_LIMIT})`
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
const encryptionService = resolveTenantEncryptionService(this.em);
|
|
306
|
+
let dek = null;
|
|
307
|
+
try {
|
|
308
|
+
dek = encryptionService ? await encryptionService.getDek(this.scope.tenantId) : null;
|
|
309
|
+
} catch (err) {
|
|
310
|
+
logger.error("Failed to resolve the tenant DEK for widget grouping", { err, entityId: source.entityId });
|
|
311
|
+
dek = null;
|
|
312
|
+
}
|
|
313
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
314
|
+
let undecryptableRows = 0;
|
|
315
|
+
for (const row of results) {
|
|
316
|
+
const resolvedKey = this.resolveDecryptedGroupKey(row.group_source, source, dek?.key ?? null);
|
|
317
|
+
if (resolvedKey === void 0) undecryptableRows += 1;
|
|
318
|
+
const groupKey = resolvedKey === void 0 ? null : resolvedKey;
|
|
319
|
+
let bucket = buckets.get(groupKey);
|
|
320
|
+
if (!bucket) {
|
|
321
|
+
bucket = createGroupBucket();
|
|
322
|
+
buckets.set(groupKey, bucket);
|
|
323
|
+
}
|
|
324
|
+
const metricValue = row.metric_value;
|
|
325
|
+
if (metricValue === null || metricValue === void 0) continue;
|
|
326
|
+
bucket.count += 1;
|
|
327
|
+
const numeric = parseExactDecimal(metricValue);
|
|
328
|
+
if (numeric) foldMetricValue(bucket, numeric);
|
|
329
|
+
}
|
|
330
|
+
if (undecryptableRows > 0) {
|
|
331
|
+
if (!dek) {
|
|
332
|
+
throw new WidgetDataEncryptionUnavailableError(
|
|
333
|
+
`Cannot group encrypted field ${request.groupBy?.field}: tenant encryption key is unavailable`
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
logger.warn("Grouped rows with an undecryptable group source as unknown", {
|
|
337
|
+
entityId: source.entityId,
|
|
338
|
+
column: source.dbColumn,
|
|
339
|
+
rows: undecryptableRows
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
let data = Array.from(buckets.entries()).map(([groupKey, bucket]) => ({ groupKey, value: aggregateBucket(request.metric.aggregate, bucket) })).sort(compareWidgetDataItemsByValueDesc);
|
|
343
|
+
if (request.groupBy?.limit && request.groupBy.limit > 0) {
|
|
344
|
+
data = data.slice(0, Math.min(request.groupBy.limit, 100));
|
|
345
|
+
}
|
|
346
|
+
if (request.groupBy?.resolveLabels) {
|
|
347
|
+
data = await this.resolveGroupLabels(data, request.entityType, request.groupBy.field);
|
|
348
|
+
}
|
|
349
|
+
const totalValue = data.reduce((sum, item) => sum + (item.value ?? 0), 0);
|
|
350
|
+
return { value: totalValue, data };
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Returns the plaintext group key for a scanned row, `null` for an empty/absent value, or
|
|
354
|
+
* `undefined` when the source is ciphertext that could not be decrypted.
|
|
355
|
+
*/
|
|
356
|
+
resolveDecryptedGroupKey(rawValue, source, dek) {
|
|
357
|
+
if (rawValue === null || rawValue === void 0) return null;
|
|
358
|
+
let value = rawValue;
|
|
359
|
+
if (typeof value === "string" && this.isEncryptedPayload(value)) {
|
|
360
|
+
if (!dek) return void 0;
|
|
361
|
+
const decrypted = this.decryptWithDek(value, dek);
|
|
362
|
+
if (decrypted === null) return void 0;
|
|
363
|
+
value = parseDecryptedFieldValue(decrypted);
|
|
364
|
+
}
|
|
365
|
+
if (source.jsonPath !== null) {
|
|
366
|
+
value = readJsonPath(value, source.jsonPath);
|
|
367
|
+
}
|
|
368
|
+
if (value === null || value === void 0 || value === "") return null;
|
|
369
|
+
if (typeof value === "object") return null;
|
|
370
|
+
const key = String(value);
|
|
371
|
+
return this.isEncryptedPayload(key) ? void 0 : key;
|
|
372
|
+
}
|
|
162
373
|
async resolveGroupLabels(data, entityType, groupByField) {
|
|
163
374
|
const config = this.registry.getLabelResolverConfig(entityType, groupByField);
|
|
164
375
|
if (!config) {
|
|
165
|
-
return data.map((item) =>
|
|
166
|
-
...item,
|
|
167
|
-
|
|
168
|
-
|
|
376
|
+
return data.map((item) => {
|
|
377
|
+
if (item.groupKey == null || item.groupKey === "") return { ...item, groupLabel: void 0 };
|
|
378
|
+
const label = String(item.groupKey);
|
|
379
|
+
return { ...item, groupLabel: this.isEncryptedPayload(label) ? void 0 : label };
|
|
380
|
+
});
|
|
169
381
|
}
|
|
170
382
|
const ids = data.map((item) => item.groupKey).filter((id) => {
|
|
171
383
|
if (typeof id !== "string" || id.length === 0) return false;
|
|
@@ -321,6 +533,15 @@ class WidgetDataService {
|
|
|
321
533
|
return null;
|
|
322
534
|
}
|
|
323
535
|
}
|
|
536
|
+
/**
|
|
537
|
+
* Resolves the encryption entity id for an analytics table. A request-scoped `EntityManager` fork
|
|
538
|
+
* frequently reports an empty metadata registry, which used to make every group source look
|
|
539
|
+
* unencrypted and send the request back to the ciphertext-grouping SQL path (#4622). The table
|
|
540
|
+
* name alone is enough for the entity-id lookup, so it is the fallback.
|
|
541
|
+
*/
|
|
542
|
+
resolveEncryptionEntityId(tableName) {
|
|
543
|
+
return this.resolveEntityId(this.resolveEntityMetadata(tableName)) ?? this.resolveEntityId({ tableName });
|
|
544
|
+
}
|
|
324
545
|
isEncryptedPayload(value) {
|
|
325
546
|
const parts = value.split(":");
|
|
326
547
|
return parts.length === 4 && parts[3] === "v1";
|
|
@@ -336,6 +557,8 @@ function createWidgetDataService(em, scope, registry, cache) {
|
|
|
336
557
|
return new WidgetDataService({ em, scope, registry, cache });
|
|
337
558
|
}
|
|
338
559
|
export {
|
|
560
|
+
WidgetDataEncryptionUnavailableError,
|
|
561
|
+
WidgetDataScanLimitError,
|
|
339
562
|
WidgetDataService,
|
|
340
563
|
WidgetDataValidationError,
|
|
341
564
|
createWidgetDataService
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/dashboards/services/widgetDataService.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport { createHash } from 'node:crypto'\nimport { decryptWithAesGcm } from '@open-mercato/shared/lib/encryption/aes'\nimport { resolveTenantEncryptionService } from '@open-mercato/shared/lib/encryption/customFieldValues'\nimport { resolveEntityIdFromMetadata } from '@open-mercato/shared/lib/encryption/entityIds'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n type DateRangePreset,\n resolveDateRange,\n getPreviousPeriod,\n calculatePercentageChange,\n determineChangeDirection,\n isValidDateRangePreset,\n} from '@open-mercato/ui/backend/date-range'\nimport {\n type AggregateFunction,\n type DateGranularity,\n buildAggregationQuery,\n} from '../lib/aggregations'\nimport type { AnalyticsRegistry } from './analyticsRegistry'\n\nconst WIDGET_DATA_CACHE_TTL = 120_000\nconst WIDGET_DATA_SEGMENT_TTL = 86_400_000\nconst WIDGET_DATA_SEGMENT_KEY = 'widget-data:__segment__'\n\nconst SAFE_IDENTIFIER_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/\n\nexport class WidgetDataValidationError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'WidgetDataValidationError'\n }\n}\n\nfunction assertSafeIdentifier(value: string, name: string): void {\n if (!SAFE_IDENTIFIER_PATTERN.test(value)) {\n throw new Error(`Invalid ${name}: ${value}`)\n }\n}\n\nexport type WidgetDataRequest = {\n entityType: string\n metric: {\n field: string\n aggregate: AggregateFunction\n }\n groupBy?: {\n field: string\n granularity?: DateGranularity\n limit?: number\n resolveLabels?: boolean\n }\n filters?: Array<{\n field: string\n operator: 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'not_in' | 'is_null' | 'is_not_null'\n value?: unknown\n }>\n dateRange?: {\n field: string\n preset: DateRangePreset\n }\n comparison?: {\n type: 'previous_period' | 'previous_year'\n }\n}\n\nexport type WidgetDataItem = {\n groupKey: unknown\n groupLabel?: string\n value: number | null\n}\n\nexport type WidgetDataResponse = {\n value: number | null\n data: WidgetDataItem[]\n comparison?: {\n value: number | null\n change: number\n direction: 'up' | 'down' | 'unchanged'\n }\n metadata: {\n fetchedAt: string\n recordCount: number\n }\n}\n\nexport type WidgetDataScope = {\n tenantId: string\n organizationIds?: string[]\n}\n\nexport type WidgetDataServiceOptions = {\n em: EntityManager\n scope: WidgetDataScope\n registry: AnalyticsRegistry\n cache?: CacheStrategy\n}\n\nexport class WidgetDataService {\n private em: EntityManager\n private scope: WidgetDataScope\n private registry: AnalyticsRegistry\n private cache?: CacheStrategy\n\n constructor(options: WidgetDataServiceOptions) {\n this.em = options.em\n this.scope = options.scope\n this.registry = options.registry\n this.cache = options.cache\n }\n\n private buildCacheKey(request: WidgetDataRequest): string {\n const hash = createHash('sha256')\n hash.update(JSON.stringify({ request, scope: this.scope }))\n return `widget-data:${hash.digest('hex').slice(0, 16)}`\n }\n\n private getCacheTags(entityType: string): string[] {\n return ['widget-data', `widget-data:${entityType}`]\n }\n\n async fetchWidgetData(request: WidgetDataRequest): Promise<WidgetDataResponse> {\n this.validateRequest(request)\n\n if (this.cache) {\n const cacheKey = this.buildCacheKey(request)\n try {\n const cached = await this.cache.get(cacheKey)\n if (cached && typeof cached === 'object' && 'value' in (cached as object)) {\n return cached as WidgetDataResponse\n }\n } catch {\n }\n }\n\n const now = new Date()\n let dateRangeResolved: { start: Date; end: Date } | undefined\n let comparisonRange: { start: Date; end: Date } | undefined\n\n if (request.dateRange) {\n dateRangeResolved = resolveDateRange(request.dateRange.preset, now)\n if (request.comparison) {\n comparisonRange = getPreviousPeriod(dateRangeResolved, request.dateRange.preset)\n }\n }\n\n const shouldFetchComparison = Boolean(comparisonRange && request.dateRange)\n\n const [mainResult, comparisonResult] = await Promise.all([\n this.executeQuery(request, dateRangeResolved),\n shouldFetchComparison && comparisonRange\n ? this.executeQuery(request, comparisonRange)\n : Promise.resolve<{ value: number | null; data: WidgetDataItem[] } | undefined>(undefined),\n ])\n\n const response: WidgetDataResponse = {\n value: mainResult.value,\n data: mainResult.data,\n metadata: {\n fetchedAt: now.toISOString(),\n recordCount: mainResult.data.length || (mainResult.value !== null ? 1 : 0),\n },\n }\n\n if (comparisonResult && mainResult.value !== null && comparisonResult.value !== null) {\n response.comparison = {\n value: comparisonResult.value,\n change: calculatePercentageChange(mainResult.value, comparisonResult.value),\n direction: determineChangeDirection(mainResult.value, comparisonResult.value),\n }\n }\n\n if (this.cache) {\n const cacheKey = this.buildCacheKey(request)\n const tags = this.getCacheTags(request.entityType)\n try {\n await this.cache.set(cacheKey, response, { ttl: WIDGET_DATA_CACHE_TTL, tags })\n await this.cache.set(\n WIDGET_DATA_SEGMENT_KEY,\n { updatedAt: response.metadata.fetchedAt },\n { ttl: WIDGET_DATA_SEGMENT_TTL, tags: ['widget-data'] },\n )\n } catch {\n }\n }\n\n return response\n }\n\n private validateRequest(request: WidgetDataRequest): void {\n if (!this.registry.isValidEntityType(request.entityType)) {\n throw new WidgetDataValidationError(`Invalid entity type: ${request.entityType}`)\n }\n\n if (!request.metric?.field || !request.metric?.aggregate) {\n throw new WidgetDataValidationError('Metric field and aggregate are required')\n }\n\n const metricMapping = this.registry.getFieldMapping(request.entityType, request.metric.field)\n if (!metricMapping) {\n throw new WidgetDataValidationError(\n `Invalid metric field: ${request.metric.field} for entity type: ${request.entityType}`\n )\n }\n\n const validAggregates: AggregateFunction[] = ['count', 'sum', 'avg', 'min', 'max']\n if (!validAggregates.includes(request.metric.aggregate)) {\n throw new WidgetDataValidationError(`Invalid aggregate function: ${request.metric.aggregate}`)\n }\n\n if (request.dateRange && !isValidDateRangePreset(request.dateRange.preset)) {\n throw new WidgetDataValidationError(`Invalid date range preset: ${request.dateRange.preset}`)\n }\n\n if (request.groupBy) {\n const groupMapping = this.registry.getFieldMapping(request.entityType, request.groupBy.field)\n if (!groupMapping) {\n const [baseField] = request.groupBy.field.split('.')\n const baseMapping = this.registry.getFieldMapping(request.entityType, baseField)\n if (!baseMapping || baseMapping.type !== 'jsonb') {\n throw new WidgetDataValidationError(`Invalid groupBy field: ${request.groupBy.field}`)\n }\n }\n }\n }\n\n private async executeQuery(\n request: WidgetDataRequest,\n dateRange?: { start: Date; end: Date },\n ): Promise<{ value: number | null; data: WidgetDataItem[] }> {\n const query = buildAggregationQuery({\n entityType: request.entityType,\n metric: request.metric,\n groupBy: request.groupBy,\n dateRange: dateRange && request.dateRange ? { field: request.dateRange.field, ...dateRange } : undefined,\n filters: request.filters,\n scope: this.scope,\n registry: this.registry,\n })\n\n if (!query) {\n throw new Error('Failed to build aggregation query')\n }\n\n const rows = await this.em.getConnection().execute(query.sql, query.params)\n const results = Array.isArray(rows) ? rows : []\n\n if (request.groupBy) {\n let data: WidgetDataItem[] = results.map((row: Record<string, unknown>) => ({\n groupKey: row.group_key,\n value: row.value !== null ? Number(row.value) : null,\n }))\n\n if (request.groupBy.resolveLabels) {\n data = await this.resolveGroupLabels(data, request.entityType, request.groupBy.field)\n }\n\n const totalValue = data.reduce((sum: number, item: WidgetDataItem) => sum + (item.value ?? 0), 0)\n return { value: totalValue, data }\n }\n\n const singleValue = results[0]?.value !== undefined ? Number(results[0].value) : null\n return { value: singleValue, data: [] }\n }\n\n private async resolveGroupLabels(\n data: WidgetDataItem[],\n entityType: string,\n groupByField: string,\n ): Promise<WidgetDataItem[]> {\n const config = this.registry.getLabelResolverConfig(entityType, groupByField)\n\n if (!config) {\n return data.map((item) => ({\n ...item,\n groupLabel: item.groupKey != null && item.groupKey !== '' ? String(item.groupKey) : undefined,\n }))\n }\n\n const ids = data\n .map((item) => item.groupKey)\n .filter((id): id is string => {\n if (typeof id !== 'string' || id.length === 0) return false\n return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id)\n })\n\n if (ids.length === 0) {\n return data.map((item) => ({ ...item, groupLabel: undefined }))\n }\n\n const uniqueIds = [...new Set(ids)]\n\n assertSafeIdentifier(config.table, 'table name')\n assertSafeIdentifier(config.idColumn, 'id column')\n assertSafeIdentifier(config.labelColumn, 'label column')\n\n const meta = this.resolveEntityMetadata(config.table)\n const idProp = meta ? this.resolveEntityPropertyName(meta, config.idColumn) : null\n const labelProp = meta ? this.resolveEntityPropertyName(meta, config.labelColumn) : null\n const tenantProp = meta\n ? (this.resolveEntityPropertyName(meta, 'tenant_id') ?? this.resolveEntityPropertyName(meta, 'tenantId'))\n : null\n const organizationProp = meta\n ? (this.resolveEntityPropertyName(meta, 'organization_id') ?? this.resolveEntityPropertyName(meta, 'organizationId'))\n : null\n const entityName = meta ? ((meta as any).class ?? meta.className ?? meta.name) : null\n\n if (meta && idProp && labelProp && tenantProp && entityName) {\n const where: Record<string, unknown> = {\n [idProp]: { $in: uniqueIds },\n [tenantProp]: this.scope.tenantId,\n }\n if (organizationProp && this.scope.organizationIds && this.scope.organizationIds.length > 0) {\n where[organizationProp] = { $in: this.scope.organizationIds }\n }\n\n try {\n const records = await findWithDecryption(\n this.em,\n entityName,\n where,\n { fields: [idProp, labelProp, tenantProp, organizationProp].filter(Boolean) },\n { tenantId: this.scope.tenantId, organizationId: this.resolveOrganizationId() },\n )\n\n const encryptionService = resolveTenantEncryptionService(this.em as any)\n const dek = encryptionService?.isEnabled() ? await encryptionService.getDek(this.scope.tenantId) : null\n let hasEncryptedLabels = false\n let hasDecryptedLabels = false\n\n const labelMap = new Map<string, string>()\n for (const record of records as Array<Record<string, unknown>>) {\n const id = record[idProp]\n let labelValue = record[labelProp]\n if (typeof labelValue === 'string' && this.isEncryptedPayload(labelValue)) {\n hasEncryptedLabels = true\n if (dek?.key) {\n const decrypted = this.decryptWithDek(labelValue, dek.key)\n if (decrypted !== null) {\n labelValue = decrypted\n hasDecryptedLabels = true\n }\n }\n } else if (labelValue != null && labelValue !== '') {\n hasDecryptedLabels = true\n }\n\n if (typeof id === 'string' && labelValue != null && labelValue !== '') {\n labelMap.set(id, String(labelValue))\n }\n }\n\n if (labelMap.size > 0 && (!hasEncryptedLabels || hasDecryptedLabels)) {\n return data.map((item) => ({\n ...item,\n groupLabel: typeof item.groupKey === 'string' && labelMap.has(item.groupKey)\n ? labelMap.get(item.groupKey)!\n : undefined,\n }))\n }\n } catch {\n // fall through to SQL resolution\n }\n }\n\n const clauses = [`\"${config.idColumn}\" = ANY(?::uuid[])`, 'tenant_id = ?']\n const params: unknown[] = [`{${uniqueIds.join(',')}}`, this.scope.tenantId]\n\n if (this.scope.organizationIds && this.scope.organizationIds.length > 0) {\n clauses.push('organization_id = ANY(?::uuid[])')\n params.push(`{${this.scope.organizationIds.join(',')}}`)\n }\n\n const sql = `SELECT \"${config.idColumn}\" as id, \"${config.labelColumn}\" as label, tenant_id, organization_id FROM \"${config.table}\" WHERE ${clauses.join(\n ' AND ',\n )}`\n\n try {\n const labelRows = await this.em.getConnection().execute(sql, params)\n const entityId = this.resolveEntityId(meta)\n const encryptionService = resolveTenantEncryptionService(this.em as any)\n const organizationId = this.resolveOrganizationId()\n const dek = encryptionService?.isEnabled() ? await encryptionService.getDek(this.scope.tenantId) : null\n\n const labelMap = new Map<string, string>()\n for (const row of labelRows as Array<{ id: string; label: string | null; tenant_id?: string | null; organization_id?: string | null }>) {\n let labelValue = row.label\n if (entityId && encryptionService?.isEnabled() && labelValue != null) {\n const rowOrgId = row.organization_id ?? organizationId ?? null\n const decrypted = await encryptionService.decryptEntityPayload(\n entityId,\n { [config.labelColumn]: labelValue },\n this.scope.tenantId,\n rowOrgId,\n )\n const resolved = decrypted[config.labelColumn]\n if (typeof resolved === 'string' || typeof resolved === 'number') {\n labelValue = String(resolved)\n }\n }\n\n if (labelValue && dek?.key && this.isEncryptedPayload(labelValue)) {\n const decrypted = this.decryptWithDek(labelValue, dek.key)\n if (decrypted !== null) {\n labelValue = decrypted\n }\n }\n\n if (row.id && labelValue != null && labelValue !== '') {\n labelMap.set(row.id, labelValue)\n }\n }\n\n return data.map((item) => ({\n ...item,\n groupLabel: typeof item.groupKey === 'string' && labelMap.has(item.groupKey)\n ? labelMap.get(item.groupKey)!\n : undefined,\n }))\n } catch {\n return data.map((item) => ({\n ...item,\n groupLabel: undefined,\n }))\n }\n }\n\n private resolveOrganizationId(): string | null {\n if (!this.scope.organizationIds || this.scope.organizationIds.length !== 1) return null\n return this.scope.organizationIds[0] ?? null\n }\n\n private resolveEntityMetadata(tableName: string): Record<string, any> | null {\n const registry = (this.em as any)?.getMetadata?.()\n if (!registry) return null\n const entries =\n (typeof registry.getAll === 'function' && registry.getAll()) ||\n (Array.isArray(registry.metadata) ? registry.metadata : Object.values(registry.metadata ?? {}))\n const metas = Array.isArray(entries) ? entries : Object.values(entries ?? {})\n const match = metas.find((meta: any) => {\n const table = meta?.tableName ?? meta?.collection\n if (typeof table !== 'string') return false\n if (table === tableName) return true\n return table.split('.').pop() === tableName\n })\n return match ?? null\n }\n\n private resolveEntityPropertyName(meta: Record<string, any>, columnName: string): string | null {\n const properties = meta?.properties ? Object.values(meta.properties) : []\n for (const prop of properties as Array<Record<string, any>>) {\n const fieldName = prop?.fieldName\n const fieldNames = prop?.fieldNames\n if (typeof fieldName === 'string' && fieldName === columnName) return prop?.name ?? null\n if (Array.isArray(fieldNames) && fieldNames.includes(columnName)) return prop?.name ?? null\n if (prop?.name === columnName) return prop?.name ?? null\n }\n return null\n }\n\n private resolveEntityId(meta: Record<string, any> | null): string | null {\n if (!meta) return null\n try {\n return resolveEntityIdFromMetadata(meta as any)\n } catch {\n return null\n }\n }\n\n private isEncryptedPayload(value: string): boolean {\n const parts = value.split(':')\n return parts.length === 4 && parts[3] === 'v1'\n }\n\n private decryptWithDek(value: string, dek: string): string | null {\n const first = decryptWithAesGcm(value, dek)\n if (first === null) return null\n if (!this.isEncryptedPayload(first)) return first\n return decryptWithAesGcm(first, dek) ?? first\n }\n}\n\nexport function createWidgetDataService(\n em: EntityManager,\n scope: WidgetDataScope,\n registry: AnalyticsRegistry,\n cache?: CacheStrategy,\n): WidgetDataService {\n return new WidgetDataService({ em, scope, registry, cache })\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport { createHash } from 'node:crypto'\nimport { decryptWithAesGcm } from '@open-mercato/shared/lib/encryption/aes'\nimport { resolveTenantEncryptionService } from '@open-mercato/shared/lib/encryption/customFieldValues'\nimport { resolveEntityIdFromMetadata } from '@open-mercato/shared/lib/encryption/entityIds'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n type DateRangePreset,\n resolveDateRange,\n getPreviousPeriod,\n calculatePercentageChange,\n determineChangeDirection,\n isValidDateRangePreset,\n} from '@open-mercato/ui/backend/date-range'\nimport { parseDecryptedFieldValue } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport type { TenantDek } from '@open-mercato/shared/lib/encryption/kms'\nimport { isTenantDataEncryptionEnabled } from '@open-mercato/shared/lib/encryption/toggles'\nimport {\n type AggregateFunction,\n type DateGranularity,\n buildAggregationQuery,\n buildGroupSourceRowsQuery,\n resolveGroupExpression,\n} from '../lib/aggregations'\nimport {\n type ExactDecimal,\n EXACT_DECIMAL_ZERO,\n addExactDecimal,\n compareExactDecimal,\n exactDecimalToNumber,\n parseExactDecimal,\n} from '../lib/exactDecimal'\nimport type { AnalyticsRegistry } from './analyticsRegistry'\n\nconst WIDGET_DATA_CACHE_TTL = 120_000\nconst WIDGET_DATA_SEGMENT_TTL = 86_400_000\nconst WIDGET_DATA_SEGMENT_KEY = 'widget-data:__segment__'\n\n/**\n * Encrypted group sources cannot be grouped by the database, so the rows are scanned and\n * aggregated in application code. The cap keeps that scan bounded; overflowing it fails loudly\n * instead of charting a silently truncated result (#4622).\n */\nconst ENCRYPTED_GROUP_SCAN_LIMIT = 20_000\n\nconst SAFE_IDENTIFIER_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/\n\nconst logger = createLogger('dashboards').child({ component: 'widget-data' })\n\nexport class WidgetDataValidationError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'WidgetDataValidationError'\n }\n}\n\nexport class WidgetDataScanLimitError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'WidgetDataScanLimitError'\n }\n}\n\n/**\n * Raised when encryption is configured but the group source cannot currently be resolved \u2014 an\n * unhealthy KMS, an unreadable encryption map, or a missing tenant DEK. Grouping would either read\n * ciphertext in SQL or silently collapse every encrypted row into \"Unknown\", so the request fails\n * closed instead (#4622).\n */\nexport class WidgetDataEncryptionUnavailableError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'WidgetDataEncryptionUnavailableError'\n }\n}\n\ntype EncryptedGroupSource = {\n entityId: string\n dbColumn: string\n jsonPath: string | null\n}\n\nfunction assertSafeIdentifier(value: string, name: string): void {\n if (!SAFE_IDENTIFIER_PATTERN.test(value)) {\n throw new Error(`Invalid ${name}: ${value}`)\n }\n}\n\n/**\n * Encryption maps may register a field in either casing (`findKey` in the encryption service\n * accepts both), so match the analytics column tolerantly rather than by exact snake_case.\n */\nfunction matchesColumn(fieldName: string, dbColumn: string): boolean {\n const normalize = (value: string) => value.replace(/_/g, '').toLowerCase()\n return normalize(fieldName) === normalize(dbColumn)\n}\n\nfunction readJsonPath(value: unknown, path: string): unknown {\n let current = value\n for (const part of path.split('.')) {\n if (current === null || typeof current !== 'object') return null\n current = (current as Record<string, unknown>)[part]\n }\n return current\n}\n\n/**\n * Running aggregate state for one group. Values are folded in as they are scanned \u2014 the bucket\n * never holds the row set \u2014 so the scan cost stays linear and independent of the row cap.\n */\ntype GroupBucket = {\n /** Rows with a non-null metric column, mirroring SQL `COUNT(column)`. */\n count: number\n /** Rows whose metric parsed as a decimal, mirroring the row set SQL aggregates operate on. */\n numericCount: number\n /** Exact running sum, so money never accumulates in binary floating point. */\n sum: ExactDecimal\n min: ExactDecimal | null\n max: ExactDecimal | null\n}\n\nfunction createGroupBucket(): GroupBucket {\n return { count: 0, numericCount: 0, sum: EXACT_DECIMAL_ZERO, min: null, max: null }\n}\n\nfunction foldMetricValue(bucket: GroupBucket, value: ExactDecimal): void {\n bucket.numericCount += 1\n bucket.sum = addExactDecimal(bucket.sum, value)\n if (bucket.min === null || compareExactDecimal(value, bucket.min) < 0) bucket.min = value\n if (bucket.max === null || compareExactDecimal(value, bucket.max) > 0) bucket.max = value\n}\n\n/**\n * Mirrors the SQL semantics of `buildAggregateExpression` for application-side aggregation. That\n * expression wraps `SUM`/`AVG` in `COALESCE(..., 0)`, so an empty value set aggregates to `0` on\n * both paths, while the uncoalesced `MIN`/`MAX` keep PostgreSQL's `NULL`. An encrypted group source\n * must not report a different value than the same data would report in plaintext (#4622).\n */\nfunction aggregateBucket(aggregate: AggregateFunction, bucket: GroupBucket): number | null {\n switch (aggregate) {\n case 'count':\n return bucket.count\n case 'sum':\n return bucket.numericCount === 0 ? 0 : exactDecimalToNumber(bucket.sum)\n case 'avg':\n return bucket.numericCount === 0 ? 0 : exactDecimalToNumber(bucket.sum) / bucket.numericCount\n case 'min':\n return bucket.min === null ? null : exactDecimalToNumber(bucket.min)\n case 'max':\n return bucket.max === null ? null : exactDecimalToNumber(bucket.max)\n default:\n return bucket.count\n }\n}\n\n/**\n * Mirrors the SQL path's `ORDER BY value DESC NULLS LAST`. PostgreSQL defaults DESC to NULLS FIRST,\n * which would let empty buckets displace real ones under a group limit, so both paths state the\n * policy explicitly: highest value first, buckets without a value last.\n */\nfunction compareWidgetDataItemsByValueDesc(\n left: { value: number | null },\n right: { value: number | null },\n): number {\n if (left.value === null && right.value === null) return 0\n if (left.value === null) return 1\n if (right.value === null) return -1\n return right.value - left.value\n}\n\nexport type WidgetDataRequest = {\n entityType: string\n metric: {\n field: string\n aggregate: AggregateFunction\n }\n groupBy?: {\n field: string\n granularity?: DateGranularity\n limit?: number\n resolveLabels?: boolean\n }\n filters?: Array<{\n field: string\n operator: 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'not_in' | 'is_null' | 'is_not_null'\n value?: unknown\n }>\n dateRange?: {\n field: string\n preset: DateRangePreset\n }\n comparison?: {\n type: 'previous_period' | 'previous_year'\n }\n}\n\nexport type WidgetDataItem = {\n groupKey: unknown\n groupLabel?: string\n value: number | null\n}\n\nexport type WidgetDataResponse = {\n value: number | null\n data: WidgetDataItem[]\n comparison?: {\n value: number | null\n change: number\n direction: 'up' | 'down' | 'unchanged'\n }\n metadata: {\n fetchedAt: string\n recordCount: number\n }\n}\n\nexport type WidgetDataScope = {\n tenantId: string\n organizationIds?: string[]\n}\n\nexport type WidgetDataServiceOptions = {\n em: EntityManager\n scope: WidgetDataScope\n registry: AnalyticsRegistry\n cache?: CacheStrategy\n}\n\nexport class WidgetDataService {\n private em: EntityManager\n private scope: WidgetDataScope\n private registry: AnalyticsRegistry\n private cache?: CacheStrategy\n\n constructor(options: WidgetDataServiceOptions) {\n this.em = options.em\n this.scope = options.scope\n this.registry = options.registry\n this.cache = options.cache\n }\n\n private buildCacheKey(request: WidgetDataRequest): string {\n const hash = createHash('sha256')\n hash.update(JSON.stringify({ request, scope: this.scope }))\n return `widget-data:${hash.digest('hex').slice(0, 16)}`\n }\n\n private getCacheTags(entityType: string): string[] {\n return ['widget-data', `widget-data:${entityType}`]\n }\n\n async fetchWidgetData(request: WidgetDataRequest): Promise<WidgetDataResponse> {\n this.validateRequest(request)\n\n if (this.cache) {\n const cacheKey = this.buildCacheKey(request)\n try {\n const cached = await this.cache.get(cacheKey)\n if (cached && typeof cached === 'object' && 'value' in (cached as object)) {\n return cached as WidgetDataResponse\n }\n } catch {\n }\n }\n\n const now = new Date()\n let dateRangeResolved: { start: Date; end: Date } | undefined\n let comparisonRange: { start: Date; end: Date } | undefined\n\n if (request.dateRange) {\n dateRangeResolved = resolveDateRange(request.dateRange.preset, now)\n if (request.comparison) {\n comparisonRange = getPreviousPeriod(dateRangeResolved, request.dateRange.preset)\n }\n }\n\n const shouldFetchComparison = Boolean(comparisonRange && request.dateRange)\n\n const [mainResult, comparisonResult] = await Promise.all([\n this.executeQuery(request, dateRangeResolved),\n shouldFetchComparison && comparisonRange\n ? this.executeQuery(request, comparisonRange)\n : Promise.resolve<{ value: number | null; data: WidgetDataItem[] } | undefined>(undefined),\n ])\n\n const response: WidgetDataResponse = {\n value: mainResult.value,\n data: mainResult.data,\n metadata: {\n fetchedAt: now.toISOString(),\n recordCount: mainResult.data.length || (mainResult.value !== null ? 1 : 0),\n },\n }\n\n if (comparisonResult && mainResult.value !== null && comparisonResult.value !== null) {\n response.comparison = {\n value: comparisonResult.value,\n change: calculatePercentageChange(mainResult.value, comparisonResult.value),\n direction: determineChangeDirection(mainResult.value, comparisonResult.value),\n }\n }\n\n if (this.cache) {\n const cacheKey = this.buildCacheKey(request)\n const tags = this.getCacheTags(request.entityType)\n try {\n await this.cache.set(cacheKey, response, { ttl: WIDGET_DATA_CACHE_TTL, tags })\n await this.cache.set(\n WIDGET_DATA_SEGMENT_KEY,\n { updatedAt: response.metadata.fetchedAt },\n { ttl: WIDGET_DATA_SEGMENT_TTL, tags: ['widget-data'] },\n )\n } catch {\n }\n }\n\n return response\n }\n\n private validateRequest(request: WidgetDataRequest): void {\n if (!this.registry.isValidEntityType(request.entityType)) {\n throw new WidgetDataValidationError(`Invalid entity type: ${request.entityType}`)\n }\n\n if (!request.metric?.field || !request.metric?.aggregate) {\n throw new WidgetDataValidationError('Metric field and aggregate are required')\n }\n\n const metricMapping = this.registry.getFieldMapping(request.entityType, request.metric.field)\n if (!metricMapping) {\n throw new WidgetDataValidationError(\n `Invalid metric field: ${request.metric.field} for entity type: ${request.entityType}`\n )\n }\n\n const validAggregates: AggregateFunction[] = ['count', 'sum', 'avg', 'min', 'max']\n if (!validAggregates.includes(request.metric.aggregate)) {\n throw new WidgetDataValidationError(`Invalid aggregate function: ${request.metric.aggregate}`)\n }\n\n if (request.dateRange && !isValidDateRangePreset(request.dateRange.preset)) {\n throw new WidgetDataValidationError(`Invalid date range preset: ${request.dateRange.preset}`)\n }\n\n if (request.groupBy) {\n const groupMapping = this.registry.getFieldMapping(request.entityType, request.groupBy.field)\n if (!groupMapping) {\n const [baseField] = request.groupBy.field.split('.')\n const baseMapping = this.registry.getFieldMapping(request.entityType, baseField)\n if (!baseMapping || baseMapping.type !== 'jsonb') {\n throw new WidgetDataValidationError(`Invalid groupBy field: ${request.groupBy.field}`)\n }\n }\n }\n }\n\n private async executeQuery(\n request: WidgetDataRequest,\n dateRange?: { start: Date; end: Date },\n ): Promise<{ value: number | null; data: WidgetDataItem[] }> {\n if (request.groupBy) {\n const encryptedSource = await this.resolveEncryptedGroupSource(request.entityType, request.groupBy)\n if (encryptedSource) {\n return this.executeEncryptedGroupQuery(request, encryptedSource, dateRange)\n }\n }\n\n const query = buildAggregationQuery({\n entityType: request.entityType,\n metric: request.metric,\n groupBy: request.groupBy,\n dateRange: dateRange && request.dateRange ? { field: request.dateRange.field, ...dateRange } : undefined,\n filters: request.filters,\n scope: this.scope,\n registry: this.registry,\n })\n\n if (!query) {\n throw new Error('Failed to build aggregation query')\n }\n\n const rows = await this.em.getConnection().execute(query.sql, query.params)\n const results = Array.isArray(rows) ? rows : []\n\n if (request.groupBy) {\n let data: WidgetDataItem[] = results.map((row: Record<string, unknown>) => ({\n groupKey: row.group_key,\n value: row.value !== null ? Number(row.value) : null,\n }))\n\n if (request.groupBy.resolveLabels) {\n data = await this.resolveGroupLabels(data, request.entityType, request.groupBy.field)\n }\n\n const totalValue = data.reduce((sum: number, item: WidgetDataItem) => sum + (item.value ?? 0), 0)\n return { value: totalValue, data }\n }\n\n const singleValue = results[0]?.value !== undefined ? Number(results[0].value) : null\n return { value: singleValue, data: [] }\n }\n\n /**\n * Reports the encrypted column a groupBy field reads from, or null when the source is stored in\n * plaintext and can be grouped by the database. Grouping over an encrypted column in SQL buckets\n * ciphertext and renders it to the user (#4622).\n */\n private async resolveEncryptedGroupSource(\n entityType: string,\n groupBy: NonNullable<WidgetDataRequest['groupBy']>,\n ): Promise<EncryptedGroupSource | null> {\n // Encryption is not configured for this deployment, so no column can hold ciphertext and the\n // database may group the source directly.\n if (!isTenantDataEncryptionEnabled()) return null\n\n const resolved = resolveGroupExpression(this.registry, entityType, groupBy)\n if (!resolved) return null\n\n const tableName = this.registry.getEntityTypeConfig(entityType)?.tableName\n if (!tableName) return null\n\n // Without an encryption entity id the table is outside the encryption map entirely, which is\n // the same state the encrypting subscriber sees when it skips the row.\n const entityId = this.resolveEncryptionEntityId(tableName)\n if (!entityId) return null\n\n const encryptionService = resolveTenantEncryptionService(this.em)\n if (!encryptionService) {\n throw new WidgetDataEncryptionUnavailableError(\n `Cannot determine whether ${groupBy.field} is encrypted: encryption service unavailable`,\n )\n }\n\n const organizationId = this.resolveOrganizationId()\n let encryptedFields: string[]\n try {\n // Deliberately independent of KMS health: the map describes how the rows were written, and\n // an unhealthy KMS must not be read as \"this column is plaintext\" (#4622).\n encryptedFields = await encryptionService.getEncryptedFieldNames(\n entityId,\n this.scope.tenantId,\n organizationId,\n { ignoreRuntimeHealth: true },\n )\n } catch (err) {\n logger.error('Failed to resolve encrypted fields for widget grouping', { err, entityId, entityType })\n throw new WidgetDataEncryptionUnavailableError(\n `Cannot determine whether ${groupBy.field} is encrypted: encryption map lookup failed`,\n )\n }\n\n if (!encryptedFields.some((field) => matchesColumn(field, resolved.dbColumn))) return null\n\n if (resolved.jsonPath === null && groupBy.granularity) {\n throw new WidgetDataValidationError(\n `Cannot group encrypted field by granularity: ${groupBy.field}`,\n )\n }\n\n return { entityId, dbColumn: resolved.dbColumn, jsonPath: resolved.jsonPath }\n }\n\n /**\n * Aggregates an encrypted group source in application code: rows are scanned, the group column is\n * decrypted, and only the resulting plaintext keys are grouped. Values that cannot be decrypted\n * collapse into the null (\"Unknown\") bucket so ciphertext never reaches the response.\n */\n private async executeEncryptedGroupQuery(\n request: WidgetDataRequest,\n source: EncryptedGroupSource,\n dateRange?: { start: Date; end: Date },\n ): Promise<{ value: number | null; data: WidgetDataItem[] }> {\n const query = buildGroupSourceRowsQuery({\n entityType: request.entityType,\n metric: request.metric,\n dateRange: dateRange && request.dateRange ? { field: request.dateRange.field, ...dateRange } : undefined,\n filters: request.filters,\n scope: this.scope,\n registry: this.registry,\n groupColumn: source.dbColumn,\n rowLimit: ENCRYPTED_GROUP_SCAN_LIMIT,\n })\n\n if (!query) {\n throw new Error('Failed to build aggregation query')\n }\n\n const rows = await this.em.getConnection().execute(query.sql, query.params)\n const results = Array.isArray(rows) ? rows : []\n\n if (results.length > ENCRYPTED_GROUP_SCAN_LIMIT) {\n throw new WidgetDataScanLimitError(\n `Too many rows to group encrypted field ${request.groupBy?.field} (limit ${ENCRYPTED_GROUP_SCAN_LIMIT})`,\n )\n }\n\n const encryptionService = resolveTenantEncryptionService(this.em)\n let dek: TenantDek | null = null\n try {\n dek = encryptionService ? await encryptionService.getDek(this.scope.tenantId) : null\n } catch (err) {\n logger.error('Failed to resolve the tenant DEK for widget grouping', { err, entityId: source.entityId })\n dek = null\n }\n\n const buckets = new Map<string | null, GroupBucket>()\n let undecryptableRows = 0\n\n for (const row of results as Array<Record<string, unknown>>) {\n const resolvedKey = this.resolveDecryptedGroupKey(row.group_source, source, dek?.key ?? null)\n // An undecryptable source joins the null (\"Unknown\") bucket rather than being dropped, so the\n // widget total still matches the underlying rows.\n if (resolvedKey === undefined) undecryptableRows += 1\n const groupKey = resolvedKey === undefined ? null : resolvedKey\n\n let bucket = buckets.get(groupKey)\n if (!bucket) {\n bucket = createGroupBucket()\n buckets.set(groupKey, bucket)\n }\n\n const metricValue = row.metric_value\n if (metricValue === null || metricValue === undefined) continue\n bucket.count += 1\n const numeric = parseExactDecimal(metricValue)\n if (numeric) foldMetricValue(bucket, numeric)\n }\n\n if (undecryptableRows > 0) {\n // No DEK at all means every ciphertext row would collapse into \"Unknown\" and the widget would\n // report a confidently wrong distribution, so fail closed rather than chart it (#4622).\n if (!dek) {\n throw new WidgetDataEncryptionUnavailableError(\n `Cannot group encrypted field ${request.groupBy?.field}: tenant encryption key is unavailable`,\n )\n }\n logger.warn('Grouped rows with an undecryptable group source as unknown', {\n entityId: source.entityId,\n column: source.dbColumn,\n rows: undecryptableRows,\n })\n }\n\n let data: WidgetDataItem[] = Array.from(buckets.entries())\n .map(([groupKey, bucket]) => ({ groupKey, value: aggregateBucket(request.metric.aggregate, bucket) }))\n .sort(compareWidgetDataItemsByValueDesc)\n\n if (request.groupBy?.limit && request.groupBy.limit > 0) {\n data = data.slice(0, Math.min(request.groupBy.limit, 100))\n }\n\n if (request.groupBy?.resolveLabels) {\n data = await this.resolveGroupLabels(data, request.entityType, request.groupBy.field)\n }\n\n const totalValue = data.reduce((sum: number, item: WidgetDataItem) => sum + (item.value ?? 0), 0)\n return { value: totalValue, data }\n }\n\n /**\n * Returns the plaintext group key for a scanned row, `null` for an empty/absent value, or\n * `undefined` when the source is ciphertext that could not be decrypted.\n */\n private resolveDecryptedGroupKey(\n rawValue: unknown,\n source: EncryptedGroupSource,\n dek: string | null,\n ): string | null | undefined {\n if (rawValue === null || rawValue === undefined) return null\n\n let value: unknown = rawValue\n if (typeof value === 'string' && this.isEncryptedPayload(value)) {\n if (!dek) return undefined\n const decrypted = this.decryptWithDek(value, dek)\n if (decrypted === null) return undefined\n value = parseDecryptedFieldValue(decrypted)\n }\n\n if (source.jsonPath !== null) {\n value = readJsonPath(value, source.jsonPath)\n }\n\n if (value === null || value === undefined || value === '') return null\n if (typeof value === 'object') return null\n const key = String(value)\n // Defense in depth: a nested value that is itself ciphertext must never reach the response.\n return this.isEncryptedPayload(key) ? undefined : key\n }\n\n private async resolveGroupLabels(\n data: WidgetDataItem[],\n entityType: string,\n groupByField: string,\n ): Promise<WidgetDataItem[]> {\n const config = this.registry.getLabelResolverConfig(entityType, groupByField)\n\n if (!config) {\n return data.map((item) => {\n if (item.groupKey == null || item.groupKey === '') return { ...item, groupLabel: undefined }\n const label = String(item.groupKey)\n // A ciphertext group key has no meaningful label; echoing it would render encrypted data\n // into the chart legend (#4622).\n return { ...item, groupLabel: this.isEncryptedPayload(label) ? undefined : label }\n })\n }\n\n const ids = data\n .map((item) => item.groupKey)\n .filter((id): id is string => {\n if (typeof id !== 'string' || id.length === 0) return false\n return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id)\n })\n\n if (ids.length === 0) {\n return data.map((item) => ({ ...item, groupLabel: undefined }))\n }\n\n const uniqueIds = [...new Set(ids)]\n\n assertSafeIdentifier(config.table, 'table name')\n assertSafeIdentifier(config.idColumn, 'id column')\n assertSafeIdentifier(config.labelColumn, 'label column')\n\n const meta = this.resolveEntityMetadata(config.table)\n const idProp = meta ? this.resolveEntityPropertyName(meta, config.idColumn) : null\n const labelProp = meta ? this.resolveEntityPropertyName(meta, config.labelColumn) : null\n const tenantProp = meta\n ? (this.resolveEntityPropertyName(meta, 'tenant_id') ?? this.resolveEntityPropertyName(meta, 'tenantId'))\n : null\n const organizationProp = meta\n ? (this.resolveEntityPropertyName(meta, 'organization_id') ?? this.resolveEntityPropertyName(meta, 'organizationId'))\n : null\n const entityName = meta ? ((meta as any).class ?? meta.className ?? meta.name) : null\n\n if (meta && idProp && labelProp && tenantProp && entityName) {\n const where: Record<string, unknown> = {\n [idProp]: { $in: uniqueIds },\n [tenantProp]: this.scope.tenantId,\n }\n if (organizationProp && this.scope.organizationIds && this.scope.organizationIds.length > 0) {\n where[organizationProp] = { $in: this.scope.organizationIds }\n }\n\n try {\n const records = await findWithDecryption(\n this.em,\n entityName,\n where,\n { fields: [idProp, labelProp, tenantProp, organizationProp].filter(Boolean) },\n { tenantId: this.scope.tenantId, organizationId: this.resolveOrganizationId() },\n )\n\n const encryptionService = resolveTenantEncryptionService(this.em as any)\n const dek = encryptionService?.isEnabled() ? await encryptionService.getDek(this.scope.tenantId) : null\n let hasEncryptedLabels = false\n let hasDecryptedLabels = false\n\n const labelMap = new Map<string, string>()\n for (const record of records as Array<Record<string, unknown>>) {\n const id = record[idProp]\n let labelValue = record[labelProp]\n if (typeof labelValue === 'string' && this.isEncryptedPayload(labelValue)) {\n hasEncryptedLabels = true\n if (dek?.key) {\n const decrypted = this.decryptWithDek(labelValue, dek.key)\n if (decrypted !== null) {\n labelValue = decrypted\n hasDecryptedLabels = true\n }\n }\n } else if (labelValue != null && labelValue !== '') {\n hasDecryptedLabels = true\n }\n\n if (typeof id === 'string' && labelValue != null && labelValue !== '') {\n labelMap.set(id, String(labelValue))\n }\n }\n\n if (labelMap.size > 0 && (!hasEncryptedLabels || hasDecryptedLabels)) {\n return data.map((item) => ({\n ...item,\n groupLabel: typeof item.groupKey === 'string' && labelMap.has(item.groupKey)\n ? labelMap.get(item.groupKey)!\n : undefined,\n }))\n }\n } catch {\n // fall through to SQL resolution\n }\n }\n\n const clauses = [`\"${config.idColumn}\" = ANY(?::uuid[])`, 'tenant_id = ?']\n const params: unknown[] = [`{${uniqueIds.join(',')}}`, this.scope.tenantId]\n\n if (this.scope.organizationIds && this.scope.organizationIds.length > 0) {\n clauses.push('organization_id = ANY(?::uuid[])')\n params.push(`{${this.scope.organizationIds.join(',')}}`)\n }\n\n const sql = `SELECT \"${config.idColumn}\" as id, \"${config.labelColumn}\" as label, tenant_id, organization_id FROM \"${config.table}\" WHERE ${clauses.join(\n ' AND ',\n )}`\n\n try {\n const labelRows = await this.em.getConnection().execute(sql, params)\n const entityId = this.resolveEntityId(meta)\n const encryptionService = resolveTenantEncryptionService(this.em as any)\n const organizationId = this.resolveOrganizationId()\n const dek = encryptionService?.isEnabled() ? await encryptionService.getDek(this.scope.tenantId) : null\n\n const labelMap = new Map<string, string>()\n for (const row of labelRows as Array<{ id: string; label: string | null; tenant_id?: string | null; organization_id?: string | null }>) {\n let labelValue = row.label\n if (entityId && encryptionService?.isEnabled() && labelValue != null) {\n const rowOrgId = row.organization_id ?? organizationId ?? null\n const decrypted = await encryptionService.decryptEntityPayload(\n entityId,\n { [config.labelColumn]: labelValue },\n this.scope.tenantId,\n rowOrgId,\n )\n const resolved = decrypted[config.labelColumn]\n if (typeof resolved === 'string' || typeof resolved === 'number') {\n labelValue = String(resolved)\n }\n }\n\n if (labelValue && dek?.key && this.isEncryptedPayload(labelValue)) {\n const decrypted = this.decryptWithDek(labelValue, dek.key)\n if (decrypted !== null) {\n labelValue = decrypted\n }\n }\n\n if (row.id && labelValue != null && labelValue !== '') {\n labelMap.set(row.id, labelValue)\n }\n }\n\n return data.map((item) => ({\n ...item,\n groupLabel: typeof item.groupKey === 'string' && labelMap.has(item.groupKey)\n ? labelMap.get(item.groupKey)!\n : undefined,\n }))\n } catch {\n return data.map((item) => ({\n ...item,\n groupLabel: undefined,\n }))\n }\n }\n\n private resolveOrganizationId(): string | null {\n if (!this.scope.organizationIds || this.scope.organizationIds.length !== 1) return null\n return this.scope.organizationIds[0] ?? null\n }\n\n private resolveEntityMetadata(tableName: string): Record<string, any> | null {\n const registry = (this.em as any)?.getMetadata?.()\n if (!registry) return null\n const entries =\n (typeof registry.getAll === 'function' && registry.getAll()) ||\n (Array.isArray(registry.metadata) ? registry.metadata : Object.values(registry.metadata ?? {}))\n const metas = Array.isArray(entries) ? entries : Object.values(entries ?? {})\n const match = metas.find((meta: any) => {\n const table = meta?.tableName ?? meta?.collection\n if (typeof table !== 'string') return false\n if (table === tableName) return true\n return table.split('.').pop() === tableName\n })\n return match ?? null\n }\n\n private resolveEntityPropertyName(meta: Record<string, any>, columnName: string): string | null {\n const properties = meta?.properties ? Object.values(meta.properties) : []\n for (const prop of properties as Array<Record<string, any>>) {\n const fieldName = prop?.fieldName\n const fieldNames = prop?.fieldNames\n if (typeof fieldName === 'string' && fieldName === columnName) return prop?.name ?? null\n if (Array.isArray(fieldNames) && fieldNames.includes(columnName)) return prop?.name ?? null\n if (prop?.name === columnName) return prop?.name ?? null\n }\n return null\n }\n\n private resolveEntityId(meta: Record<string, any> | null): string | null {\n if (!meta) return null\n try {\n return resolveEntityIdFromMetadata(meta as any)\n } catch {\n return null\n }\n }\n\n /**\n * Resolves the encryption entity id for an analytics table. A request-scoped `EntityManager` fork\n * frequently reports an empty metadata registry, which used to make every group source look\n * unencrypted and send the request back to the ciphertext-grouping SQL path (#4622). The table\n * name alone is enough for the entity-id lookup, so it is the fallback.\n */\n private resolveEncryptionEntityId(tableName: string): string | null {\n return this.resolveEntityId(this.resolveEntityMetadata(tableName)) ?? this.resolveEntityId({ tableName })\n }\n\n private isEncryptedPayload(value: string): boolean {\n const parts = value.split(':')\n return parts.length === 4 && parts[3] === 'v1'\n }\n\n private decryptWithDek(value: string, dek: string): string | null {\n const first = decryptWithAesGcm(value, dek)\n if (first === null) return null\n if (!this.isEncryptedPayload(first)) return first\n return decryptWithAesGcm(first, dek) ?? first\n }\n}\n\nexport function createWidgetDataService(\n em: EntityManager,\n scope: WidgetDataScope,\n registry: AnalyticsRegistry,\n cache?: CacheStrategy,\n): WidgetDataService {\n return new WidgetDataService({ em, scope, registry, cache })\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -5,9 +5,10 @@ function FeatureGuard({
|
|
|
5
5
|
id,
|
|
6
6
|
children,
|
|
7
7
|
fallback = null,
|
|
8
|
-
loadingFallback = null
|
|
8
|
+
loadingFallback = null,
|
|
9
|
+
defaultValue = false
|
|
9
10
|
}) {
|
|
10
|
-
const { enabled, isLoading } = useFeatureFlagBoolean({ id });
|
|
11
|
+
const { enabled, isLoading } = useFeatureFlagBoolean({ id, defaultValue });
|
|
11
12
|
if (isLoading) {
|
|
12
13
|
return /* @__PURE__ */ jsx(Fragment, { children: loadingFallback });
|
|
13
14
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/feature_toggles/components/FeatureGuard.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useFeatureFlagBoolean } from './hooks/useFeatureFlagBoolean'\n\nexport type FeatureGuardProps = {\n id: string\n children: React.ReactNode\n fallback?: React.ReactNode\n loadingFallback?: React.ReactNode\n}\n\n/**\n * FeatureGuard component that conditionally renders children based on feature toggle state.\n
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useFeatureFlagBoolean } from './hooks/useFeatureFlagBoolean'\n\nexport type FeatureGuardProps = {\n id: string\n children: React.ReactNode\n fallback?: React.ReactNode\n loadingFallback?: React.ReactNode\n defaultValue?: boolean\n}\n\n/**\n * FeatureGuard component that conditionally renders children based on feature toggle state.\n *\n * @param id - The feature toggle identifier\n * @param children - Content to render when the feature is enabled\n * @param fallback - Optional content to render when the feature is disabled\n * @param loadingFallback - Optional content to render while loading the feature state\n * @param defaultValue - Toggle value assumed when the check cannot be resolved\n * (undefined toggle, type mismatch, network or permission failure). Defaults\n * to `false`; pass `true` to keep guarded UI visible when the check fails.\n * Distinct from `fallback`, which is what gets rendered once the feature is\n * known to be disabled.\n */\nexport function FeatureGuard({\n id,\n children,\n fallback = null,\n loadingFallback = null,\n defaultValue = false,\n}: FeatureGuardProps) {\n const { enabled, isLoading } = useFeatureFlagBoolean({ id, defaultValue })\n\n if (isLoading) {\n return <>{loadingFallback}</>\n }\n\n if (!enabled) {\n return <>{fallback}</>\n }\n\n return <>{children}</>\n}\n"],
|
|
5
|
+
"mappings": ";AAoCW;AAjCX,SAAS,6BAA6B;AAuB/B,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,kBAAkB;AAAA,EAClB,eAAe;AACjB,GAAsB;AACpB,QAAM,EAAE,SAAS,UAAU,IAAI,sBAAsB,EAAE,IAAI,aAAa,CAAC;AAEzE,MAAI,WAAW;AACb,WAAO,gCAAG,2BAAgB;AAAA,EAC5B;AAEA,MAAI,CAAC,SAAS;AACZ,WAAO,gCAAG,oBAAS;AAAA,EACrB;AAEA,SAAO,gCAAG,UAAS;AACrB;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/modules/feature_toggles/components/hooks/staleTime.ts"],
|
|
4
|
+
"sourcesContent": ["// Mirrors the server-side toggle resolution cache TTL in\n// `lib/feature-flag-check.ts`, so several components reading the same toggle\n// share one request instead of refetching a value the server would not have\n// re-resolved anyway.\nexport const FEATURE_FLAG_STALE_TIME_MS = 60_000\n"],
|
|
5
|
+
"mappings": "AAIO,MAAM,6BAA6B;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { useQuery } from "@tanstack/react-query";
|
|
3
3
|
import { readApiResultOrThrow } from "@open-mercato/ui/backend/utils/apiCall";
|
|
4
|
+
import { FEATURE_FLAG_STALE_TIME_MS } from "./staleTime.js";
|
|
4
5
|
function useFeatureFlagBoolean(options) {
|
|
6
|
+
const defaultValue = options.defaultValue ?? false;
|
|
5
7
|
const query = useQuery({
|
|
6
8
|
queryKey: ["featureToggles", "check", options?.id],
|
|
7
9
|
queryFn: async () => {
|
|
@@ -15,9 +17,10 @@ function useFeatureFlagBoolean(options) {
|
|
|
15
17
|
);
|
|
16
18
|
return result;
|
|
17
19
|
},
|
|
18
|
-
enabled: !!options.id
|
|
20
|
+
enabled: !!options.id,
|
|
21
|
+
staleTime: FEATURE_FLAG_STALE_TIME_MS
|
|
19
22
|
});
|
|
20
|
-
const enabled = query.data?.ok ? query.data.value :
|
|
23
|
+
const enabled = query.data?.ok ? query.data.value : defaultValue;
|
|
21
24
|
const isLoading = query.isLoading;
|
|
22
25
|
return {
|
|
23
26
|
enabled,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/feature_toggles/components/hooks/useFeatureFlagBoolean.ts"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport { useQuery } from '@tanstack/react-query'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\n\nexport type UseFeatureFlagOptions = {\n id: string\n}\n\nexport type UseFeatureFlagResult = {\n enabled: boolean\n isLoading: boolean\n}\n\ntype Result<T> = {\n ok: true\n value: T\n} | {\n ok: false\n error: unknown\n}\n\nexport function useFeatureFlagBoolean(options: UseFeatureFlagOptions): UseFeatureFlagResult {\n const query = useQuery({\n queryKey: ['featureToggles', 'check', options?.id],\n queryFn: async () => {\n const params = new URLSearchParams({\n identifier: options.id,\n })\n\n const result = await readApiResultOrThrow<Result<boolean>>(\n `/api/feature_toggles/check/boolean?${params.toString()}`,\n undefined,\n { errorMessage: 'Failed to check feature flag.' },\n )\n\n return result\n },\n enabled: !!options.id,\n })\n\n const enabled = query.data?.ok ? query.data.value :
|
|
5
|
-
"mappings": ";AAEA,SAAS,gBAAgB;AACzB,SAAS,4BAA4B;
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport { useQuery } from '@tanstack/react-query'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { FEATURE_FLAG_STALE_TIME_MS } from './staleTime'\n\nexport type UseFeatureFlagOptions = {\n id: string\n /**\n * Value used while the check is in flight and whenever the toggle cannot be\n * resolved (undefined toggle, type mismatch, network or permission failure).\n *\n * Defaults to `false` so an unresolved toggle keeps a feature switched off.\n * Pass `true` for toggles that hide existing UI \u2014 there a failed check must\n * not make the feature disappear.\n */\n defaultValue?: boolean\n}\n\nexport type UseFeatureFlagResult = {\n enabled: boolean\n isLoading: boolean\n}\n\ntype Result<T> = {\n ok: true\n value: T\n} | {\n ok: false\n error: unknown\n}\n\nexport function useFeatureFlagBoolean(options: UseFeatureFlagOptions): UseFeatureFlagResult {\n const defaultValue = options.defaultValue ?? false\n const query = useQuery({\n queryKey: ['featureToggles', 'check', options?.id],\n queryFn: async () => {\n const params = new URLSearchParams({\n identifier: options.id,\n })\n\n const result = await readApiResultOrThrow<Result<boolean>>(\n `/api/feature_toggles/check/boolean?${params.toString()}`,\n undefined,\n { errorMessage: 'Failed to check feature flag.' },\n )\n\n return result\n },\n enabled: !!options.id,\n staleTime: FEATURE_FLAG_STALE_TIME_MS,\n })\n\n const enabled = query.data?.ok ? query.data.value : defaultValue\n const isLoading = query.isLoading\n\n return {\n enabled,\n isLoading,\n }\n}\n"],
|
|
5
|
+
"mappings": ";AAEA,SAAS,gBAAgB;AACzB,SAAS,4BAA4B;AACrC,SAAS,kCAAkC;AA4BpC,SAAS,sBAAsB,SAAsD;AAC1F,QAAM,eAAe,QAAQ,gBAAgB;AAC7C,QAAM,QAAQ,SAAS;AAAA,IACrB,UAAU,CAAC,kBAAkB,SAAS,SAAS,EAAE;AAAA,IACjD,SAAS,YAAY;AACnB,YAAM,SAAS,IAAI,gBAAgB;AAAA,QACjC,YAAY,QAAQ;AAAA,MACtB,CAAC;AAED,YAAM,SAAS,MAAM;AAAA,QACnB,sCAAsC,OAAO,SAAS,CAAC;AAAA,QACvD;AAAA,QACA,EAAE,cAAc,gCAAgC;AAAA,MAClD;AAEA,aAAO;AAAA,IACT;AAAA,IACA,SAAS,CAAC,CAAC,QAAQ;AAAA,IACnB,WAAW;AAAA,EACb,CAAC;AAED,QAAM,UAAU,MAAM,MAAM,KAAK,MAAM,KAAK,QAAQ;AACpD,QAAM,YAAY,MAAM;AAExB,SAAO;AAAA,IACL;AAAA,IACA;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { useQuery } from "@tanstack/react-query";
|
|
3
3
|
import { readApiResultOrThrow } from "@open-mercato/ui/backend/utils/apiCall";
|
|
4
|
+
import { FEATURE_FLAG_STALE_TIME_MS } from "./staleTime.js";
|
|
4
5
|
function useFeatureFlagJson(options) {
|
|
6
|
+
const defaultValue = options.defaultValue ?? null;
|
|
5
7
|
const query = useQuery({
|
|
6
8
|
queryKey: ["featureToggles", "check", "json", options?.id],
|
|
7
9
|
queryFn: async () => {
|
|
@@ -15,9 +17,10 @@ function useFeatureFlagJson(options) {
|
|
|
15
17
|
);
|
|
16
18
|
return result;
|
|
17
19
|
},
|
|
18
|
-
enabled: !!options.id
|
|
20
|
+
enabled: !!options.id,
|
|
21
|
+
staleTime: FEATURE_FLAG_STALE_TIME_MS
|
|
19
22
|
});
|
|
20
|
-
const value = query.data?.ok ? query.data.value :
|
|
23
|
+
const value = query.data?.ok ? query.data.value : defaultValue;
|
|
21
24
|
const isLoading = query.isLoading;
|
|
22
25
|
return {
|
|
23
26
|
value,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/feature_toggles/components/hooks/useFeatureFlagJson.ts"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport { useQuery } from '@tanstack/react-query'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\n\nexport type UseFeatureFlagJsonOptions = {\n id: string\n}\n\nexport type UseFeatureFlagJsonResult<T = unknown> = {\n value: T | null\n isLoading: boolean\n}\n\ntype Result<T> = {\n ok: true\n value: T\n} | {\n ok: false\n error: unknown\n}\n\nexport function useFeatureFlagJson<T = unknown>(options: UseFeatureFlagJsonOptions): UseFeatureFlagJsonResult<T> {\n const query = useQuery({\n queryKey: ['featureToggles', 'check', 'json', options?.id],\n queryFn: async () => {\n const params = new URLSearchParams({\n identifier: options.id,\n })\n\n const result = await readApiResultOrThrow<Result<T>>(\n `/api/feature_toggles/check/json?${params.toString()}`,\n undefined,\n { errorMessage: 'Failed to check feature flag.' },\n )\n\n return result\n },\n enabled: !!options.id,\n })\n\n const value = query.data?.ok ? query.data.value :
|
|
5
|
-
"mappings": ";AAEA,SAAS,gBAAgB;AACzB,SAAS,4BAA4B;
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport { useQuery } from '@tanstack/react-query'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { FEATURE_FLAG_STALE_TIME_MS } from './staleTime'\n\nexport type UseFeatureFlagJsonOptions<T = unknown> = {\n id: string\n /**\n * Value used while the check is in flight and whenever the toggle cannot be\n * resolved (undefined toggle, type mismatch, network or permission failure).\n * Defaults to `null`.\n */\n defaultValue?: T | null\n}\n\nexport type UseFeatureFlagJsonResult<T = unknown> = {\n value: T | null\n isLoading: boolean\n}\n\ntype Result<T> = {\n ok: true\n value: T\n} | {\n ok: false\n error: unknown\n}\n\nexport function useFeatureFlagJson<T = unknown>(options: UseFeatureFlagJsonOptions<T>): UseFeatureFlagJsonResult<T> {\n const defaultValue = options.defaultValue ?? null\n const query = useQuery({\n queryKey: ['featureToggles', 'check', 'json', options?.id],\n queryFn: async () => {\n const params = new URLSearchParams({\n identifier: options.id,\n })\n\n const result = await readApiResultOrThrow<Result<T>>(\n `/api/feature_toggles/check/json?${params.toString()}`,\n undefined,\n { errorMessage: 'Failed to check feature flag.' },\n )\n\n return result\n },\n enabled: !!options.id,\n staleTime: FEATURE_FLAG_STALE_TIME_MS,\n })\n\n const value = query.data?.ok ? query.data.value : defaultValue\n const isLoading = query.isLoading\n\n return {\n value,\n isLoading,\n }\n}\n"],
|
|
5
|
+
"mappings": ";AAEA,SAAS,gBAAgB;AACzB,SAAS,4BAA4B;AACrC,SAAS,kCAAkC;AAyBpC,SAAS,mBAAgC,SAAoE;AAChH,QAAM,eAAe,QAAQ,gBAAgB;AAC7C,QAAM,QAAQ,SAAS;AAAA,IACnB,UAAU,CAAC,kBAAkB,SAAS,QAAQ,SAAS,EAAE;AAAA,IACzD,SAAS,YAAY;AACjB,YAAM,SAAS,IAAI,gBAAgB;AAAA,QAC/B,YAAY,QAAQ;AAAA,MACxB,CAAC;AAED,YAAM,SAAS,MAAM;AAAA,QACjB,mCAAmC,OAAO,SAAS,CAAC;AAAA,QACpD;AAAA,QACA,EAAE,cAAc,gCAAgC;AAAA,MACpD;AAEA,aAAO;AAAA,IACX;AAAA,IACA,SAAS,CAAC,CAAC,QAAQ;AAAA,IACnB,WAAW;AAAA,EACf,CAAC;AAED,QAAM,QAAQ,MAAM,MAAM,KAAK,MAAM,KAAK,QAAQ;AAClD,QAAM,YAAY,MAAM;AAExB,SAAO;AAAA,IACH;AAAA,IACA;AAAA,EACJ;AACJ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { useQuery } from "@tanstack/react-query";
|
|
3
3
|
import { readApiResultOrThrow } from "@open-mercato/ui/backend/utils/apiCall";
|
|
4
|
+
import { FEATURE_FLAG_STALE_TIME_MS } from "./staleTime.js";
|
|
4
5
|
function useFeatureFlagNumber(options) {
|
|
6
|
+
const defaultValue = options.defaultValue ?? null;
|
|
5
7
|
const query = useQuery({
|
|
6
8
|
queryKey: ["featureToggles", "check", "number", options?.id],
|
|
7
9
|
queryFn: async () => {
|
|
@@ -15,9 +17,10 @@ function useFeatureFlagNumber(options) {
|
|
|
15
17
|
);
|
|
16
18
|
return result;
|
|
17
19
|
},
|
|
18
|
-
enabled: !!options.id
|
|
20
|
+
enabled: !!options.id,
|
|
21
|
+
staleTime: FEATURE_FLAG_STALE_TIME_MS
|
|
19
22
|
});
|
|
20
|
-
const value = query.data?.ok ? query.data.value :
|
|
23
|
+
const value = query.data?.ok ? query.data.value : defaultValue;
|
|
21
24
|
const isLoading = query.isLoading;
|
|
22
25
|
return {
|
|
23
26
|
value,
|