@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.
Files changed (154) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/auth/api/admin/nav.js +14 -4
  3. package/dist/modules/auth/api/admin/nav.js.map +2 -2
  4. package/dist/modules/auth/api/users/route.js +8 -19
  5. package/dist/modules/auth/api/users/route.js.map +2 -2
  6. package/dist/modules/auth/frontend/login.js +4 -1
  7. package/dist/modules/auth/frontend/login.js.map +2 -2
  8. package/dist/modules/auth/lib/backendChrome.js +31 -14
  9. package/dist/modules/auth/lib/backendChrome.js.map +2 -2
  10. package/dist/modules/customers/api/companies/[id]/route.js +3 -4
  11. package/dist/modules/customers/api/companies/[id]/route.js.map +2 -2
  12. package/dist/modules/customers/api/people/[id]/companies/enriched/route.js +3 -2
  13. package/dist/modules/customers/api/people/[id]/companies/enriched/route.js.map +2 -2
  14. package/dist/modules/customers/api/utils.js +12 -18
  15. package/dist/modules/customers/api/utils.js.map +2 -2
  16. package/dist/modules/customers/cli.js +11 -4
  17. package/dist/modules/customers/cli.js.map +2 -2
  18. package/dist/modules/customers/components/detail/ActiveDealCard.js +2 -1
  19. package/dist/modules/customers/components/detail/ActiveDealCard.js.map +2 -2
  20. package/dist/modules/customers/components/detail/CompanyKpiBar.js +6 -5
  21. package/dist/modules/customers/components/detail/CompanyKpiBar.js.map +2 -2
  22. package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +4 -2
  23. package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
  24. package/dist/modules/customers/components/detail/dashboard/helpers.js +4 -3
  25. package/dist/modules/customers/components/detail/dashboard/helpers.js.map +2 -2
  26. package/dist/modules/customers/components/formConfig.js +14 -10
  27. package/dist/modules/customers/components/formConfig.js.map +2 -2
  28. package/dist/modules/customers/lib/dealStatus.js +38 -0
  29. package/dist/modules/customers/lib/dealStatus.js.map +7 -0
  30. package/dist/modules/dashboards/api/widgets/data/batch/route.js +4 -2
  31. package/dist/modules/dashboards/api/widgets/data/batch/route.js.map +2 -2
  32. package/dist/modules/dashboards/api/widgets/data/route.js +20 -2
  33. package/dist/modules/dashboards/api/widgets/data/route.js.map +2 -2
  34. package/dist/modules/dashboards/lib/aggregations.js +76 -40
  35. package/dist/modules/dashboards/lib/aggregations.js.map +2 -2
  36. package/dist/modules/dashboards/lib/exactDecimal.js +69 -0
  37. package/dist/modules/dashboards/lib/exactDecimal.js.map +7 -0
  38. package/dist/modules/dashboards/services/widgetDataService.js +228 -5
  39. package/dist/modules/dashboards/services/widgetDataService.js.map +2 -2
  40. package/dist/modules/feature_toggles/components/FeatureGuard.js +3 -2
  41. package/dist/modules/feature_toggles/components/FeatureGuard.js.map +2 -2
  42. package/dist/modules/feature_toggles/components/hooks/staleTime.js +5 -0
  43. package/dist/modules/feature_toggles/components/hooks/staleTime.js.map +7 -0
  44. package/dist/modules/feature_toggles/components/hooks/useFeatureFlagBoolean.js +5 -2
  45. package/dist/modules/feature_toggles/components/hooks/useFeatureFlagBoolean.js.map +2 -2
  46. package/dist/modules/feature_toggles/components/hooks/useFeatureFlagJson.js +5 -2
  47. package/dist/modules/feature_toggles/components/hooks/useFeatureFlagJson.js.map +2 -2
  48. package/dist/modules/feature_toggles/components/hooks/useFeatureFlagNumber.js +5 -2
  49. package/dist/modules/feature_toggles/components/hooks/useFeatureFlagNumber.js.map +2 -2
  50. package/dist/modules/feature_toggles/components/hooks/useFeatureFlagString.js +5 -2
  51. package/dist/modules/feature_toggles/components/hooks/useFeatureFlagString.js.map +2 -2
  52. package/dist/modules/inbox_ops/api/proposals/route.js +16 -1
  53. package/dist/modules/inbox_ops/api/proposals/route.js.map +2 -2
  54. package/dist/modules/inbox_ops/subscribers/search-reindex-proposal-created.js +32 -0
  55. package/dist/modules/inbox_ops/subscribers/search-reindex-proposal-created.js.map +7 -0
  56. package/dist/modules/integrations/widgets/injection/external-ids/widget.client.js +0 -5
  57. package/dist/modules/integrations/widgets/injection/external-ids/widget.client.js.map +2 -2
  58. package/dist/modules/integrations/widgets/injection/external-ids/widget.js +14 -0
  59. package/dist/modules/integrations/widgets/injection/external-ids/widget.js.map +7 -0
  60. package/dist/modules/messages/lib/searchLookup.js +8 -16
  61. package/dist/modules/messages/lib/searchLookup.js.map +2 -2
  62. package/dist/modules/payment_gateways/api/sessions/route.js +5 -0
  63. package/dist/modules/payment_gateways/api/sessions/route.js.map +2 -2
  64. package/dist/modules/payment_gateways/di.js +17 -1
  65. package/dist/modules/payment_gateways/di.js.map +2 -2
  66. package/dist/modules/payment_gateways/lib/gateway-service.js +8 -0
  67. package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
  68. package/dist/modules/payment_gateways/lib/order-amount-reconciliation.js +58 -0
  69. package/dist/modules/payment_gateways/lib/order-amount-reconciliation.js.map +7 -0
  70. package/dist/modules/query_index/api/openapi.js +13 -1
  71. package/dist/modules/query_index/api/openapi.js.map +2 -2
  72. package/dist/modules/query_index/api/status.js +165 -105
  73. package/dist/modules/query_index/api/status.js.map +2 -2
  74. package/dist/modules/query_index/components/QueryIndexesTable.js +128 -41
  75. package/dist/modules/query_index/components/QueryIndexesTable.js.map +2 -2
  76. package/dist/modules/query_index/lib/batch.js +11 -4
  77. package/dist/modules/query_index/lib/batch.js.map +2 -2
  78. package/dist/modules/query_index/lib/document.js +12 -5
  79. package/dist/modules/query_index/lib/document.js.map +2 -2
  80. package/dist/modules/query_index/lib/engine.js +36 -4
  81. package/dist/modules/query_index/lib/engine.js.map +2 -2
  82. package/dist/modules/query_index/lib/indexer.js +1 -1
  83. package/dist/modules/query_index/lib/indexer.js.map +2 -2
  84. package/dist/modules/query_index/lib/search-tokens.js +7 -4
  85. package/dist/modules/query_index/lib/search-tokens.js.map +2 -2
  86. package/dist/modules/sales/api/documents/factory.js +14 -3
  87. package/dist/modules/sales/api/documents/factory.js.map +2 -2
  88. package/dist/modules/sales/commands/documents.js +22 -1
  89. package/dist/modules/sales/commands/documents.js.map +2 -2
  90. package/dist/modules/sales/components/useSalesChannelsEnabled.js +2 -35
  91. package/dist/modules/sales/components/useSalesChannelsEnabled.js.map +2 -2
  92. package/dist/modules/sales/data/validators.js +25 -4
  93. package/dist/modules/sales/data/validators.js.map +2 -2
  94. package/dist/modules/sales/di.js +7 -0
  95. package/dist/modules/sales/di.js.map +2 -2
  96. package/dist/modules/sales/services/paymentOrderTotalResolver.js +52 -0
  97. package/dist/modules/sales/services/paymentOrderTotalResolver.js.map +7 -0
  98. package/package.json +7 -7
  99. package/src/modules/auth/api/admin/nav.ts +24 -4
  100. package/src/modules/auth/api/users/route.ts +8 -28
  101. package/src/modules/auth/frontend/login.tsx +4 -1
  102. package/src/modules/auth/lib/backendChrome.tsx +51 -13
  103. package/src/modules/customers/api/companies/[id]/route.ts +3 -4
  104. package/src/modules/customers/api/people/[id]/companies/enriched/route.ts +3 -2
  105. package/src/modules/customers/api/utils.ts +12 -31
  106. package/src/modules/customers/cli.ts +11 -4
  107. package/src/modules/customers/components/detail/ActiveDealCard.tsx +2 -1
  108. package/src/modules/customers/components/detail/CompanyKpiBar.tsx +6 -5
  109. package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +4 -0
  110. package/src/modules/customers/components/detail/dashboard/helpers.ts +4 -3
  111. package/src/modules/customers/components/formConfig.tsx +12 -8
  112. package/src/modules/customers/lib/dealStatus.ts +48 -0
  113. package/src/modules/dashboards/api/widgets/data/batch/route.ts +4 -0
  114. package/src/modules/dashboards/api/widgets/data/route.ts +21 -0
  115. package/src/modules/dashboards/lib/aggregations.ts +122 -43
  116. package/src/modules/dashboards/lib/exactDecimal.ts +96 -0
  117. package/src/modules/dashboards/services/widgetDataService.ts +342 -4
  118. package/src/modules/feature_toggles/components/FeatureGuard.tsx +9 -2
  119. package/src/modules/feature_toggles/components/hooks/staleTime.ts +5 -0
  120. package/src/modules/feature_toggles/components/hooks/useFeatureFlagBoolean.ts +13 -1
  121. package/src/modules/feature_toggles/components/hooks/useFeatureFlagJson.ts +12 -3
  122. package/src/modules/feature_toggles/components/hooks/useFeatureFlagNumber.ts +10 -1
  123. package/src/modules/feature_toggles/components/hooks/useFeatureFlagString.ts +10 -1
  124. package/src/modules/inbox_ops/api/proposals/route.ts +20 -1
  125. package/src/modules/inbox_ops/subscribers/search-reindex-proposal-created.ts +39 -0
  126. package/src/modules/integrations/widgets/injection/external-ids/widget.client.tsx +0 -5
  127. package/src/modules/integrations/widgets/injection/external-ids/widget.ts +13 -0
  128. package/src/modules/messages/lib/searchLookup.ts +11 -34
  129. package/src/modules/payment_gateways/api/sessions/route.ts +5 -0
  130. package/src/modules/payment_gateways/di.ts +28 -2
  131. package/src/modules/payment_gateways/i18n/de.json +3 -0
  132. package/src/modules/payment_gateways/i18n/en.json +3 -0
  133. package/src/modules/payment_gateways/i18n/es.json +3 -0
  134. package/src/modules/payment_gateways/i18n/pl.json +3 -0
  135. package/src/modules/payment_gateways/lib/gateway-service.ts +14 -0
  136. package/src/modules/payment_gateways/lib/order-amount-reconciliation.ts +93 -0
  137. package/src/modules/query_index/api/openapi.ts +12 -0
  138. package/src/modules/query_index/api/status.ts +238 -136
  139. package/src/modules/query_index/components/QueryIndexesTable.tsx +192 -46
  140. package/src/modules/query_index/i18n/de.json +5 -3
  141. package/src/modules/query_index/i18n/en.json +5 -3
  142. package/src/modules/query_index/i18n/es.json +5 -3
  143. package/src/modules/query_index/i18n/pl.json +5 -3
  144. package/src/modules/query_index/lib/batch.ts +11 -4
  145. package/src/modules/query_index/lib/document.ts +35 -4
  146. package/src/modules/query_index/lib/engine.ts +53 -5
  147. package/src/modules/query_index/lib/indexer.ts +1 -1
  148. package/src/modules/query_index/lib/search-tokens.ts +13 -4
  149. package/src/modules/sales/api/documents/factory.ts +28 -3
  150. package/src/modules/sales/commands/documents.ts +26 -2
  151. package/src/modules/sales/components/useSalesChannelsEnabled.ts +2 -48
  152. package/src/modules/sales/data/validators.ts +50 -5
  153. package/src/modules/sales/di.ts +9 -0
  154. package/src/modules/sales/services/paymentOrderTotalResolver.ts +67 -0
@@ -0,0 +1,38 @@
1
+ const DEAL_STATUS_WIN = "win";
2
+ const DEAL_STATUS_LOSE = "loose";
3
+ const DEAL_STATUS_CLOSED = "closed";
4
+ const WON_DEAL_STATUS_LIST = [DEAL_STATUS_WIN, "won"];
5
+ const LOST_DEAL_STATUS_LIST = [DEAL_STATUS_LOSE, "lost"];
6
+ const CLOSED_DEAL_STATUS_LIST = [
7
+ ...WON_DEAL_STATUS_LIST,
8
+ ...LOST_DEAL_STATUS_LIST,
9
+ DEAL_STATUS_CLOSED
10
+ ];
11
+ const WON_DEAL_STATUSES = new Set(WON_DEAL_STATUS_LIST);
12
+ const LOST_DEAL_STATUSES = new Set(LOST_DEAL_STATUS_LIST);
13
+ const CLOSED_DEAL_STATUSES = new Set(CLOSED_DEAL_STATUS_LIST);
14
+ function isWonDealStatus(value) {
15
+ return value != null && WON_DEAL_STATUSES.has(value);
16
+ }
17
+ function isLostDealStatus(value) {
18
+ return value != null && LOST_DEAL_STATUSES.has(value);
19
+ }
20
+ function isClosedDealStatus(value) {
21
+ return value != null && CLOSED_DEAL_STATUSES.has(value);
22
+ }
23
+ function isOpenDealStatus(value) {
24
+ return !isClosedDealStatus(value);
25
+ }
26
+ export {
27
+ CLOSED_DEAL_STATUS_LIST,
28
+ DEAL_STATUS_CLOSED,
29
+ DEAL_STATUS_LOSE,
30
+ DEAL_STATUS_WIN,
31
+ LOST_DEAL_STATUS_LIST,
32
+ WON_DEAL_STATUS_LIST,
33
+ isClosedDealStatus,
34
+ isLostDealStatus,
35
+ isOpenDealStatus,
36
+ isWonDealStatus
37
+ };
38
+ //# sourceMappingURL=dealStatus.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/customers/lib/dealStatus.ts"],
4
+ "sourcesContent": ["// Single source of truth for deal open/closed (terminal) semantics.\n//\n// `customer_deals.status` is a lenient text column and the supported writers do not agree on\n// one spelling: the closure hooks and the kanban board persist `win` / `loose`, the AI tool\n// `customers.update_deal_stage` persists `won` / `lost`, and the seeded vocabulary also carries\n// `closed`. The terminal set below therefore covers every spelling a status writer persists, so\n// status-based readers can share one classification. `closureOutcome` is a separate signal and\n// callers that combine both columns must handle it explicitly.\n//\n// A status not known to code counts as OPEN. Deal stages are configurable per tenant, so the\n// lenient column can hold values this module has never seen \u2014 treating them as open keeps them\n// visible in active-deal counts instead of silently reclassifying them as closed.\n\nexport const DEAL_STATUS_WIN = 'win' as const\nexport const DEAL_STATUS_LOSE = 'loose' as const\nexport const DEAL_STATUS_CLOSED = 'closed' as const\n\n// `won` / `lost` are the spellings the AI stage tool writes; `win` / `loose` are what the UI\n// closure flows persist. Both are accepted so neither writer produces an unreadable status.\nexport const WON_DEAL_STATUS_LIST: readonly string[] = [DEAL_STATUS_WIN, 'won']\n\nexport const LOST_DEAL_STATUS_LIST: readonly string[] = [DEAL_STATUS_LOSE, 'lost']\n\nexport const CLOSED_DEAL_STATUS_LIST: readonly string[] = [\n ...WON_DEAL_STATUS_LIST,\n ...LOST_DEAL_STATUS_LIST,\n DEAL_STATUS_CLOSED,\n]\n\nconst WON_DEAL_STATUSES = new Set<string>(WON_DEAL_STATUS_LIST)\nconst LOST_DEAL_STATUSES = new Set<string>(LOST_DEAL_STATUS_LIST)\nconst CLOSED_DEAL_STATUSES = new Set<string>(CLOSED_DEAL_STATUS_LIST)\n\nexport function isWonDealStatus(value: string | null | undefined): boolean {\n return value != null && WON_DEAL_STATUSES.has(value)\n}\n\nexport function isLostDealStatus(value: string | null | undefined): boolean {\n return value != null && LOST_DEAL_STATUSES.has(value)\n}\n\nexport function isClosedDealStatus(value: string | null | undefined): boolean {\n return value != null && CLOSED_DEAL_STATUSES.has(value)\n}\n\nexport function isOpenDealStatus(value: string | null | undefined): boolean {\n return !isClosedDealStatus(value)\n}\n"],
5
+ "mappings": "AAaO,MAAM,kBAAkB;AACxB,MAAM,mBAAmB;AACzB,MAAM,qBAAqB;AAI3B,MAAM,uBAA0C,CAAC,iBAAiB,KAAK;AAEvE,MAAM,wBAA2C,CAAC,kBAAkB,MAAM;AAE1E,MAAM,0BAA6C;AAAA,EACxD,GAAG;AAAA,EACH,GAAG;AAAA,EACH;AACF;AAEA,MAAM,oBAAoB,IAAI,IAAY,oBAAoB;AAC9D,MAAM,qBAAqB,IAAI,IAAY,qBAAqB;AAChE,MAAM,uBAAuB,IAAI,IAAY,uBAAuB;AAE7D,SAAS,gBAAgB,OAA2C;AACzE,SAAO,SAAS,QAAQ,kBAAkB,IAAI,KAAK;AACrD;AAEO,SAAS,iBAAiB,OAA2C;AAC1E,SAAO,SAAS,QAAQ,mBAAmB,IAAI,KAAK;AACtD;AAEO,SAAS,mBAAmB,OAA2C;AAC5E,SAAO,SAAS,QAAQ,qBAAqB,IAAI,KAAK;AACxD;AAEO,SAAS,iBAAiB,OAA2C;AAC1E,SAAO,CAAC,mBAAmB,KAAK;AAClC;",
6
+ "names": []
7
+ }
@@ -5,7 +5,9 @@ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
5
5
  import { resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/directory/utils/organizationScope";
6
6
  import {
7
7
  createWidgetDataService,
8
- WidgetDataValidationError
8
+ WidgetDataValidationError,
9
+ WidgetDataScanLimitError,
10
+ WidgetDataEncryptionUnavailableError
9
11
  } from "../../../../services/widgetDataService.js";
10
12
  import { runWidgetDataBatch } from "../../../../lib/widgetDataBatch.js";
11
13
  import { dashboardsTag, dashboardsErrorSchema } from "../../../openapi.js";
@@ -91,7 +93,7 @@ async function POST(req) {
91
93
  });
92
94
  },
93
95
  fetchOne: (request) => service.fetchWidgetData(request),
94
- describeError: (error) => error instanceof WidgetDataValidationError ? error.message : "An error occurred while processing your request"
96
+ describeError: (error) => error instanceof WidgetDataValidationError || error instanceof WidgetDataScanLimitError || error instanceof WidgetDataEncryptionUnavailableError ? error.message : "An error occurred while processing your request"
95
97
  });
96
98
  return NextResponse.json({ results });
97
99
  } catch (err) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../src/modules/dashboards/api/widgets/data/batch/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport {\n createWidgetDataService,\n type WidgetDataRequest,\n WidgetDataValidationError,\n} from '../../../../services/widgetDataService'\nimport { runWidgetDataBatch } from '../../../../lib/widgetDataBatch'\nimport type { AnalyticsRegistry } from '../../../../services/analyticsRegistry'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { dashboardsTag, dashboardsErrorSchema } from '../../../openapi'\nimport { widgetDataRequestSchema, widgetDataResponseSchema } from '../schema'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('dashboards').child({ component: 'widgets-data-batch' })\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['analytics.view'] },\n}\n\nconst MAX_BATCH_SIZE = 50\n\nconst widgetDataBatchRequestSchema = z.object({\n requests: z\n .array(\n z.object({\n id: z.string().min(1),\n request: widgetDataRequestSchema,\n }),\n )\n .min(1)\n .max(MAX_BATCH_SIZE),\n})\n\nconst widgetDataBatchResponseSchema = z.object({\n results: z.array(\n z.discriminatedUnion('ok', [\n z.object({\n id: z.string(),\n ok: z.literal(true),\n data: widgetDataResponseSchema,\n }),\n z.object({\n id: z.string(),\n ok: z.literal(false),\n error: z.string(),\n }),\n ]),\n ),\n})\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n\n const parsed = widgetDataBatchRequestSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json(\n { error: 'Invalid request payload', issues: parsed.error.issues },\n { status: 400 },\n )\n }\n\n const tenantId = auth.tenantId ?? null\n if (!tenantId) {\n return NextResponse.json({ error: 'Tenant context is required' }, { status: 400 })\n }\n\n // Build the per-request DI/RBAC/org-scope stack exactly once for the whole\n // batch instead of once per widget (see issue #2273).\n const container = await createRequestContainer()\n const analyticsRegistry = container.resolve<AnalyticsRegistry>('analyticsRegistry')\n\n const em = (container.resolve('em') as EntityManager).fork({\n clear: true,\n freshEventManager: true,\n useContext: true,\n })\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n\n const organizationIds = (() => {\n if (scope?.selectedId) return [scope.selectedId]\n if (Array.isArray(scope?.filterIds) && scope.filterIds.length > 0) return scope.filterIds\n if (scope?.allowedIds === null) return undefined\n if (auth.orgId) return [auth.orgId]\n return undefined\n })()\n\n const cache = container.resolve<CacheStrategy>('cache')\n const service = createWidgetDataService(em, { tenantId, organizationIds }, analyticsRegistry, cache)\n\n const rbacService = container.resolve<{\n userHasAllFeatures: (\n userId: string,\n features: string[],\n scope: { tenantId: string; organizationId?: string | null },\n ) => Promise<boolean>\n }>('rbacService')\n\n try {\n const results = await runWidgetDataBatch(parsed.data.requests as Array<{ id: string; request: WidgetDataRequest }>, {\n getRequiredFeatures: (entityType) => analyticsRegistry.getRequiredFeatures(entityType),\n checkFeatures: (features) => {\n if (features.length === 0) return Promise.resolve(true)\n return rbacService.userHasAllFeatures(auth.sub, features, {\n tenantId,\n organizationId: auth.orgId,\n })\n },\n fetchOne: (request) => service.fetchWidgetData(request),\n describeError: (error) =>\n error instanceof WidgetDataValidationError\n ? error.message\n : 'An error occurred while processing your request',\n })\n return NextResponse.json({ results })\n } catch (err) {\n logger.error('Widget batch data request failed', { err })\n return NextResponse.json(\n { error: 'An error occurred while processing your request' },\n { status: 500 },\n )\n }\n}\n\nconst widgetDataBatchPostDoc: OpenApiMethodDoc = {\n summary: 'Fetch aggregated data for multiple dashboard widgets in one request',\n description:\n 'Resolves a batch of widget data requests with a single authentication, RBAC, organization-scope, and database-context setup. Each request is keyed by an opaque widget id and resolved independently, so a failure in one widget does not fail the batch.',\n tags: [dashboardsTag],\n requestBody: {\n contentType: 'application/json',\n schema: widgetDataBatchRequestSchema,\n description: 'A list of id-keyed widget data requests to resolve together.',\n },\n responses: [\n {\n status: 200,\n description: 'Per-widget aggregation results keyed by request id.',\n schema: widgetDataBatchResponseSchema,\n },\n ],\n errors: [\n { status: 400, description: 'Invalid request payload', schema: dashboardsErrorSchema },\n { status: 401, description: 'Authentication required', schema: dashboardsErrorSchema },\n { status: 500, description: 'Internal server error', schema: dashboardsErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: dashboardsTag,\n summary: 'Batch widget data aggregation endpoint',\n methods: {\n POST: widgetDataBatchPostDoc,\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport {\n createWidgetDataService,\n type WidgetDataRequest,\n WidgetDataValidationError,\n WidgetDataScanLimitError,\n WidgetDataEncryptionUnavailableError,\n} from '../../../../services/widgetDataService'\nimport { runWidgetDataBatch } from '../../../../lib/widgetDataBatch'\nimport type { AnalyticsRegistry } from '../../../../services/analyticsRegistry'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { dashboardsTag, dashboardsErrorSchema } from '../../../openapi'\nimport { widgetDataRequestSchema, widgetDataResponseSchema } from '../schema'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('dashboards').child({ component: 'widgets-data-batch' })\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['analytics.view'] },\n}\n\nconst MAX_BATCH_SIZE = 50\n\nconst widgetDataBatchRequestSchema = z.object({\n requests: z\n .array(\n z.object({\n id: z.string().min(1),\n request: widgetDataRequestSchema,\n }),\n )\n .min(1)\n .max(MAX_BATCH_SIZE),\n})\n\nconst widgetDataBatchResponseSchema = z.object({\n results: z.array(\n z.discriminatedUnion('ok', [\n z.object({\n id: z.string(),\n ok: z.literal(true),\n data: widgetDataResponseSchema,\n }),\n z.object({\n id: z.string(),\n ok: z.literal(false),\n error: z.string(),\n }),\n ]),\n ),\n})\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n\n const parsed = widgetDataBatchRequestSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json(\n { error: 'Invalid request payload', issues: parsed.error.issues },\n { status: 400 },\n )\n }\n\n const tenantId = auth.tenantId ?? null\n if (!tenantId) {\n return NextResponse.json({ error: 'Tenant context is required' }, { status: 400 })\n }\n\n // Build the per-request DI/RBAC/org-scope stack exactly once for the whole\n // batch instead of once per widget (see issue #2273).\n const container = await createRequestContainer()\n const analyticsRegistry = container.resolve<AnalyticsRegistry>('analyticsRegistry')\n\n const em = (container.resolve('em') as EntityManager).fork({\n clear: true,\n freshEventManager: true,\n useContext: true,\n })\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n\n const organizationIds = (() => {\n if (scope?.selectedId) return [scope.selectedId]\n if (Array.isArray(scope?.filterIds) && scope.filterIds.length > 0) return scope.filterIds\n if (scope?.allowedIds === null) return undefined\n if (auth.orgId) return [auth.orgId]\n return undefined\n })()\n\n const cache = container.resolve<CacheStrategy>('cache')\n const service = createWidgetDataService(em, { tenantId, organizationIds }, analyticsRegistry, cache)\n\n const rbacService = container.resolve<{\n userHasAllFeatures: (\n userId: string,\n features: string[],\n scope: { tenantId: string; organizationId?: string | null },\n ) => Promise<boolean>\n }>('rbacService')\n\n try {\n const results = await runWidgetDataBatch(parsed.data.requests as Array<{ id: string; request: WidgetDataRequest }>, {\n getRequiredFeatures: (entityType) => analyticsRegistry.getRequiredFeatures(entityType),\n checkFeatures: (features) => {\n if (features.length === 0) return Promise.resolve(true)\n return rbacService.userHasAllFeatures(auth.sub, features, {\n tenantId,\n organizationId: auth.orgId,\n })\n },\n fetchOne: (request) => service.fetchWidgetData(request),\n describeError: (error) =>\n error instanceof WidgetDataValidationError\n || error instanceof WidgetDataScanLimitError\n || error instanceof WidgetDataEncryptionUnavailableError\n ? error.message\n : 'An error occurred while processing your request',\n })\n return NextResponse.json({ results })\n } catch (err) {\n logger.error('Widget batch data request failed', { err })\n return NextResponse.json(\n { error: 'An error occurred while processing your request' },\n { status: 500 },\n )\n }\n}\n\nconst widgetDataBatchPostDoc: OpenApiMethodDoc = {\n summary: 'Fetch aggregated data for multiple dashboard widgets in one request',\n description:\n 'Resolves a batch of widget data requests with a single authentication, RBAC, organization-scope, and database-context setup. Each request is keyed by an opaque widget id and resolved independently, so a failure in one widget does not fail the batch.',\n tags: [dashboardsTag],\n requestBody: {\n contentType: 'application/json',\n schema: widgetDataBatchRequestSchema,\n description: 'A list of id-keyed widget data requests to resolve together.',\n },\n responses: [\n {\n status: 200,\n description: 'Per-widget aggregation results keyed by request id.',\n schema: widgetDataBatchResponseSchema,\n },\n ],\n errors: [\n { status: 400, description: 'Invalid request payload', schema: dashboardsErrorSchema },\n { status: 401, description: 'Authentication required', schema: dashboardsErrorSchema },\n { status: 500, description: 'Internal server error', schema: dashboardsErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: dashboardsTag,\n summary: 'Batch widget data aggregation endpoint',\n methods: {\n POST: widgetDataBatchPostDoc,\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -4,7 +4,9 @@ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
4
4
  import { resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/directory/utils/organizationScope";
5
5
  import {
6
6
  createWidgetDataService,
7
- WidgetDataValidationError
7
+ WidgetDataValidationError,
8
+ WidgetDataScanLimitError,
9
+ WidgetDataEncryptionUnavailableError
8
10
  } from "../../../services/widgetDataService.js";
9
11
  import { runApiInterceptorsBefore } from "@open-mercato/shared/lib/crud/interceptor-runner";
10
12
  import { dashboardsTag, dashboardsErrorSchema } from "../../openapi.js";
@@ -99,6 +101,12 @@ async function POST(req) {
99
101
  if (err instanceof WidgetDataValidationError) {
100
102
  return NextResponse.json({ error: err.message }, { status: 400 });
101
103
  }
104
+ if (err instanceof WidgetDataScanLimitError) {
105
+ return NextResponse.json({ error: err.message }, { status: 422 });
106
+ }
107
+ if (err instanceof WidgetDataEncryptionUnavailableError) {
108
+ return NextResponse.json({ error: err.message }, { status: 503 });
109
+ }
102
110
  return NextResponse.json(
103
111
  { error: "An error occurred while processing your request" },
104
112
  { status: 500 }
@@ -124,8 +132,18 @@ const widgetDataPostDoc = {
124
132
  errors: [
125
133
  { status: 400, description: "Invalid request payload", schema: dashboardsErrorSchema },
126
134
  { status: 401, description: "Authentication required", schema: dashboardsErrorSchema },
135
+ {
136
+ status: 422,
137
+ description: "Too many rows to group an encrypted field in application code",
138
+ schema: dashboardsErrorSchema
139
+ },
127
140
  { status: 403, description: "Missing analytics.view feature", schema: dashboardsErrorSchema },
128
- { status: 500, description: "Internal server error", schema: dashboardsErrorSchema }
141
+ { status: 500, description: "Internal server error", schema: dashboardsErrorSchema },
142
+ {
143
+ status: 503,
144
+ description: "Encryption is configured but the group source cannot currently be resolved",
145
+ schema: dashboardsErrorSchema
146
+ }
129
147
  ]
130
148
  };
131
149
  const openApi = {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/dashboards/api/widgets/data/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport {\n createWidgetDataService,\n type WidgetDataRequest,\n WidgetDataValidationError,\n} from '../../../services/widgetDataService'\nimport type { AnalyticsRegistry } from '../../../services/analyticsRegistry'\nimport { runApiInterceptorsBefore } from '@open-mercato/shared/lib/crud/interceptor-runner'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { dashboardsTag, dashboardsErrorSchema } from '../../openapi'\nimport { widgetDataRequestSchema, widgetDataResponseSchema } from './schema'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('dashboards').child({ component: 'widgets-data' })\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['analytics.view'] },\n}\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n\n const parsed = widgetDataRequestSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json(\n { error: 'Invalid request payload', issues: parsed.error.issues },\n { status: 400 },\n )\n }\n\n const container = await createRequestContainer()\n const analyticsRegistry = container.resolve<AnalyticsRegistry>('analyticsRegistry')\n\n const entityFeatures = analyticsRegistry.getRequiredFeatures(parsed.data.entityType)\n if (entityFeatures && entityFeatures.length > 0) {\n const rbacService = container.resolve<{\n userHasAllFeatures: (\n userId: string,\n features: string[],\n scope: { tenantId: string; organizationId?: string | null },\n ) => Promise<boolean>\n }>('rbacService')\n const hasAccess = await rbacService.userHasAllFeatures(auth.sub, entityFeatures, {\n tenantId: auth.tenantId!,\n organizationId: auth.orgId,\n })\n if (!hasAccess) {\n return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n }\n\n const em = (container.resolve('em') as EntityManager).fork({\n clear: true,\n freshEventManager: true,\n useContext: true,\n })\n\n const tenantId = auth.tenantId ?? null\n if (!tenantId) {\n return NextResponse.json({ error: 'Tenant context is required' }, { status: 400 })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n\n const organizationIds = (() => {\n if (scope?.selectedId) return [scope.selectedId]\n if (Array.isArray(scope?.filterIds) && scope.filterIds.length > 0) return scope.filterIds\n if (scope?.allowedIds === null) return undefined\n if (auth.orgId) return [auth.orgId]\n return undefined\n })()\n\n const userFeatures = Array.isArray(auth.features)\n ? auth.features.filter((value): value is string => typeof value === 'string')\n : []\n\n const headers: Record<string, string> = {}\n req.headers.forEach((value, key) => {\n headers[key] = value\n })\n\n const interceptorResult = await runApiInterceptorsBefore({\n routePath: 'dashboards/widgets/data',\n method: 'POST',\n request: {\n method: 'POST',\n url: req.url,\n headers,\n body: parsed.data as unknown as Record<string, unknown>,\n },\n context: {\n em,\n container,\n userId: auth.sub ?? '',\n tenantId,\n organizationId: organizationIds?.[0] ?? auth.orgId ?? '',\n userFeatures,\n },\n })\n\n if (!interceptorResult.ok) {\n return NextResponse.json(interceptorResult.body, { status: interceptorResult.statusCode })\n }\n\n const requestData = (interceptorResult.request.body ?? parsed.data) as WidgetDataRequest\n\n try {\n const cache = container.resolve<CacheStrategy>('cache')\n const service = createWidgetDataService(em, { tenantId, organizationIds }, analyticsRegistry, cache)\n const result = await service.fetchWidgetData(requestData)\n return NextResponse.json(result)\n } catch (err) {\n logger.error('Widget data request failed', { err })\n if (err instanceof WidgetDataValidationError) {\n return NextResponse.json({ error: err.message }, { status: 400 })\n }\n return NextResponse.json(\n { error: 'An error occurred while processing your request' },\n { status: 500 },\n )\n }\n}\n\nconst widgetDataPostDoc: OpenApiMethodDoc = {\n summary: 'Fetch aggregated data for dashboard widgets',\n description:\n 'Executes an aggregation query against the specified entity type and returns the result. Supports date range filtering, grouping, and period-over-period comparison.',\n tags: [dashboardsTag],\n requestBody: {\n contentType: 'application/json',\n schema: widgetDataRequestSchema,\n description: 'Widget data request configuration specifying entity type, metric, filters, and grouping.',\n },\n responses: [\n {\n status: 200,\n description: 'Aggregated data for the widget.',\n schema: widgetDataResponseSchema,\n },\n ],\n errors: [\n { status: 400, description: 'Invalid request payload', schema: dashboardsErrorSchema },\n { status: 401, description: 'Authentication required', schema: dashboardsErrorSchema },\n { status: 403, description: 'Missing analytics.view feature', schema: dashboardsErrorSchema },\n { status: 500, description: 'Internal server error', schema: dashboardsErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: dashboardsTag,\n summary: 'Widget data aggregation endpoint',\n methods: {\n POST: widgetDataPostDoc,\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport {\n createWidgetDataService,\n type WidgetDataRequest,\n WidgetDataValidationError,\n WidgetDataScanLimitError,\n WidgetDataEncryptionUnavailableError,\n} from '../../../services/widgetDataService'\nimport type { AnalyticsRegistry } from '../../../services/analyticsRegistry'\nimport { runApiInterceptorsBefore } from '@open-mercato/shared/lib/crud/interceptor-runner'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { dashboardsTag, dashboardsErrorSchema } from '../../openapi'\nimport { widgetDataRequestSchema, widgetDataResponseSchema } from './schema'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('dashboards').child({ component: 'widgets-data' })\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['analytics.view'] },\n}\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n\n const parsed = widgetDataRequestSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json(\n { error: 'Invalid request payload', issues: parsed.error.issues },\n { status: 400 },\n )\n }\n\n const container = await createRequestContainer()\n const analyticsRegistry = container.resolve<AnalyticsRegistry>('analyticsRegistry')\n\n const entityFeatures = analyticsRegistry.getRequiredFeatures(parsed.data.entityType)\n if (entityFeatures && entityFeatures.length > 0) {\n const rbacService = container.resolve<{\n userHasAllFeatures: (\n userId: string,\n features: string[],\n scope: { tenantId: string; organizationId?: string | null },\n ) => Promise<boolean>\n }>('rbacService')\n const hasAccess = await rbacService.userHasAllFeatures(auth.sub, entityFeatures, {\n tenantId: auth.tenantId!,\n organizationId: auth.orgId,\n })\n if (!hasAccess) {\n return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n }\n\n const em = (container.resolve('em') as EntityManager).fork({\n clear: true,\n freshEventManager: true,\n useContext: true,\n })\n\n const tenantId = auth.tenantId ?? null\n if (!tenantId) {\n return NextResponse.json({ error: 'Tenant context is required' }, { status: 400 })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n\n const organizationIds = (() => {\n if (scope?.selectedId) return [scope.selectedId]\n if (Array.isArray(scope?.filterIds) && scope.filterIds.length > 0) return scope.filterIds\n if (scope?.allowedIds === null) return undefined\n if (auth.orgId) return [auth.orgId]\n return undefined\n })()\n\n const userFeatures = Array.isArray(auth.features)\n ? auth.features.filter((value): value is string => typeof value === 'string')\n : []\n\n const headers: Record<string, string> = {}\n req.headers.forEach((value, key) => {\n headers[key] = value\n })\n\n const interceptorResult = await runApiInterceptorsBefore({\n routePath: 'dashboards/widgets/data',\n method: 'POST',\n request: {\n method: 'POST',\n url: req.url,\n headers,\n body: parsed.data as unknown as Record<string, unknown>,\n },\n context: {\n em,\n container,\n userId: auth.sub ?? '',\n tenantId,\n organizationId: organizationIds?.[0] ?? auth.orgId ?? '',\n userFeatures,\n },\n })\n\n if (!interceptorResult.ok) {\n return NextResponse.json(interceptorResult.body, { status: interceptorResult.statusCode })\n }\n\n const requestData = (interceptorResult.request.body ?? parsed.data) as WidgetDataRequest\n\n try {\n const cache = container.resolve<CacheStrategy>('cache')\n const service = createWidgetDataService(em, { tenantId, organizationIds }, analyticsRegistry, cache)\n const result = await service.fetchWidgetData(requestData)\n return NextResponse.json(result)\n } catch (err) {\n logger.error('Widget data request failed', { err })\n if (err instanceof WidgetDataValidationError) {\n return NextResponse.json({ error: err.message }, { status: 400 })\n }\n // The dataset is too large to group in application code; surface why rather than a masked 500.\n if (err instanceof WidgetDataScanLimitError) {\n return NextResponse.json({ error: err.message }, { status: 422 })\n }\n // Encryption is configured but currently unresolvable. Refusing the request keeps ciphertext and\n // silently-wrong buckets out of the response, and 503 tells the client it is worth retrying.\n if (err instanceof WidgetDataEncryptionUnavailableError) {\n return NextResponse.json({ error: err.message }, { status: 503 })\n }\n return NextResponse.json(\n { error: 'An error occurred while processing your request' },\n { status: 500 },\n )\n }\n}\n\nconst widgetDataPostDoc: OpenApiMethodDoc = {\n summary: 'Fetch aggregated data for dashboard widgets',\n description:\n 'Executes an aggregation query against the specified entity type and returns the result. Supports date range filtering, grouping, and period-over-period comparison.',\n tags: [dashboardsTag],\n requestBody: {\n contentType: 'application/json',\n schema: widgetDataRequestSchema,\n description: 'Widget data request configuration specifying entity type, metric, filters, and grouping.',\n },\n responses: [\n {\n status: 200,\n description: 'Aggregated data for the widget.',\n schema: widgetDataResponseSchema,\n },\n ],\n errors: [\n { status: 400, description: 'Invalid request payload', schema: dashboardsErrorSchema },\n { status: 401, description: 'Authentication required', schema: dashboardsErrorSchema },\n {\n status: 422,\n description: 'Too many rows to group an encrypted field in application code',\n schema: dashboardsErrorSchema,\n },\n { status: 403, description: 'Missing analytics.view feature', schema: dashboardsErrorSchema },\n { status: 500, description: 'Internal server error', schema: dashboardsErrorSchema },\n {\n status: 503,\n description: 'Encryption is configured but the group source cannot currently be resolved',\n schema: dashboardsErrorSchema,\n },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: dashboardsTag,\n summary: 'Widget data aggregation endpoint',\n methods: {\n POST: widgetDataPostDoc,\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -46,44 +46,32 @@ function buildJsonbFieldExpression(column, path) {
46
46
  const lastPart = parts[parts.length - 1];
47
47
  return `${column}->${intermediate}->>'${lastPart}'`;
48
48
  }
49
- function buildAggregationQuery(options) {
49
+ function resolveGroupExpression(registry, entityType, groupBy) {
50
+ const groupMapping = registry.getFieldMapping(entityType, groupBy.field);
51
+ if (!groupMapping && groupBy.field.includes(".")) {
52
+ const [baseField, ...pathParts] = groupBy.field.split(".");
53
+ const baseMapping = registry.getFieldMapping(entityType, baseField);
54
+ if (baseMapping?.type !== "jsonb") return null;
55
+ const jsonPath = pathParts.join(".");
56
+ return {
57
+ expression: buildJsonbFieldExpression(baseMapping.dbColumn, jsonPath),
58
+ dbColumn: baseMapping.dbColumn,
59
+ jsonPath
60
+ };
61
+ }
62
+ if (!groupMapping) return null;
63
+ if (groupMapping.type === "timestamp" && groupBy.granularity) {
64
+ return {
65
+ expression: buildDateTruncExpression(groupMapping.dbColumn, groupBy.granularity),
66
+ dbColumn: groupMapping.dbColumn,
67
+ jsonPath: null
68
+ };
69
+ }
70
+ return { expression: groupMapping.dbColumn, dbColumn: groupMapping.dbColumn, jsonPath: null };
71
+ }
72
+ function buildWhereClause(options) {
50
73
  const { registry } = options;
51
- const config = registry.getEntityTypeConfig(options.entityType);
52
- if (!config) return null;
53
- const metricMapping = registry.getFieldMapping(options.entityType, options.metric.field);
54
- if (!metricMapping) return null;
55
74
  const params = [];
56
- const tableName = config.schema ? `"${config.schema}"."${config.tableName}"` : `"${config.tableName}"`;
57
- const aggregateExpr = buildAggregateExpression(options.metric.aggregate, metricMapping.dbColumn);
58
- let selectClause = `SELECT ${aggregateExpr} AS value`;
59
- let groupByClause = "";
60
- let orderByClause = "";
61
- let limitClause = "";
62
- if (options.groupBy) {
63
- let groupMapping = registry.getFieldMapping(options.entityType, options.groupBy.field);
64
- let groupExpr = null;
65
- if (!groupMapping && options.groupBy.field.includes(".")) {
66
- const [baseField, ...pathParts] = options.groupBy.field.split(".");
67
- const baseMapping = registry.getFieldMapping(options.entityType, baseField);
68
- if (baseMapping?.type === "jsonb") {
69
- groupExpr = buildJsonbFieldExpression(baseMapping.dbColumn, pathParts.join("."));
70
- }
71
- } else if (groupMapping) {
72
- if (groupMapping.type === "timestamp" && options.groupBy.granularity) {
73
- groupExpr = buildDateTruncExpression(groupMapping.dbColumn, options.groupBy.granularity);
74
- } else {
75
- groupExpr = groupMapping.dbColumn;
76
- }
77
- }
78
- if (groupExpr) {
79
- selectClause = `SELECT ${groupExpr} AS group_key, ${aggregateExpr} AS value`;
80
- groupByClause = `GROUP BY ${groupExpr}`;
81
- orderByClause = `ORDER BY value DESC`;
82
- if (options.groupBy.limit && options.groupBy.limit > 0) {
83
- limitClause = `LIMIT ${Math.min(options.groupBy.limit, 100)}`;
84
- }
85
- }
86
- }
87
75
  const whereClauses = [];
88
76
  whereClauses.push(`tenant_id = ?`);
89
77
  params.push(options.scope.tenantId);
@@ -147,16 +135,64 @@ function buildAggregationQuery(options) {
147
135
  }
148
136
  }
149
137
  }
150
- const whereClause = whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : "";
151
- const sql = [selectClause, `FROM ${tableName}`, whereClause, groupByClause, orderByClause, limitClause].filter(Boolean).join(" ");
152
- return { sql, params };
138
+ return { clause: whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : "", params };
139
+ }
140
+ function resolveTableName(config) {
141
+ return config.schema ? `"${config.schema}"."${config.tableName}"` : `"${config.tableName}"`;
142
+ }
143
+ function buildAggregationQuery(options) {
144
+ const { registry } = options;
145
+ const config = registry.getEntityTypeConfig(options.entityType);
146
+ if (!config) return null;
147
+ const metricMapping = registry.getFieldMapping(options.entityType, options.metric.field);
148
+ if (!metricMapping) return null;
149
+ const tableName = resolveTableName(config);
150
+ const aggregateExpr = buildAggregateExpression(options.metric.aggregate, metricMapping.dbColumn);
151
+ let selectClause = `SELECT ${aggregateExpr} AS value`;
152
+ let groupByClause = "";
153
+ let orderByClause = "";
154
+ let limitClause = "";
155
+ if (options.groupBy) {
156
+ const resolved = resolveGroupExpression(registry, options.entityType, options.groupBy);
157
+ if (resolved) {
158
+ selectClause = `SELECT ${resolved.expression} AS group_key, ${aggregateExpr} AS value`;
159
+ groupByClause = `GROUP BY ${resolved.expression}`;
160
+ orderByClause = `ORDER BY value DESC NULLS LAST`;
161
+ if (options.groupBy.limit && options.groupBy.limit > 0) {
162
+ limitClause = `LIMIT ${Math.min(options.groupBy.limit, 100)}`;
163
+ }
164
+ }
165
+ }
166
+ const where = buildWhereClause(options);
167
+ const sql = [selectClause, `FROM ${tableName}`, where.clause, groupByClause, orderByClause, limitClause].filter(Boolean).join(" ");
168
+ return { sql, params: where.params };
169
+ }
170
+ function buildGroupSourceRowsQuery(options) {
171
+ const { registry } = options;
172
+ const config = registry.getEntityTypeConfig(options.entityType);
173
+ if (!config) return null;
174
+ const metricMapping = registry.getFieldMapping(options.entityType, options.metric.field);
175
+ if (!metricMapping) return null;
176
+ if (!isSafeIdentifier(options.groupColumn)) {
177
+ throw new Error(`Invalid group column: ${options.groupColumn}`);
178
+ }
179
+ const where = buildWhereClause(options);
180
+ const sql = [
181
+ `SELECT ${options.groupColumn} AS group_source, ${metricMapping.dbColumn} AS metric_value`,
182
+ `FROM ${resolveTableName(config)}`,
183
+ where.clause,
184
+ `LIMIT ${Math.max(1, Math.floor(options.rowLimit)) + 1}`
185
+ ].filter(Boolean).join(" ");
186
+ return { sql, params: where.params };
153
187
  }
154
188
  export {
155
189
  buildAggregateExpression,
156
190
  buildAggregationQuery,
157
191
  buildDateTruncExpression,
192
+ buildGroupSourceRowsQuery,
158
193
  buildJsonbFieldExpression,
159
194
  isValidAggregate,
160
- isValidGranularity
195
+ isValidGranularity,
196
+ resolveGroupExpression
161
197
  };
162
198
  //# sourceMappingURL=aggregations.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/dashboards/lib/aggregations.ts"],
4
- "sourcesContent": ["import type { AnalyticsRegistry } from '../services/analyticsRegistry'\nimport type { AnalyticsEntityTypeConfig, AnalyticsFieldMapping } from '@open-mercato/shared/modules/analytics'\n\nexport type AggregateFunction = 'count' | 'sum' | 'avg' | 'min' | 'max'\nexport type DateGranularity = 'day' | 'week' | 'month' | 'quarter' | 'year'\n\nconst VALID_GRANULARITIES: readonly DateGranularity[] = ['day', 'week', 'month', 'quarter', 'year']\nconst VALID_AGGREGATES: readonly AggregateFunction[] = ['count', 'sum', 'avg', 'min', 'max']\nconst SAFE_IDENTIFIER_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/\n\nexport function isValidGranularity(value: unknown): value is DateGranularity {\n return typeof value === 'string' && VALID_GRANULARITIES.includes(value as DateGranularity)\n}\n\nexport function isValidAggregate(value: unknown): value is AggregateFunction {\n return typeof value === 'string' && VALID_AGGREGATES.includes(value as AggregateFunction)\n}\n\nfunction isSafeIdentifier(value: string): boolean {\n return SAFE_IDENTIFIER_PATTERN.test(value)\n}\n\n// Re-export types from shared module for convenience\nexport type EntityTypeConfig = AnalyticsEntityTypeConfig\nexport type FieldMapping = AnalyticsFieldMapping\n\nexport function buildAggregateExpression(aggregate: AggregateFunction, column: string): string {\n switch (aggregate) {\n case 'count':\n return column === 'id' ? 'COUNT(*)' : `COUNT(${column})`\n case 'sum':\n return `COALESCE(SUM(${column}::numeric), 0)`\n case 'avg':\n return `COALESCE(AVG(${column}::numeric), 0)`\n case 'min':\n return `MIN(${column}::numeric)`\n case 'max':\n return `MAX(${column}::numeric)`\n default:\n return `COUNT(*)`\n }\n}\n\nexport function buildDateTruncExpression(column: string, granularity: DateGranularity): string {\n if (!isValidGranularity(granularity)) {\n throw new Error(`Invalid granularity: ${granularity}`)\n }\n return `DATE_TRUNC('${granularity}', ${column})`\n}\n\nexport function buildJsonbFieldExpression(column: string, path: string): string {\n const parts = path.split('.')\n for (const part of parts) {\n if (!isSafeIdentifier(part)) {\n throw new Error(`Invalid JSONB path part: ${part}`)\n }\n }\n if (parts.length === 1) {\n return `${column}->>'${parts[0]}'`\n }\n const intermediate = parts.slice(0, -1).map((p) => `'${p}'`).join('->')\n const lastPart = parts[parts.length - 1]\n return `${column}->${intermediate}->>'${lastPart}'`\n}\n\nexport type AggregationQuery = {\n sql: string\n params: unknown[]\n}\n\nexport type BuildAggregationQueryOptions = {\n entityType: string\n metric: {\n field: string\n aggregate: AggregateFunction\n }\n groupBy?: {\n field: string\n granularity?: DateGranularity\n limit?: number\n }\n dateRange?: {\n field: string\n start: Date\n end: Date\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 scope: {\n tenantId: string\n organizationIds?: string[]\n }\n /** Analytics registry for resolving entity and field configurations */\n registry: AnalyticsRegistry\n}\n\nexport function buildAggregationQuery(options: BuildAggregationQueryOptions): AggregationQuery | null {\n const { registry } = options\n const config = registry.getEntityTypeConfig(options.entityType)\n if (!config) return null\n\n const metricMapping = registry.getFieldMapping(options.entityType, options.metric.field)\n if (!metricMapping) return null\n\n const params: unknown[] = []\n\n const tableName = config.schema ? `\"${config.schema}\".\"${config.tableName}\"` : `\"${config.tableName}\"`\n const aggregateExpr = buildAggregateExpression(options.metric.aggregate, metricMapping.dbColumn)\n\n let selectClause = `SELECT ${aggregateExpr} AS value`\n let groupByClause = ''\n let orderByClause = ''\n let limitClause = ''\n\n if (options.groupBy) {\n let groupMapping = registry.getFieldMapping(options.entityType, options.groupBy.field)\n let groupExpr: string | null = null\n\n // Handle JSONB path notation (e.g., shippingAddressSnapshot.region)\n if (!groupMapping && options.groupBy.field.includes('.')) {\n const [baseField, ...pathParts] = options.groupBy.field.split('.')\n const baseMapping = registry.getFieldMapping(options.entityType, baseField)\n if (baseMapping?.type === 'jsonb') {\n groupExpr = buildJsonbFieldExpression(baseMapping.dbColumn, pathParts.join('.'))\n }\n } else if (groupMapping) {\n if (groupMapping.type === 'timestamp' && options.groupBy.granularity) {\n groupExpr = buildDateTruncExpression(groupMapping.dbColumn, options.groupBy.granularity)\n } else {\n groupExpr = groupMapping.dbColumn\n }\n }\n\n if (groupExpr) {\n selectClause = `SELECT ${groupExpr} AS group_key, ${aggregateExpr} AS value`\n groupByClause = `GROUP BY ${groupExpr}`\n orderByClause = `ORDER BY value DESC`\n\n if (options.groupBy.limit && options.groupBy.limit > 0) {\n limitClause = `LIMIT ${Math.min(options.groupBy.limit, 100)}`\n }\n }\n }\n\n const whereClauses: string[] = []\n\n whereClauses.push(`tenant_id = ?`)\n params.push(options.scope.tenantId)\n\n if (options.scope.organizationIds && options.scope.organizationIds.length > 0) {\n whereClauses.push(`organization_id = ANY(?::uuid[])`)\n params.push(`{${options.scope.organizationIds.join(',')}}`)\n }\n\n whereClauses.push(`deleted_at IS NULL`)\n\n if (options.dateRange) {\n const dateMapping = registry.getFieldMapping(options.entityType, options.dateRange.field)\n if (dateMapping) {\n whereClauses.push(`${dateMapping.dbColumn} >= ?`)\n params.push(options.dateRange.start)\n whereClauses.push(`${dateMapping.dbColumn} <= ?`)\n params.push(options.dateRange.end)\n }\n }\n\n if (options.filters) {\n for (const filter of options.filters) {\n const filterMapping = registry.getFieldMapping(options.entityType, filter.field)\n if (!filterMapping) continue\n\n switch (filter.operator) {\n case 'eq':\n whereClauses.push(`${filterMapping.dbColumn} = ?`)\n params.push(filter.value)\n break\n case 'neq':\n whereClauses.push(`${filterMapping.dbColumn} != ?`)\n params.push(filter.value)\n break\n case 'gt':\n whereClauses.push(`${filterMapping.dbColumn} > ?`)\n params.push(filter.value)\n break\n case 'gte':\n whereClauses.push(`${filterMapping.dbColumn} >= ?`)\n params.push(filter.value)\n break\n case 'lt':\n whereClauses.push(`${filterMapping.dbColumn} < ?`)\n params.push(filter.value)\n break\n case 'lte':\n whereClauses.push(`${filterMapping.dbColumn} <= ?`)\n params.push(filter.value)\n break\n case 'in':\n whereClauses.push(`${filterMapping.dbColumn} = ANY(?)`)\n params.push(filter.value)\n break\n case 'not_in':\n whereClauses.push(`${filterMapping.dbColumn} != ALL(?)`)\n params.push(filter.value)\n break\n case 'is_null':\n whereClauses.push(`${filterMapping.dbColumn} IS NULL`)\n break\n case 'is_not_null':\n whereClauses.push(`${filterMapping.dbColumn} IS NOT NULL`)\n break\n }\n }\n }\n\n const whereClause = whereClauses.length > 0 ? `WHERE ${whereClauses.join(' AND ')}` : ''\n\n const sql = [selectClause, `FROM ${tableName}`, whereClause, groupByClause, orderByClause, limitClause]\n .filter(Boolean)\n .join(' ')\n\n return { sql, params }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { AnalyticsRegistry } from '../services/analyticsRegistry'\nimport type { AnalyticsEntityTypeConfig, AnalyticsFieldMapping } from '@open-mercato/shared/modules/analytics'\n\nexport type AggregateFunction = 'count' | 'sum' | 'avg' | 'min' | 'max'\nexport type DateGranularity = 'day' | 'week' | 'month' | 'quarter' | 'year'\n\nconst VALID_GRANULARITIES: readonly DateGranularity[] = ['day', 'week', 'month', 'quarter', 'year']\nconst VALID_AGGREGATES: readonly AggregateFunction[] = ['count', 'sum', 'avg', 'min', 'max']\nconst SAFE_IDENTIFIER_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/\n\nexport function isValidGranularity(value: unknown): value is DateGranularity {\n return typeof value === 'string' && VALID_GRANULARITIES.includes(value as DateGranularity)\n}\n\nexport function isValidAggregate(value: unknown): value is AggregateFunction {\n return typeof value === 'string' && VALID_AGGREGATES.includes(value as AggregateFunction)\n}\n\nfunction isSafeIdentifier(value: string): boolean {\n return SAFE_IDENTIFIER_PATTERN.test(value)\n}\n\n// Re-export types from shared module for convenience\nexport type EntityTypeConfig = AnalyticsEntityTypeConfig\nexport type FieldMapping = AnalyticsFieldMapping\n\nexport function buildAggregateExpression(aggregate: AggregateFunction, column: string): string {\n switch (aggregate) {\n case 'count':\n return column === 'id' ? 'COUNT(*)' : `COUNT(${column})`\n case 'sum':\n return `COALESCE(SUM(${column}::numeric), 0)`\n case 'avg':\n return `COALESCE(AVG(${column}::numeric), 0)`\n case 'min':\n return `MIN(${column}::numeric)`\n case 'max':\n return `MAX(${column}::numeric)`\n default:\n return `COUNT(*)`\n }\n}\n\nexport function buildDateTruncExpression(column: string, granularity: DateGranularity): string {\n if (!isValidGranularity(granularity)) {\n throw new Error(`Invalid granularity: ${granularity}`)\n }\n return `DATE_TRUNC('${granularity}', ${column})`\n}\n\nexport function buildJsonbFieldExpression(column: string, path: string): string {\n const parts = path.split('.')\n for (const part of parts) {\n if (!isSafeIdentifier(part)) {\n throw new Error(`Invalid JSONB path part: ${part}`)\n }\n }\n if (parts.length === 1) {\n return `${column}->>'${parts[0]}'`\n }\n const intermediate = parts.slice(0, -1).map((p) => `'${p}'`).join('->')\n const lastPart = parts[parts.length - 1]\n return `${column}->${intermediate}->>'${lastPart}'`\n}\n\nexport type AggregationQuery = {\n sql: string\n params: unknown[]\n}\n\nexport type BuildAggregationQueryOptions = {\n entityType: string\n metric: {\n field: string\n aggregate: AggregateFunction\n }\n groupBy?: {\n field: string\n granularity?: DateGranularity\n limit?: number\n }\n dateRange?: {\n field: string\n start: Date\n end: Date\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 scope: {\n tenantId: string\n organizationIds?: string[]\n }\n /** Analytics registry for resolving entity and field configurations */\n registry: AnalyticsRegistry\n}\n\nexport type ResolvedGroupExpression = {\n expression: string\n /** Column the group expression reads from \u2014 the encryption map is keyed by this. */\n dbColumn: string\n /** JSONB path below `dbColumn`, when the groupBy field used path notation. */\n jsonPath: string | null\n}\n\n/**\n * Resolves the SQL expression a groupBy field maps to, together with the underlying column so\n * callers can decide whether the source is encrypted at rest before grouping over it (#4622).\n */\nexport function resolveGroupExpression(\n registry: AnalyticsRegistry,\n entityType: string,\n groupBy: { field: string; granularity?: DateGranularity },\n): ResolvedGroupExpression | null {\n const groupMapping = registry.getFieldMapping(entityType, groupBy.field)\n\n // Handle JSONB path notation (e.g., shippingAddressSnapshot.region)\n if (!groupMapping && groupBy.field.includes('.')) {\n const [baseField, ...pathParts] = groupBy.field.split('.')\n const baseMapping = registry.getFieldMapping(entityType, baseField)\n if (baseMapping?.type !== 'jsonb') return null\n const jsonPath = pathParts.join('.')\n return {\n expression: buildJsonbFieldExpression(baseMapping.dbColumn, jsonPath),\n dbColumn: baseMapping.dbColumn,\n jsonPath,\n }\n }\n\n if (!groupMapping) return null\n\n if (groupMapping.type === 'timestamp' && groupBy.granularity) {\n return {\n expression: buildDateTruncExpression(groupMapping.dbColumn, groupBy.granularity),\n dbColumn: groupMapping.dbColumn,\n jsonPath: null,\n }\n }\n\n return { expression: groupMapping.dbColumn, dbColumn: groupMapping.dbColumn, jsonPath: null }\n}\n\nfunction buildWhereClause(\n options: Pick<BuildAggregationQueryOptions, 'entityType' | 'dateRange' | 'filters' | 'scope' | 'registry'>,\n): { clause: string; params: unknown[] } {\n const { registry } = options\n const params: unknown[] = []\n const whereClauses: string[] = []\n\n whereClauses.push(`tenant_id = ?`)\n params.push(options.scope.tenantId)\n\n if (options.scope.organizationIds && options.scope.organizationIds.length > 0) {\n whereClauses.push(`organization_id = ANY(?::uuid[])`)\n params.push(`{${options.scope.organizationIds.join(',')}}`)\n }\n\n whereClauses.push(`deleted_at IS NULL`)\n\n if (options.dateRange) {\n const dateMapping = registry.getFieldMapping(options.entityType, options.dateRange.field)\n if (dateMapping) {\n whereClauses.push(`${dateMapping.dbColumn} >= ?`)\n params.push(options.dateRange.start)\n whereClauses.push(`${dateMapping.dbColumn} <= ?`)\n params.push(options.dateRange.end)\n }\n }\n\n if (options.filters) {\n for (const filter of options.filters) {\n const filterMapping = registry.getFieldMapping(options.entityType, filter.field)\n if (!filterMapping) continue\n\n switch (filter.operator) {\n case 'eq':\n whereClauses.push(`${filterMapping.dbColumn} = ?`)\n params.push(filter.value)\n break\n case 'neq':\n whereClauses.push(`${filterMapping.dbColumn} != ?`)\n params.push(filter.value)\n break\n case 'gt':\n whereClauses.push(`${filterMapping.dbColumn} > ?`)\n params.push(filter.value)\n break\n case 'gte':\n whereClauses.push(`${filterMapping.dbColumn} >= ?`)\n params.push(filter.value)\n break\n case 'lt':\n whereClauses.push(`${filterMapping.dbColumn} < ?`)\n params.push(filter.value)\n break\n case 'lte':\n whereClauses.push(`${filterMapping.dbColumn} <= ?`)\n params.push(filter.value)\n break\n case 'in':\n whereClauses.push(`${filterMapping.dbColumn} = ANY(?)`)\n params.push(filter.value)\n break\n case 'not_in':\n whereClauses.push(`${filterMapping.dbColumn} != ALL(?)`)\n params.push(filter.value)\n break\n case 'is_null':\n whereClauses.push(`${filterMapping.dbColumn} IS NULL`)\n break\n case 'is_not_null':\n whereClauses.push(`${filterMapping.dbColumn} IS NOT NULL`)\n break\n }\n }\n }\n\n return { clause: whereClauses.length > 0 ? `WHERE ${whereClauses.join(' AND ')}` : '', params }\n}\n\nfunction resolveTableName(config: EntityTypeConfig): string {\n return config.schema ? `\"${config.schema}\".\"${config.tableName}\"` : `\"${config.tableName}\"`\n}\n\nexport function buildAggregationQuery(options: BuildAggregationQueryOptions): AggregationQuery | null {\n const { registry } = options\n const config = registry.getEntityTypeConfig(options.entityType)\n if (!config) return null\n\n const metricMapping = registry.getFieldMapping(options.entityType, options.metric.field)\n if (!metricMapping) return null\n\n const tableName = resolveTableName(config)\n const aggregateExpr = buildAggregateExpression(options.metric.aggregate, metricMapping.dbColumn)\n\n let selectClause = `SELECT ${aggregateExpr} AS value`\n let groupByClause = ''\n let orderByClause = ''\n let limitClause = ''\n\n if (options.groupBy) {\n const resolved = resolveGroupExpression(registry, options.entityType, options.groupBy)\n\n if (resolved) {\n selectClause = `SELECT ${resolved.expression} AS group_key, ${aggregateExpr} AS value`\n groupByClause = `GROUP BY ${resolved.expression}`\n // NULLS LAST is stated explicitly (PostgreSQL defaults DESC to NULLS FIRST) so a group limit\n // keeps the highest-value buckets instead of the empty ones, and so the application-side\n // encrypted path can mirror the same ordering (#4622).\n orderByClause = `ORDER BY value DESC NULLS LAST`\n\n if (options.groupBy.limit && options.groupBy.limit > 0) {\n limitClause = `LIMIT ${Math.min(options.groupBy.limit, 100)}`\n }\n }\n }\n\n const where = buildWhereClause(options)\n\n const sql = [selectClause, `FROM ${tableName}`, where.clause, groupByClause, orderByClause, limitClause]\n .filter(Boolean)\n .join(' ')\n\n return { sql, params: where.params }\n}\n\nexport type BuildGroupSourceRowsQueryOptions = Omit<BuildAggregationQueryOptions, 'groupBy'> & {\n /** Column holding the (encrypted) group source, resolved via `resolveGroupExpression`. */\n groupColumn: string\n /** Hard cap on scanned rows; callers fetch `rowLimit + 1` to detect overflow. */\n rowLimit: number\n}\n\n/**\n * Builds a per-row query for group sources that cannot be grouped in SQL because the column is\n * encrypted at rest. Returns the raw group source alongside the metric value so the caller can\n * decrypt, then aggregate in application code (#4622).\n */\nexport function buildGroupSourceRowsQuery(options: BuildGroupSourceRowsQueryOptions): AggregationQuery | null {\n const { registry } = options\n const config = registry.getEntityTypeConfig(options.entityType)\n if (!config) return null\n\n const metricMapping = registry.getFieldMapping(options.entityType, options.metric.field)\n if (!metricMapping) return null\n\n if (!isSafeIdentifier(options.groupColumn)) {\n throw new Error(`Invalid group column: ${options.groupColumn}`)\n }\n\n const where = buildWhereClause(options)\n const sql = [\n `SELECT ${options.groupColumn} AS group_source, ${metricMapping.dbColumn} AS metric_value`,\n `FROM ${resolveTableName(config)}`,\n where.clause,\n `LIMIT ${Math.max(1, Math.floor(options.rowLimit)) + 1}`,\n ]\n .filter(Boolean)\n .join(' ')\n\n return { sql, params: where.params }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,69 @@
1
+ const DECIMAL_PATTERN = /^([+-]?)(\d*)(?:\.(\d*))?(?:[eE]([+-]?\d+))?$/;
2
+ const EXACT_DECIMAL_ZERO = { units: 0n, scale: 0 };
3
+ function pow10(exponent) {
4
+ return 10n ** BigInt(exponent);
5
+ }
6
+ function parseExactDecimal(raw) {
7
+ if (raw === null || raw === void 0) return null;
8
+ if (typeof raw === "bigint") return { units: raw, scale: 0 };
9
+ let text;
10
+ if (typeof raw === "number") {
11
+ if (!Number.isFinite(raw)) return null;
12
+ text = String(raw);
13
+ } else if (typeof raw === "string") {
14
+ text = raw.trim();
15
+ } else {
16
+ return null;
17
+ }
18
+ if (!text) return null;
19
+ const match = DECIMAL_PATTERN.exec(text);
20
+ if (!match) return null;
21
+ const [, sign, intPart = "", fracPart = "", exponentPart] = match;
22
+ if (!intPart && !fracPart) return null;
23
+ const digits = `${intPart}${fracPart}`;
24
+ let units = BigInt(digits);
25
+ let scale = fracPart.length;
26
+ if (exponentPart) {
27
+ scale -= Number(exponentPart);
28
+ if (scale < 0) {
29
+ units *= pow10(-scale);
30
+ scale = 0;
31
+ }
32
+ }
33
+ if (sign === "-") units = -units;
34
+ return { units, scale };
35
+ }
36
+ function rescale(value, scale) {
37
+ return scale === value.scale ? value.units : value.units * pow10(scale - value.scale);
38
+ }
39
+ function addExactDecimal(left, right) {
40
+ const scale = Math.max(left.scale, right.scale);
41
+ return { units: rescale(left, scale) + rescale(right, scale), scale };
42
+ }
43
+ function compareExactDecimal(left, right) {
44
+ const scale = Math.max(left.scale, right.scale);
45
+ const leftUnits = rescale(left, scale);
46
+ const rightUnits = rescale(right, scale);
47
+ if (leftUnits === rightUnits) return 0;
48
+ return leftUnits < rightUnits ? -1 : 1;
49
+ }
50
+ function exactDecimalToString(value) {
51
+ const negative = value.units < 0n;
52
+ const digits = (negative ? -value.units : value.units).toString().padStart(value.scale + 1, "0");
53
+ const boundary = digits.length - value.scale;
54
+ const integerPart = digits.slice(0, boundary);
55
+ const fractionPart = value.scale > 0 ? digits.slice(boundary) : "";
56
+ return `${negative ? "-" : ""}${integerPart}${fractionPart ? `.${fractionPart}` : ""}`;
57
+ }
58
+ function exactDecimalToNumber(value) {
59
+ return Number(exactDecimalToString(value));
60
+ }
61
+ export {
62
+ EXACT_DECIMAL_ZERO,
63
+ addExactDecimal,
64
+ compareExactDecimal,
65
+ exactDecimalToNumber,
66
+ exactDecimalToString,
67
+ parseExactDecimal
68
+ };
69
+ //# sourceMappingURL=exactDecimal.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/dashboards/lib/exactDecimal.ts"],
4
+ "sourcesContent": ["/**\n * Minimal exact decimal arithmetic for application-side aggregation of PostgreSQL `numeric`\n * columns. The SQL path sums money in `numeric` and converts once, so the application path must\n * not accumulate in binary floating point \u2014 `0.1 + 0.2` would drift the totals of encrypted group\n * sources away from the plaintext ones (#4622).\n *\n * A value is stored as scaled integer `units` plus a decimal `scale`, so `12.34` is\n * `{ units: 1234n, scale: 2 }`. Conversion to `number` happens only at the response boundary.\n */\n\nexport type ExactDecimal = {\n units: bigint\n scale: number\n}\n\nconst DECIMAL_PATTERN = /^([+-]?)(\\d*)(?:\\.(\\d*))?(?:[eE]([+-]?\\d+))?$/\n\nexport const EXACT_DECIMAL_ZERO: ExactDecimal = { units: 0n, scale: 0 }\n\nfunction pow10(exponent: number): bigint {\n return 10n ** BigInt(exponent)\n}\n\n/**\n * Parses a `numeric` column value. The PostgreSQL driver hands these over as strings to preserve\n * precision, so the string form is authoritative; numbers and bigints are accepted for callers that\n * already hold a JavaScript value. Returns `null` for anything that is not a finite decimal.\n */\nexport function parseExactDecimal(raw: unknown): ExactDecimal | null {\n if (raw === null || raw === undefined) return null\n if (typeof raw === 'bigint') return { units: raw, scale: 0 }\n\n let text: string\n if (typeof raw === 'number') {\n if (!Number.isFinite(raw)) return null\n text = String(raw)\n } else if (typeof raw === 'string') {\n text = raw.trim()\n } else {\n return null\n }\n\n if (!text) return null\n\n const match = DECIMAL_PATTERN.exec(text)\n if (!match) return null\n\n const [, sign, intPart = '', fracPart = '', exponentPart] = match\n if (!intPart && !fracPart) return null\n\n const digits = `${intPart}${fracPart}`\n let units = BigInt(digits)\n let scale = fracPart.length\n\n if (exponentPart) {\n scale -= Number(exponentPart)\n if (scale < 0) {\n units *= pow10(-scale)\n scale = 0\n }\n }\n\n if (sign === '-') units = -units\n return { units, scale }\n}\n\nfunction rescale(value: ExactDecimal, scale: number): bigint {\n return scale === value.scale ? value.units : value.units * pow10(scale - value.scale)\n}\n\nexport function addExactDecimal(left: ExactDecimal, right: ExactDecimal): ExactDecimal {\n const scale = Math.max(left.scale, right.scale)\n return { units: rescale(left, scale) + rescale(right, scale), scale }\n}\n\nexport function compareExactDecimal(left: ExactDecimal, right: ExactDecimal): number {\n const scale = Math.max(left.scale, right.scale)\n const leftUnits = rescale(left, scale)\n const rightUnits = rescale(right, scale)\n if (leftUnits === rightUnits) return 0\n return leftUnits < rightUnits ? -1 : 1\n}\n\nexport function exactDecimalToString(value: ExactDecimal): string {\n const negative = value.units < 0n\n const digits = (negative ? -value.units : value.units).toString().padStart(value.scale + 1, '0')\n const boundary = digits.length - value.scale\n const integerPart = digits.slice(0, boundary)\n const fractionPart = value.scale > 0 ? digits.slice(boundary) : ''\n return `${negative ? '-' : ''}${integerPart}${fractionPart ? `.${fractionPart}` : ''}`\n}\n\n/** Response-boundary conversion: the only place where the exact value becomes a binary float. */\nexport function exactDecimalToNumber(value: ExactDecimal): number {\n return Number(exactDecimalToString(value))\n}\n"],
5
+ "mappings": "AAeA,MAAM,kBAAkB;AAEjB,MAAM,qBAAmC,EAAE,OAAO,IAAI,OAAO,EAAE;AAEtE,SAAS,MAAM,UAA0B;AACvC,SAAO,OAAO,OAAO,QAAQ;AAC/B;AAOO,SAAS,kBAAkB,KAAmC;AACnE,MAAI,QAAQ,QAAQ,QAAQ,OAAW,QAAO;AAC9C,MAAI,OAAO,QAAQ,SAAU,QAAO,EAAE,OAAO,KAAK,OAAO,EAAE;AAE3D,MAAI;AACJ,MAAI,OAAO,QAAQ,UAAU;AAC3B,QAAI,CAAC,OAAO,SAAS,GAAG,EAAG,QAAO;AAClC,WAAO,OAAO,GAAG;AAAA,EACnB,WAAW,OAAO,QAAQ,UAAU;AAClC,WAAO,IAAI,KAAK;AAAA,EAClB,OAAO;AACL,WAAO;AAAA,EACT;AAEA,MAAI,CAAC,KAAM,QAAO;AAElB,QAAM,QAAQ,gBAAgB,KAAK,IAAI;AACvC,MAAI,CAAC,MAAO,QAAO;AAEnB,QAAM,CAAC,EAAE,MAAM,UAAU,IAAI,WAAW,IAAI,YAAY,IAAI;AAC5D,MAAI,CAAC,WAAW,CAAC,SAAU,QAAO;AAElC,QAAM,SAAS,GAAG,OAAO,GAAG,QAAQ;AACpC,MAAI,QAAQ,OAAO,MAAM;AACzB,MAAI,QAAQ,SAAS;AAErB,MAAI,cAAc;AAChB,aAAS,OAAO,YAAY;AAC5B,QAAI,QAAQ,GAAG;AACb,eAAS,MAAM,CAAC,KAAK;AACrB,cAAQ;AAAA,IACV;AAAA,EACF;AAEA,MAAI,SAAS,IAAK,SAAQ,CAAC;AAC3B,SAAO,EAAE,OAAO,MAAM;AACxB;AAEA,SAAS,QAAQ,OAAqB,OAAuB;AAC3D,SAAO,UAAU,MAAM,QAAQ,MAAM,QAAQ,MAAM,QAAQ,MAAM,QAAQ,MAAM,KAAK;AACtF;AAEO,SAAS,gBAAgB,MAAoB,OAAmC;AACrF,QAAM,QAAQ,KAAK,IAAI,KAAK,OAAO,MAAM,KAAK;AAC9C,SAAO,EAAE,OAAO,QAAQ,MAAM,KAAK,IAAI,QAAQ,OAAO,KAAK,GAAG,MAAM;AACtE;AAEO,SAAS,oBAAoB,MAAoB,OAA6B;AACnF,QAAM,QAAQ,KAAK,IAAI,KAAK,OAAO,MAAM,KAAK;AAC9C,QAAM,YAAY,QAAQ,MAAM,KAAK;AACrC,QAAM,aAAa,QAAQ,OAAO,KAAK;AACvC,MAAI,cAAc,WAAY,QAAO;AACrC,SAAO,YAAY,aAAa,KAAK;AACvC;AAEO,SAAS,qBAAqB,OAA6B;AAChE,QAAM,WAAW,MAAM,QAAQ;AAC/B,QAAM,UAAU,WAAW,CAAC,MAAM,QAAQ,MAAM,OAAO,SAAS,EAAE,SAAS,MAAM,QAAQ,GAAG,GAAG;AAC/F,QAAM,WAAW,OAAO,SAAS,MAAM;AACvC,QAAM,cAAc,OAAO,MAAM,GAAG,QAAQ;AAC5C,QAAM,eAAe,MAAM,QAAQ,IAAI,OAAO,MAAM,QAAQ,IAAI;AAChE,SAAO,GAAG,WAAW,MAAM,EAAE,GAAG,WAAW,GAAG,eAAe,IAAI,YAAY,KAAK,EAAE;AACtF;AAGO,SAAS,qBAAqB,OAA6B;AAChE,SAAO,OAAO,qBAAqB,KAAK,CAAC;AAC3C;",
6
+ "names": []
7
+ }