@open-mercato/core 0.6.4-develop.4113.1.5e87922616 → 0.6.4-develop.4133.1.48fc6c8f7b

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 (53) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/auth/lib/sessionIntegrity.js +16 -13
  3. package/dist/modules/auth/lib/sessionIntegrity.js.map +2 -2
  4. package/dist/modules/customers/api/utils.js +14 -9
  5. package/dist/modules/customers/api/utils.js.map +2 -2
  6. package/dist/modules/dashboards/api/widgets/data/batch/route.js +137 -0
  7. package/dist/modules/dashboards/api/widgets/data/batch/route.js.map +7 -0
  8. package/dist/modules/dashboards/api/widgets/data/route.js +1 -75
  9. package/dist/modules/dashboards/api/widgets/data/route.js.map +2 -2
  10. package/dist/modules/dashboards/api/widgets/data/schema.js +85 -0
  11. package/dist/modules/dashboards/api/widgets/data/schema.js.map +7 -0
  12. package/dist/modules/dashboards/lib/widgetDataBatch.js +49 -0
  13. package/dist/modules/dashboards/lib/widgetDataBatch.js.map +7 -0
  14. package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.js +6 -14
  15. package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.js.map +2 -2
  16. package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.js +6 -14
  17. package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.js.map +2 -2
  18. package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.js +6 -14
  19. package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.js.map +2 -2
  20. package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.js +6 -14
  21. package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.js.map +2 -2
  22. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js +6 -14
  23. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js.map +2 -2
  24. package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.js +6 -14
  25. package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.js.map +2 -2
  26. package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.js +6 -14
  27. package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.js.map +2 -2
  28. package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.js +6 -14
  29. package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.js.map +2 -2
  30. package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.client.js +6 -14
  31. package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.client.js.map +2 -2
  32. package/dist/modules/dashboards/widgets/dashboard/top-products/widget.client.js +6 -14
  33. package/dist/modules/dashboards/widgets/dashboard/top-products/widget.client.js.map +2 -2
  34. package/dist/modules/directory/utils/organizationScope.js +33 -20
  35. package/dist/modules/directory/utils/organizationScope.js.map +2 -2
  36. package/package.json +7 -7
  37. package/src/modules/auth/lib/sessionIntegrity.ts +37 -16
  38. package/src/modules/customers/api/utils.ts +17 -11
  39. package/src/modules/dashboards/api/widgets/data/batch/route.ts +168 -0
  40. package/src/modules/dashboards/api/widgets/data/route.ts +1 -90
  41. package/src/modules/dashboards/api/widgets/data/schema.ts +90 -0
  42. package/src/modules/dashboards/lib/widgetDataBatch.ts +89 -0
  43. package/src/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.tsx +6 -16
  44. package/src/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.tsx +6 -16
  45. package/src/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.tsx +6 -16
  46. package/src/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.tsx +6 -16
  47. package/src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.tsx +6 -16
  48. package/src/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.tsx +6 -16
  49. package/src/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.tsx +6 -16
  50. package/src/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.tsx +6 -16
  51. package/src/modules/dashboards/widgets/dashboard/top-customers/widget.client.tsx +6 -16
  52. package/src/modules/dashboards/widgets/dashboard/top-products/widget.client.tsx +6 -16
  53. package/src/modules/directory/utils/organizationScope.ts +51 -20
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/dashboards/widgets/dashboard/top-products/widget.client.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { DashboardWidgetComponentProps } from '@open-mercato/shared/modules/dashboard/widgets'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { BarChart, type BarChartDataItem } from '@open-mercato/ui/backend/charts'\nimport { DateRangeSelect, InlineDateRangeSelect, type DateRangePreset } from '@open-mercato/ui/backend/date-range'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport { DEFAULT_SETTINGS, hydrateSettings, type TopProductsSettings } from './config'\nimport type { WidgetDataResponse } from '../../../services/widgetDataService'\nimport { formatCurrencyCompact } from '../../../lib/formatters'\n\nasync function fetchTopProductsData(settings: TopProductsSettings): Promise<WidgetDataResponse> {\n const body = {\n entityType: 'sales:order_lines',\n metric: {\n field: 'totalGrossAmount',\n aggregate: 'sum',\n },\n groupBy: {\n field: 'productId',\n limit: settings.limit,\n resolveLabels: true,\n },\n dateRange: {\n field: 'createdAt',\n preset: settings.dateRange,\n },\n }\n\n const call = await apiCall<WidgetDataResponse>('/api/dashboards/widgets/data', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n })\n\n if (!call.ok) {\n const errorMsg = (call.result as Record<string, unknown>)?.error\n throw new Error(typeof errorMsg === 'string' ? errorMsg : 'Failed to fetch top products data')\n }\n\n return call.result as WidgetDataResponse\n}\n\nfunction truncateLabel(\n label: unknown,\n t: (key: string, fallback: string) => string,\n maxLength: number = 20\n): string {\n if (label == null || label === '') return t('dashboards.analytics.labels.unknownProduct', 'Unknown Product')\n const labelStr = String(label)\n // Check for UUID-like strings or meaningless values\n if (labelStr === '0' || labelStr === 'null' || labelStr === 'undefined') {\n return t('dashboards.analytics.labels.unknownProduct', 'Unknown Product')\n }\n if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(labelStr)) {\n return t('dashboards.analytics.labels.unnamedProduct', 'Unnamed Product')\n }\n if (labelStr.length <= maxLength) return labelStr\n return labelStr.slice(0, maxLength - 3) + '...'\n}\n\nconst TopProductsWidget: React.FC<DashboardWidgetComponentProps<TopProductsSettings>> = ({\n mode,\n settings = DEFAULT_SETTINGS,\n onSettingsChange,\n refreshToken,\n onRefreshStateChange,\n}) => {\n const t = useT()\n const hydrated = React.useMemo(() => hydrateSettings(settings), [settings])\n const [data, setData] = React.useState<BarChartDataItem[]>([])\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const fetchingRef = React.useRef(false)\n\n const refresh = React.useCallback(async () => {\n if (fetchingRef.current) return\n fetchingRef.current = true\n onRefreshStateChange?.(true)\n setLoading(true)\n setError(null)\n try {\n const result = await fetchTopProductsData(hydrated)\n const chartData = result.data.map((item, index) => ({\n name: truncateLabel(item.groupLabel ?? item.groupKey ?? `Product ${index + 1}`, t),\n Revenue: item.value ?? 0,\n }))\n setData(chartData)\n } catch (err) {\n console.error('Failed to load top products data', err)\n setError(t('dashboards.analytics.widgets.topProducts.error', 'Failed to load data'))\n } finally {\n setLoading(false)\n onRefreshStateChange?.(false)\n fetchingRef.current = false\n }\n }, [hydrated, onRefreshStateChange, t])\n\n React.useEffect(() => {\n refresh().catch(() => {})\n }, [refresh, refreshToken])\n\n if (mode === 'settings') {\n return (\n <div className=\"space-y-4 text-sm\">\n <DateRangeSelect\n id=\"top-products-date-range\"\n label={t('dashboards.analytics.settings.dateRange', 'Date Range')}\n value={hydrated.dateRange}\n onChange={(dateRange: DateRangePreset) => onSettingsChange({ ...hydrated, dateRange })}\n />\n <div className=\"space-y-1.5\">\n <label\n htmlFor=\"top-products-limit\"\n className=\"text-xs font-semibold uppercase text-muted-foreground\"\n >\n {t('dashboards.analytics.settings.limit', 'Number of items')}\n </label>\n <Input\n id=\"top-products-limit\"\n type=\"number\"\n min={1}\n max={20}\n className=\"w-24\"\n value={hydrated.limit}\n onChange={(e) => {\n const next = Number(e.target.value)\n onSettingsChange({ ...hydrated, limit: Number.isFinite(next) ? next : hydrated.limit })\n }}\n />\n </div>\n <div className=\"space-y-1.5\">\n <label\n htmlFor=\"top-products-layout\"\n className=\"text-xs font-semibold uppercase text-muted-foreground\"\n >\n {t('dashboards.analytics.settings.chartLayout', 'Chart Layout')}\n </label>\n <Select\n value={hydrated.layout}\n onValueChange={(value) => onSettingsChange({ ...hydrated, layout: value as 'horizontal' | 'vertical' })}\n >\n <SelectTrigger id=\"top-products-layout\" size=\"sm\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"horizontal\">{t('dashboards.analytics.settings.horizontal', 'Horizontal')}</SelectItem>\n <SelectItem value=\"vertical\">{t('dashboards.analytics.settings.vertical', 'Vertical')}</SelectItem>\n </SelectContent>\n </Select>\n </div>\n </div>\n )\n }\n\n return (\n <div className=\"flex flex-col h-full\">\n <div className=\"flex justify-end mb-2\">\n <InlineDateRangeSelect\n value={hydrated.dateRange}\n onChange={(dateRange) => onSettingsChange({ ...hydrated, dateRange })}\n />\n </div>\n <div className=\"flex-1 min-h-0\">\n <BarChart\n data={data}\n index=\"name\"\n categories={['Revenue']}\n categoryLabels={{ Revenue: t('dashboards.analytics.widgets.topCustomers.column.revenue', 'Revenue') }}\n loading={loading}\n error={error}\n layout={hydrated.layout}\n valueFormatter={formatCurrencyCompact}\n colors={['emerald']}\n showLegend={false}\n emptyMessage={t('dashboards.analytics.widgets.topProducts.empty', 'No product sales data for this period')}\n />\n </div>\n </div>\n )\n}\n\nexport default TopProductsWidget\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { DashboardWidgetComponentProps } from '@open-mercato/shared/modules/dashboard/widgets'\nimport { useWidgetData, type WidgetDataFetcher } from '@open-mercato/ui/backend/dashboard/widgetData'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { BarChart, type BarChartDataItem } from '@open-mercato/ui/backend/charts'\nimport { DateRangeSelect, InlineDateRangeSelect, type DateRangePreset } from '@open-mercato/ui/backend/date-range'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport { DEFAULT_SETTINGS, hydrateSettings, type TopProductsSettings } from './config'\nimport type { WidgetDataResponse } from '../../../services/widgetDataService'\nimport { formatCurrencyCompact } from '../../../lib/formatters'\n\nasync function fetchTopProductsData(settings: TopProductsSettings, fetchWidgetData: WidgetDataFetcher): Promise<WidgetDataResponse> {\n const body = {\n entityType: 'sales:order_lines',\n metric: {\n field: 'totalGrossAmount',\n aggregate: 'sum',\n },\n groupBy: {\n field: 'productId',\n limit: settings.limit,\n resolveLabels: true,\n },\n dateRange: {\n field: 'createdAt',\n preset: settings.dateRange,\n },\n }\n\n return fetchWidgetData<WidgetDataResponse>(body)\n}\n\nfunction truncateLabel(\n label: unknown,\n t: (key: string, fallback: string) => string,\n maxLength: number = 20\n): string {\n if (label == null || label === '') return t('dashboards.analytics.labels.unknownProduct', 'Unknown Product')\n const labelStr = String(label)\n // Check for UUID-like strings or meaningless values\n if (labelStr === '0' || labelStr === 'null' || labelStr === 'undefined') {\n return t('dashboards.analytics.labels.unknownProduct', 'Unknown Product')\n }\n if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(labelStr)) {\n return t('dashboards.analytics.labels.unnamedProduct', 'Unnamed Product')\n }\n if (labelStr.length <= maxLength) return labelStr\n return labelStr.slice(0, maxLength - 3) + '...'\n}\n\nconst TopProductsWidget: React.FC<DashboardWidgetComponentProps<TopProductsSettings>> = ({\n mode,\n settings = DEFAULT_SETTINGS,\n onSettingsChange,\n refreshToken,\n onRefreshStateChange,\n}) => {\n const t = useT()\n const hydrated = React.useMemo(() => hydrateSettings(settings), [settings])\n const [data, setData] = React.useState<BarChartDataItem[]>([])\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const fetchingRef = React.useRef(false)\n\n const fetchWidgetData = useWidgetData()\n const refresh = React.useCallback(async () => {\n if (fetchingRef.current) return\n fetchingRef.current = true\n onRefreshStateChange?.(true)\n setLoading(true)\n setError(null)\n try {\n const result = await fetchTopProductsData(hydrated, fetchWidgetData)\n const chartData = result.data.map((item, index) => ({\n name: truncateLabel(item.groupLabel ?? item.groupKey ?? `Product ${index + 1}`, t),\n Revenue: item.value ?? 0,\n }))\n setData(chartData)\n } catch (err) {\n console.error('Failed to load top products data', err)\n setError(t('dashboards.analytics.widgets.topProducts.error', 'Failed to load data'))\n } finally {\n setLoading(false)\n onRefreshStateChange?.(false)\n fetchingRef.current = false\n }\n }, [hydrated, fetchWidgetData, onRefreshStateChange, t])\n\n React.useEffect(() => {\n refresh().catch(() => {})\n }, [refresh, refreshToken])\n\n if (mode === 'settings') {\n return (\n <div className=\"space-y-4 text-sm\">\n <DateRangeSelect\n id=\"top-products-date-range\"\n label={t('dashboards.analytics.settings.dateRange', 'Date Range')}\n value={hydrated.dateRange}\n onChange={(dateRange: DateRangePreset) => onSettingsChange({ ...hydrated, dateRange })}\n />\n <div className=\"space-y-1.5\">\n <label\n htmlFor=\"top-products-limit\"\n className=\"text-xs font-semibold uppercase text-muted-foreground\"\n >\n {t('dashboards.analytics.settings.limit', 'Number of items')}\n </label>\n <Input\n id=\"top-products-limit\"\n type=\"number\"\n min={1}\n max={20}\n className=\"w-24\"\n value={hydrated.limit}\n onChange={(e) => {\n const next = Number(e.target.value)\n onSettingsChange({ ...hydrated, limit: Number.isFinite(next) ? next : hydrated.limit })\n }}\n />\n </div>\n <div className=\"space-y-1.5\">\n <label\n htmlFor=\"top-products-layout\"\n className=\"text-xs font-semibold uppercase text-muted-foreground\"\n >\n {t('dashboards.analytics.settings.chartLayout', 'Chart Layout')}\n </label>\n <Select\n value={hydrated.layout}\n onValueChange={(value) => onSettingsChange({ ...hydrated, layout: value as 'horizontal' | 'vertical' })}\n >\n <SelectTrigger id=\"top-products-layout\" size=\"sm\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"horizontal\">{t('dashboards.analytics.settings.horizontal', 'Horizontal')}</SelectItem>\n <SelectItem value=\"vertical\">{t('dashboards.analytics.settings.vertical', 'Vertical')}</SelectItem>\n </SelectContent>\n </Select>\n </div>\n </div>\n )\n }\n\n return (\n <div className=\"flex flex-col h-full\">\n <div className=\"flex justify-end mb-2\">\n <InlineDateRangeSelect\n value={hydrated.dateRange}\n onChange={(dateRange) => onSettingsChange({ ...hydrated, dateRange })}\n />\n </div>\n <div className=\"flex-1 min-h-0\">\n <BarChart\n data={data}\n index=\"name\"\n categories={['Revenue']}\n categoryLabels={{ Revenue: t('dashboards.analytics.widgets.topCustomers.column.revenue', 'Revenue') }}\n loading={loading}\n error={error}\n layout={hydrated.layout}\n valueFormatter={formatCurrencyCompact}\n colors={['emerald']}\n showLegend={false}\n emptyMessage={t('dashboards.analytics.widgets.topProducts.empty', 'No product sales data for this period')}\n />\n </div>\n </div>\n )\n}\n\nexport default TopProductsWidget\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -81,29 +81,43 @@ function getSelectedTenantFromRequest(req) {
81
81
  const header = typeof headerContainer?.get === "function" ? headerContainer.get("cookie") : null;
82
82
  return parseSelectedTenantCookie(header);
83
83
  }
84
- async function collectWithDescendants(em, tenantId, ids) {
85
- if (!ids.length) return /* @__PURE__ */ new Set();
86
- const unique = Array.from(new Set(
84
+ function normalizeOrganizationIds(ids) {
85
+ return Array.from(new Set(
87
86
  ids.map((value) => normalizeOrganizationId(value)).filter((value) => {
88
87
  if (!value) return false;
89
88
  if (isAllOrganizationsSelection(value)) return false;
90
89
  return true;
91
90
  })
92
91
  ));
93
- if (!unique.length) return /* @__PURE__ */ new Set();
92
+ }
93
+ async function loadOrgDescendantMap(em, tenantId, ids) {
94
+ const unique = normalizeOrganizationIds(ids);
95
+ if (!unique.length) return /* @__PURE__ */ new Map();
94
96
  const filter = {
95
97
  tenant: tenantId,
96
98
  id: { $in: unique },
97
99
  deletedAt: null
98
100
  };
99
101
  const orgs = await em.find(Organization, filter);
100
- const set = /* @__PURE__ */ new Set();
102
+ const map = /* @__PURE__ */ new Map();
101
103
  for (const org of orgs) {
102
104
  const id = String(org.id);
103
- set.add(id);
105
+ const expansion = [id];
104
106
  if (Array.isArray(org.descendantIds)) {
105
- for (const desc of org.descendantIds) set.add(String(desc));
107
+ for (const desc of org.descendantIds) expansion.push(String(desc));
106
108
  }
109
+ map.set(id, expansion);
110
+ }
111
+ return map;
112
+ }
113
+ function expandWithDescendants(map, ids) {
114
+ const set = /* @__PURE__ */ new Set();
115
+ for (const value of ids) {
116
+ const id = normalizeOrganizationId(value);
117
+ if (!id || isAllOrganizationsSelection(id)) continue;
118
+ const expansion = map.get(id);
119
+ if (!expansion) continue;
120
+ for (const entry of expansion) set.add(entry);
107
121
  }
108
122
  return set;
109
123
  }
@@ -139,24 +153,23 @@ async function resolveOrganizationScope({
139
153
  const accessibleList = effectiveSuperAdmin ? null : normalizedAccessible && normalizedAccessible.some((value) => isAllOrganizationsSelection(value)) ? null : normalizedAccessible?.filter((value) => !isAllOrganizationsSelection(value)) ?? null;
140
154
  const accountOrgId = actorTenantId && actorTenantId === tenantId ? normalizeOrganizationId(auth.orgId) : null;
141
155
  const fallbackOrgId = accountOrgId ?? null;
142
- let fallbackSet = null;
143
- const loadFallbackSet = async () => {
144
- if (!fallbackOrgId) return null;
145
- if (!fallbackSet) {
146
- fallbackSet = await collectWithDescendants(em, tenantId, [fallbackOrgId]);
147
- }
148
- return fallbackSet;
149
- };
156
+ const candidateIds = [
157
+ ...accessibleList ?? [],
158
+ ...fallbackOrgId ? [fallbackOrgId] : [],
159
+ ...normalizedSelectedId ? [normalizedSelectedId] : []
160
+ ];
161
+ const orgDescendants = await loadOrgDescendantMap(em, tenantId, candidateIds);
162
+ const loadFallbackSet = () => fallbackOrgId ? expandWithDescendants(orgDescendants, [fallbackOrgId]) : null;
150
163
  let allowedSet = null;
151
164
  if (accessibleList === null) {
152
165
  allowedSet = null;
153
166
  } else if (accessibleList.length === 0) {
154
167
  allowedSet = /* @__PURE__ */ new Set();
155
168
  } else {
156
- allowedSet = await collectWithDescendants(em, tenantId, accessibleList);
169
+ allowedSet = expandWithDescendants(orgDescendants, accessibleList);
157
170
  }
158
171
  if (allowedSet && allowedSet.size === 0 && fallbackOrgId) {
159
- const computed = await loadFallbackSet();
172
+ const computed = loadFallbackSet();
160
173
  if (computed && computed.size > 0) {
161
174
  allowedSet = computed;
162
175
  }
@@ -173,16 +186,16 @@ async function resolveOrganizationScope({
173
186
  }
174
187
  let filterSet = null;
175
188
  if (effectiveSelected) {
176
- filterSet = await collectWithDescendants(em, tenantId, [effectiveSelected]);
189
+ filterSet = expandWithDescendants(orgDescendants, [effectiveSelected]);
177
190
  } else if (allowedSet !== null) {
178
191
  filterSet = allowedSet;
179
192
  } else if (widenToAllOrgs) {
180
193
  filterSet = null;
181
194
  } else if (auth.orgId) {
182
- filterSet = await loadFallbackSet();
195
+ filterSet = loadFallbackSet();
183
196
  }
184
197
  if ((!filterSet || filterSet.size === 0) && fallbackOrgId && !widenToAllOrgs) {
185
- const computed = await loadFallbackSet();
198
+ const computed = loadFallbackSet();
186
199
  if (computed && computed.size > 0) {
187
200
  filterSet = computed;
188
201
  if (!effectiveSelected) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/directory/utils/organizationScope.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { FilterQuery } from '@mikro-orm/core'\nimport type { AwilixContainer } from 'awilix'\nimport { Organization } from '@open-mercato/core/modules/directory/data/entities'\nimport { isAllOrganizationsSelection } from '@open-mercato/core/modules/directory/constants'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport type { AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport { parseSelectedOrganizationCookie, parseSelectedTenantCookie } from './scopeCookies'\n\nexport { parseSelectedOrganizationCookie, parseSelectedTenantCookie }\n\nexport type OrganizationScope = {\n selectedId: string | null\n filterIds: string[] | null\n allowedIds: string[] | null\n tenantId: string | null\n}\n\n// Phase 4 \u2014 short-TTL cache for resolveOrganizationScopeForRequest.\n// OrganizationScope is a pure function of (userId, tenantId, selectedOrgId,\n// requestedTenant) between membership changes; caching it bypasses 1\n// SELECT on `organizations` per CRUD request. TTL is short (60s default)\n// to keep staleness bounded for membership/visibility changes. Tag-based\n// invalidation kicks the cache when user_organizations or organizations\n// mutate (wired via invalidateOrganizationScopeCacheFor).\nconst ORG_SCOPE_CACHE_KEY_PREFIX = 'org-scope'\n// Phase 4 default-off until the same readiness probe (`GET /api/customers/people`)\n// stays green with the cache layer engaged. Set `OM_ORG_SCOPE_CACHE_TTL_MS=60000`\n// (or any positive integer) to opt in once cross-request safety is re-verified.\nconst ORG_SCOPE_DEFAULT_TTL_MS = 0\n\nfunction resolveOrgScopeTtlMs(): number {\n const raw = process.env.OM_ORG_SCOPE_CACHE_TTL_MS\n if (raw === undefined) return ORG_SCOPE_DEFAULT_TTL_MS\n const parsed = Number(raw)\n if (!Number.isFinite(parsed) || parsed < 0) return ORG_SCOPE_DEFAULT_TTL_MS\n return parsed\n}\n\nfunction buildOrgScopeCacheKey(parts: {\n userId: string\n effectiveTenantId: string\n selectedOrgId: string | null\n requestedTenantId: string | null\n}): string {\n const selected = parts.selectedOrgId ?? 'none'\n const requested = parts.requestedTenantId ?? 'none'\n return `${ORG_SCOPE_CACHE_KEY_PREFIX}:${parts.userId}:${parts.effectiveTenantId}:${selected}:${requested}`\n}\n\nfunction buildOrgScopeCacheTags(parts: { userId: string; effectiveTenantId: string }): string[] {\n return [\n `${ORG_SCOPE_CACHE_KEY_PREFIX}:user:${parts.userId}`,\n `${ORG_SCOPE_CACHE_KEY_PREFIX}:tenant:${parts.effectiveTenantId}`,\n ]\n}\n\nfunction isValidCachedScope(value: unknown): value is OrganizationScope {\n if (typeof value !== 'object' || value === null) return false\n const record = value as Partial<OrganizationScope>\n const idOk = (v: unknown) => v === null || typeof v === 'string'\n const arrOk = (v: unknown) => v === null || (Array.isArray(v) && v.every((entry) => typeof entry === 'string'))\n return idOk(record.selectedId) && idOk(record.tenantId) && arrOk(record.filterIds) && arrOk(record.allowedIds)\n}\n\nfunction resolveCacheFromContainer(container: AwilixContainer | null | undefined): CacheStrategy | null {\n if (!container) return null\n try {\n const c = container.resolve('cache') as CacheStrategy | undefined\n if (c && typeof c.get === 'function' && typeof c.set === 'function') return c\n } catch {\n return null\n }\n return null\n}\n\nexport async function invalidateOrganizationScopeCacheForUser(\n container: AwilixContainer,\n userId: string,\n): Promise<void> {\n const cache = resolveCacheFromContainer(container)\n if (!cache?.deleteByTags) return\n try {\n await cache.deleteByTags([`${ORG_SCOPE_CACHE_KEY_PREFIX}:user:${userId}`])\n } catch (err) {\n console.warn('[org-scope:cache] invalidate user failed', err)\n }\n}\n\nexport async function invalidateOrganizationScopeCacheForTenant(\n container: AwilixContainer,\n tenantId: string,\n): Promise<void> {\n const cache = resolveCacheFromContainer(container)\n if (!cache?.deleteByTags) return\n try {\n await cache.deleteByTags([`${ORG_SCOPE_CACHE_KEY_PREFIX}:tenant:${tenantId}`])\n } catch (err) {\n console.warn('[org-scope:cache] invalidate tenant failed', err)\n }\n}\n\nfunction normalizeOrganizationId(value: unknown): string | null {\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : null\n}\n\nexport function getSelectedOrganizationFromRequest(req: Request | { cookies?: { get: (name: string) => { value: string } | undefined }; headers?: { get(name: string): string | null } }): string | null {\n const cookieContainer = (req as { cookies?: { get: (name: string) => { value: string } | undefined } }).cookies\n if (cookieContainer && typeof cookieContainer.get === 'function') {\n const val = cookieContainer.get('om_selected_org')?.value\n return val ?? null\n }\n const headerContainer = (req as { headers?: { get(name: string): string | null } }).headers\n const header = typeof headerContainer?.get === 'function' ? headerContainer.get('cookie') : null\n return parseSelectedOrganizationCookie(header)\n}\n\nexport function getSelectedTenantFromRequest(\n req: Request | { cookies?: { get: (name: string) => { value: string } | undefined }; headers?: { get(name: string): string | null } },\n): string | null {\n const cookieContainer = (req as { cookies?: { get: (name: string) => { value: string } | undefined } }).cookies\n if (cookieContainer && typeof cookieContainer.get === 'function') {\n const val = cookieContainer.get('om_selected_tenant')?.value\n return val ?? null\n }\n const headerContainer = (req as { headers?: { get(name: string): string | null } }).headers\n const header = typeof headerContainer?.get === 'function' ? headerContainer.get('cookie') : null\n return parseSelectedTenantCookie(header)\n}\n\nasync function collectWithDescendants(em: EntityManager, tenantId: string, ids: string[]): Promise<Set<string>> {\n if (!ids.length) return new Set()\n const unique = Array.from(new Set(\n ids.map((value) => normalizeOrganizationId(value)).filter((value): value is string => {\n if (!value) return false\n if (isAllOrganizationsSelection(value)) return false\n return true\n })\n ))\n if (!unique.length) return new Set()\n const filter: FilterQuery<Organization> = {\n tenant: tenantId,\n id: { $in: unique },\n deletedAt: null,\n }\n const orgs = await em.find(Organization, filter)\n const set = new Set<string>()\n for (const org of orgs) {\n const id = String(org.id)\n set.add(id)\n if (Array.isArray(org.descendantIds)) {\n for (const desc of org.descendantIds) set.add(String(desc))\n }\n }\n return set\n}\n\nexport async function resolveOrganizationScope({\n em,\n rbac,\n auth,\n selectedId,\n tenantId: tenantIdOverride,\n}: {\n em: EntityManager\n rbac: RbacService\n auth: AuthContext\n selectedId?: string | null\n tenantId?: string | null\n}): Promise<OrganizationScope> {\n if (!auth || !auth.sub) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n const actorTenantId = typeof auth.tenantId === 'string' && auth.tenantId.trim().length > 0 ? auth.tenantId.trim() : null\n const candidateTenantId = typeof tenantIdOverride === 'string' && tenantIdOverride.trim().length > 0\n ? tenantIdOverride.trim()\n : tenantIdOverride === null\n ? null\n : actorTenantId\n if (!candidateTenantId) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n const usingOverride = candidateTenantId !== actorTenantId\n const isSuperAdminActor = auth.isSuperAdmin === true\n const tenantId = usingOverride && actorTenantId && !isSuperAdminActor ? actorTenantId : candidateTenantId\n if (!tenantId) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n const normalizedRequestedSelection = normalizeOrganizationId(selectedId)\n const explicitAllOrgsChoice =\n normalizedRequestedSelection !== null && isAllOrganizationsSelection(normalizedRequestedSelection)\n const normalizedSelectedId = explicitAllOrgsChoice\n ? null\n : normalizedRequestedSelection\n const contextOrgId = actorTenantId && actorTenantId === tenantId ? normalizeOrganizationId(auth.orgId) : null\n const acl = await rbac.loadAcl(auth.sub, { tenantId, organizationId: contextOrgId })\n const aclIsSuperAdmin = acl?.isSuperAdmin === true\n const effectiveSuperAdmin = aclIsSuperAdmin || isSuperAdminActor\n const normalizedAccessible = effectiveSuperAdmin\n ? null\n : Array.isArray(acl?.organizations)\n ? acl.organizations\n .map((value) => normalizeOrganizationId(value))\n .filter((value): value is string => value !== null)\n : null\n const accessibleList = effectiveSuperAdmin\n ? null\n : normalizedAccessible && normalizedAccessible.some((value) => isAllOrganizationsSelection(value))\n ? null\n : normalizedAccessible?.filter((value) => !isAllOrganizationsSelection(value)) ?? null\n\n const accountOrgId = actorTenantId && actorTenantId === tenantId ? normalizeOrganizationId(auth.orgId) : null\n const fallbackOrgId = accountOrgId ?? null\n let fallbackSet: Set<string> | null = null\n const loadFallbackSet = async (): Promise<Set<string> | null> => {\n if (!fallbackOrgId) return null\n if (!fallbackSet) {\n fallbackSet = await collectWithDescendants(em, tenantId, [fallbackOrgId])\n }\n return fallbackSet\n }\n\n let allowedSet: Set<string> | null = null\n if (accessibleList === null) {\n allowedSet = null\n } else if (accessibleList.length === 0) {\n allowedSet = new Set()\n } else {\n allowedSet = await collectWithDescendants(em, tenantId, accessibleList)\n }\n\n if (allowedSet && allowedSet.size === 0 && fallbackOrgId) {\n const computed = await loadFallbackSet()\n if (computed && computed.size > 0) {\n allowedSet = computed\n }\n }\n\n const hasUnrestrictedAccess = effectiveSuperAdmin || (accessibleList === null)\n const noOrgSelection = normalizedSelectedId === null && !explicitAllOrgsChoice\n const widenToAllOrgs =\n (explicitAllOrgsChoice && hasUnrestrictedAccess)\n || (effectiveSuperAdmin && noOrgSelection)\n const initialSelected =\n normalizedSelectedId\n ?? (widenToAllOrgs ? null : accountOrgId ?? null)\n let effectiveSelected: string | null = null\n if (initialSelected) {\n if (allowedSet === null || allowedSet.has(initialSelected)) {\n effectiveSelected = initialSelected\n }\n }\n\n let filterSet: Set<string> | null = null\n if (effectiveSelected) {\n filterSet = await collectWithDescendants(em, tenantId, [effectiveSelected])\n } else if (allowedSet !== null) {\n filterSet = allowedSet\n } else if (widenToAllOrgs) {\n filterSet = null\n } else if (auth.orgId) {\n filterSet = await loadFallbackSet()\n }\n\n if ((!filterSet || filterSet.size === 0) && fallbackOrgId && !widenToAllOrgs) {\n const computed = await loadFallbackSet()\n if (computed && computed.size > 0) {\n filterSet = computed\n if (!effectiveSelected) {\n effectiveSelected = fallbackOrgId\n }\n }\n }\n\n return {\n selectedId: effectiveSelected,\n filterIds: filterSet ? Array.from(filterSet) : null,\n allowedIds: allowedSet ? Array.from(allowedSet) : null,\n tenantId,\n }\n}\n\nexport async function resolveOrganizationScopeForRequest({\n container,\n auth,\n request,\n selectedId,\n tenantId: tenantOverride,\n}: {\n container: AwilixContainer\n auth: AuthContext | null | undefined\n request?: Request | { cookies?: { get: (name: string) => { value: string } | undefined }; headers?: { get(name: string): string | null } }\n selectedId?: string | null\n tenantId?: string | null\n}): Promise<OrganizationScope> {\n if (!auth || !auth.sub) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n\n let em: EntityManager | null = null\n let rbac: RbacService | null = null\n try { em = container.resolve<EntityManager>('em') } catch { em = null }\n try { rbac = container.resolve<RbacService>('rbacService') } catch { rbac = null }\n const normalizeString = (value: unknown): string | null => {\n if (typeof value === 'string' && value.trim().length > 0) return value.trim()\n return null\n }\n if (!em || !rbac) {\n const fallbackSelected = normalizeOrganizationId(selectedId ?? auth.orgId ?? null)\n return {\n selectedId: fallbackSelected,\n filterIds: fallbackSelected ? [fallbackSelected] : null,\n allowedIds: fallbackSelected ? [fallbackSelected] : null,\n tenantId: normalizeString(auth.tenantId),\n }\n }\n\n const actorTenantField = (auth as { actorTenantId?: string | null }).actorTenantId\n const actorTenant = actorTenantField === undefined\n ? normalizeString(auth.tenantId)\n : actorTenantField === null\n ? null\n : normalizeString(actorTenantField)\n const actorOrgField = (auth as { actorOrgId?: string | null }).actorOrgId\n const actorOrgId = actorOrgField === undefined\n ? normalizeString(auth.orgId)\n : actorOrgField === null\n ? null\n : normalizeString(actorOrgField)\n\n const cookieTenant = request ? getSelectedTenantFromRequest(request) : null\n const requestedTenant =\n tenantOverride !== undefined\n ? tenantOverride\n : cookieTenant !== undefined\n ? cookieTenant\n : undefined\n const requestedTenantId = typeof requestedTenant === 'string' && requestedTenant.trim().length > 0 ? requestedTenant.trim() : null\n const isSuperAdminActor = auth.isSuperAdmin === true\n let effectiveTenantId = requestedTenantId ?? actorTenant ?? null\n if (actorTenant && effectiveTenantId && effectiveTenantId !== actorTenant && !isSuperAdminActor) {\n effectiveTenantId = actorTenant\n }\n if (!effectiveTenantId) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n\n const scopedAuth = {\n ...auth,\n tenantId: effectiveTenantId,\n orgId: actorTenant && actorTenant === effectiveTenantId ? actorOrgId ?? null : null,\n }\n\n const rawSelected = selectedId !== undefined ? selectedId : (request ? getSelectedOrganizationFromRequest(request) : null)\n const normalizedSelectedId = typeof rawSelected === 'string' && rawSelected.trim().length > 0\n ? rawSelected.trim()\n : null\n\n const userId = typeof auth.sub === 'string' && auth.sub.length > 0 ? auth.sub : null\n const ttlMs = resolveOrgScopeTtlMs()\n const cache = ttlMs > 0 ? resolveCacheFromContainer(container) : null\n const cacheKey = userId\n ? buildOrgScopeCacheKey({\n userId,\n effectiveTenantId,\n selectedOrgId: normalizedSelectedId,\n requestedTenantId: requestedTenantId ?? null,\n })\n : null\n\n if (cache && cacheKey && typeof cache.get === 'function') {\n try {\n const cached = await cache.get(cacheKey)\n if (isValidCachedScope(cached)) return cached\n } catch (err) {\n console.warn('[org-scope:cache] read failed', err)\n }\n }\n\n const baseScope = await resolveOrganizationScope({\n em,\n rbac,\n auth: scopedAuth,\n selectedId: rawSelected,\n tenantId: effectiveTenantId,\n })\n\n if (cache && cacheKey && userId && typeof cache.set === 'function') {\n try {\n await cache.set(cacheKey, baseScope, {\n ttl: ttlMs,\n tags: buildOrgScopeCacheTags({ userId, effectiveTenantId }),\n })\n } catch (err) {\n console.warn('[org-scope:cache] write failed', err)\n }\n }\n\n return baseScope\n}\n\nexport type FeatureCheckContext = {\n organizationId: string | null\n scope: OrganizationScope\n allowedOrganizationIds: string[] | null\n}\n\nexport async function resolveFeatureCheckContext({\n container,\n auth,\n request,\n selectedId,\n tenantId,\n}: {\n container: AwilixContainer\n auth: AuthContext | null | undefined\n request?: Request | { cookies?: { get: (name: string) => { value: string } | undefined } }\n selectedId?: string | null\n tenantId?: string | null\n}): Promise<FeatureCheckContext> {\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request, selectedId, tenantId })\n const allowedOrganizationIds = scope.allowedIds ?? null\n const authOrgId = auth?.orgId ?? null\n const organizationId =\n scope.selectedId\n ?? (authOrgId && (!Array.isArray(allowedOrganizationIds) || allowedOrganizationIds.includes(authOrgId)) ? authOrgId : null)\n ?? (Array.isArray(allowedOrganizationIds) && allowedOrganizationIds.length ? allowedOrganizationIds[0] : null)\n\n return { organizationId, scope, allowedOrganizationIds }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { FilterQuery } from '@mikro-orm/core'\nimport type { AwilixContainer } from 'awilix'\nimport { Organization } from '@open-mercato/core/modules/directory/data/entities'\nimport { isAllOrganizationsSelection } from '@open-mercato/core/modules/directory/constants'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport type { AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport { parseSelectedOrganizationCookie, parseSelectedTenantCookie } from './scopeCookies'\n\nexport { parseSelectedOrganizationCookie, parseSelectedTenantCookie }\n\nexport type OrganizationScope = {\n selectedId: string | null\n filterIds: string[] | null\n allowedIds: string[] | null\n tenantId: string | null\n}\n\n// Phase 4 \u2014 short-TTL cache for resolveOrganizationScopeForRequest.\n// OrganizationScope is a pure function of (userId, tenantId, selectedOrgId,\n// requestedTenant) between membership changes; caching it bypasses 1\n// SELECT on `organizations` per CRUD request. TTL is short (60s default)\n// to keep staleness bounded for membership/visibility changes. Tag-based\n// invalidation kicks the cache when user_organizations or organizations\n// mutate (wired via invalidateOrganizationScopeCacheFor).\nconst ORG_SCOPE_CACHE_KEY_PREFIX = 'org-scope'\n// Phase 4 default-off until the same readiness probe (`GET /api/customers/people`)\n// stays green with the cache layer engaged. Set `OM_ORG_SCOPE_CACHE_TTL_MS=60000`\n// (or any positive integer) to opt in once cross-request safety is re-verified.\nconst ORG_SCOPE_DEFAULT_TTL_MS = 0\n\nfunction resolveOrgScopeTtlMs(): number {\n const raw = process.env.OM_ORG_SCOPE_CACHE_TTL_MS\n if (raw === undefined) return ORG_SCOPE_DEFAULT_TTL_MS\n const parsed = Number(raw)\n if (!Number.isFinite(parsed) || parsed < 0) return ORG_SCOPE_DEFAULT_TTL_MS\n return parsed\n}\n\nfunction buildOrgScopeCacheKey(parts: {\n userId: string\n effectiveTenantId: string\n selectedOrgId: string | null\n requestedTenantId: string | null\n}): string {\n const selected = parts.selectedOrgId ?? 'none'\n const requested = parts.requestedTenantId ?? 'none'\n return `${ORG_SCOPE_CACHE_KEY_PREFIX}:${parts.userId}:${parts.effectiveTenantId}:${selected}:${requested}`\n}\n\nfunction buildOrgScopeCacheTags(parts: { userId: string; effectiveTenantId: string }): string[] {\n return [\n `${ORG_SCOPE_CACHE_KEY_PREFIX}:user:${parts.userId}`,\n `${ORG_SCOPE_CACHE_KEY_PREFIX}:tenant:${parts.effectiveTenantId}`,\n ]\n}\n\nfunction isValidCachedScope(value: unknown): value is OrganizationScope {\n if (typeof value !== 'object' || value === null) return false\n const record = value as Partial<OrganizationScope>\n const idOk = (v: unknown) => v === null || typeof v === 'string'\n const arrOk = (v: unknown) => v === null || (Array.isArray(v) && v.every((entry) => typeof entry === 'string'))\n return idOk(record.selectedId) && idOk(record.tenantId) && arrOk(record.filterIds) && arrOk(record.allowedIds)\n}\n\nfunction resolveCacheFromContainer(container: AwilixContainer | null | undefined): CacheStrategy | null {\n if (!container) return null\n try {\n const c = container.resolve('cache') as CacheStrategy | undefined\n if (c && typeof c.get === 'function' && typeof c.set === 'function') return c\n } catch {\n return null\n }\n return null\n}\n\nexport async function invalidateOrganizationScopeCacheForUser(\n container: AwilixContainer,\n userId: string,\n): Promise<void> {\n const cache = resolveCacheFromContainer(container)\n if (!cache?.deleteByTags) return\n try {\n await cache.deleteByTags([`${ORG_SCOPE_CACHE_KEY_PREFIX}:user:${userId}`])\n } catch (err) {\n console.warn('[org-scope:cache] invalidate user failed', err)\n }\n}\n\nexport async function invalidateOrganizationScopeCacheForTenant(\n container: AwilixContainer,\n tenantId: string,\n): Promise<void> {\n const cache = resolveCacheFromContainer(container)\n if (!cache?.deleteByTags) return\n try {\n await cache.deleteByTags([`${ORG_SCOPE_CACHE_KEY_PREFIX}:tenant:${tenantId}`])\n } catch (err) {\n console.warn('[org-scope:cache] invalidate tenant failed', err)\n }\n}\n\nfunction normalizeOrganizationId(value: unknown): string | null {\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : null\n}\n\nexport function getSelectedOrganizationFromRequest(req: Request | { cookies?: { get: (name: string) => { value: string } | undefined }; headers?: { get(name: string): string | null } }): string | null {\n const cookieContainer = (req as { cookies?: { get: (name: string) => { value: string } | undefined } }).cookies\n if (cookieContainer && typeof cookieContainer.get === 'function') {\n const val = cookieContainer.get('om_selected_org')?.value\n return val ?? null\n }\n const headerContainer = (req as { headers?: { get(name: string): string | null } }).headers\n const header = typeof headerContainer?.get === 'function' ? headerContainer.get('cookie') : null\n return parseSelectedOrganizationCookie(header)\n}\n\nexport function getSelectedTenantFromRequest(\n req: Request | { cookies?: { get: (name: string) => { value: string } | undefined }; headers?: { get(name: string): string | null } },\n): string | null {\n const cookieContainer = (req as { cookies?: { get: (name: string) => { value: string } | undefined } }).cookies\n if (cookieContainer && typeof cookieContainer.get === 'function') {\n const val = cookieContainer.get('om_selected_tenant')?.value\n return val ?? null\n }\n const headerContainer = (req as { headers?: { get(name: string): string | null } }).headers\n const header = typeof headerContainer?.get === 'function' ? headerContainer.get('cookie') : null\n return parseSelectedTenantCookie(header)\n}\n\nfunction normalizeOrganizationIds(ids: string[]): string[] {\n return Array.from(new Set(\n ids.map((value) => normalizeOrganizationId(value)).filter((value): value is string => {\n if (!value) return false\n if (isAllOrganizationsSelection(value)) return false\n return true\n })\n ))\n}\n\n// Map each organization id to itself plus its persisted descendant ids. Only\n// orgs that exist for the tenant and are not soft-deleted are included, so an\n// unknown/inaccessible id simply has no entry (matching the per-id query that\n// returned an empty set for it).\ntype OrgDescendantMap = Map<string, string[]>\n\n// Issue #2228 \u2014 single round-trip for org-scope resolution. Instead of issuing\n// one `organizations` SELECT per `collectWithDescendants` call (up to 3-4\n// sequential queries per request: accessible set, fallback set, selected set),\n// gather every candidate id up front and fetch their descendant expansions in\n// one `em.find(Organization, { id: $in })`. Expansion then happens in-memory.\nasync function loadOrgDescendantMap(em: EntityManager, tenantId: string, ids: string[]): Promise<OrgDescendantMap> {\n const unique = normalizeOrganizationIds(ids)\n if (!unique.length) return new Map()\n const filter: FilterQuery<Organization> = {\n tenant: tenantId,\n id: { $in: unique },\n deletedAt: null,\n }\n const orgs = await em.find(Organization, filter)\n const map: OrgDescendantMap = new Map()\n for (const org of orgs) {\n const id = String(org.id)\n const expansion = [id]\n if (Array.isArray(org.descendantIds)) {\n for (const desc of org.descendantIds) expansion.push(String(desc))\n }\n map.set(id, expansion)\n }\n return map\n}\n\nfunction expandWithDescendants(map: OrgDescendantMap, ids: string[]): Set<string> {\n const set = new Set<string>()\n for (const value of ids) {\n const id = normalizeOrganizationId(value)\n if (!id || isAllOrganizationsSelection(id)) continue\n const expansion = map.get(id)\n if (!expansion) continue\n for (const entry of expansion) set.add(entry)\n }\n return set\n}\n\nexport async function resolveOrganizationScope({\n em,\n rbac,\n auth,\n selectedId,\n tenantId: tenantIdOverride,\n}: {\n em: EntityManager\n rbac: RbacService\n auth: AuthContext\n selectedId?: string | null\n tenantId?: string | null\n}): Promise<OrganizationScope> {\n if (!auth || !auth.sub) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n const actorTenantId = typeof auth.tenantId === 'string' && auth.tenantId.trim().length > 0 ? auth.tenantId.trim() : null\n const candidateTenantId = typeof tenantIdOverride === 'string' && tenantIdOverride.trim().length > 0\n ? tenantIdOverride.trim()\n : tenantIdOverride === null\n ? null\n : actorTenantId\n if (!candidateTenantId) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n const usingOverride = candidateTenantId !== actorTenantId\n const isSuperAdminActor = auth.isSuperAdmin === true\n const tenantId = usingOverride && actorTenantId && !isSuperAdminActor ? actorTenantId : candidateTenantId\n if (!tenantId) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n const normalizedRequestedSelection = normalizeOrganizationId(selectedId)\n const explicitAllOrgsChoice =\n normalizedRequestedSelection !== null && isAllOrganizationsSelection(normalizedRequestedSelection)\n const normalizedSelectedId = explicitAllOrgsChoice\n ? null\n : normalizedRequestedSelection\n const contextOrgId = actorTenantId && actorTenantId === tenantId ? normalizeOrganizationId(auth.orgId) : null\n const acl = await rbac.loadAcl(auth.sub, { tenantId, organizationId: contextOrgId })\n const aclIsSuperAdmin = acl?.isSuperAdmin === true\n const effectiveSuperAdmin = aclIsSuperAdmin || isSuperAdminActor\n const normalizedAccessible = effectiveSuperAdmin\n ? null\n : Array.isArray(acl?.organizations)\n ? acl.organizations\n .map((value) => normalizeOrganizationId(value))\n .filter((value): value is string => value !== null)\n : null\n const accessibleList = effectiveSuperAdmin\n ? null\n : normalizedAccessible && normalizedAccessible.some((value) => isAllOrganizationsSelection(value))\n ? null\n : normalizedAccessible?.filter((value) => !isAllOrganizationsSelection(value)) ?? null\n\n const accountOrgId = actorTenantId && actorTenantId === tenantId ? normalizeOrganizationId(auth.orgId) : null\n const fallbackOrgId = accountOrgId ?? null\n\n // Every id that could be expanded below \u2014 accessible set, fallback (account)\n // org, and the requested selection \u2014 is known up front, so fetch them all in\n // a single `organizations` query and expand from the in-memory map.\n const candidateIds = [\n ...(accessibleList ?? []),\n ...(fallbackOrgId ? [fallbackOrgId] : []),\n ...(normalizedSelectedId ? [normalizedSelectedId] : []),\n ]\n const orgDescendants = await loadOrgDescendantMap(em, tenantId, candidateIds)\n const loadFallbackSet = (): Set<string> | null =>\n fallbackOrgId ? expandWithDescendants(orgDescendants, [fallbackOrgId]) : null\n\n let allowedSet: Set<string> | null = null\n if (accessibleList === null) {\n allowedSet = null\n } else if (accessibleList.length === 0) {\n allowedSet = new Set()\n } else {\n allowedSet = expandWithDescendants(orgDescendants, accessibleList)\n }\n\n if (allowedSet && allowedSet.size === 0 && fallbackOrgId) {\n const computed = loadFallbackSet()\n if (computed && computed.size > 0) {\n allowedSet = computed\n }\n }\n\n const hasUnrestrictedAccess = effectiveSuperAdmin || (accessibleList === null)\n const noOrgSelection = normalizedSelectedId === null && !explicitAllOrgsChoice\n const widenToAllOrgs =\n (explicitAllOrgsChoice && hasUnrestrictedAccess)\n || (effectiveSuperAdmin && noOrgSelection)\n const initialSelected =\n normalizedSelectedId\n ?? (widenToAllOrgs ? null : accountOrgId ?? null)\n let effectiveSelected: string | null = null\n if (initialSelected) {\n if (allowedSet === null || allowedSet.has(initialSelected)) {\n effectiveSelected = initialSelected\n }\n }\n\n let filterSet: Set<string> | null = null\n if (effectiveSelected) {\n filterSet = expandWithDescendants(orgDescendants, [effectiveSelected])\n } else if (allowedSet !== null) {\n filterSet = allowedSet\n } else if (widenToAllOrgs) {\n filterSet = null\n } else if (auth.orgId) {\n filterSet = loadFallbackSet()\n }\n\n if ((!filterSet || filterSet.size === 0) && fallbackOrgId && !widenToAllOrgs) {\n const computed = loadFallbackSet()\n if (computed && computed.size > 0) {\n filterSet = computed\n if (!effectiveSelected) {\n effectiveSelected = fallbackOrgId\n }\n }\n }\n\n return {\n selectedId: effectiveSelected,\n filterIds: filterSet ? Array.from(filterSet) : null,\n allowedIds: allowedSet ? Array.from(allowedSet) : null,\n tenantId,\n }\n}\n\nexport async function resolveOrganizationScopeForRequest({\n container,\n auth,\n request,\n selectedId,\n tenantId: tenantOverride,\n}: {\n container: AwilixContainer\n auth: AuthContext | null | undefined\n request?: Request | { cookies?: { get: (name: string) => { value: string } | undefined }; headers?: { get(name: string): string | null } }\n selectedId?: string | null\n tenantId?: string | null\n}): Promise<OrganizationScope> {\n if (!auth || !auth.sub) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n\n let em: EntityManager | null = null\n let rbac: RbacService | null = null\n try { em = container.resolve<EntityManager>('em') } catch { em = null }\n try { rbac = container.resolve<RbacService>('rbacService') } catch { rbac = null }\n const normalizeString = (value: unknown): string | null => {\n if (typeof value === 'string' && value.trim().length > 0) return value.trim()\n return null\n }\n if (!em || !rbac) {\n const fallbackSelected = normalizeOrganizationId(selectedId ?? auth.orgId ?? null)\n return {\n selectedId: fallbackSelected,\n filterIds: fallbackSelected ? [fallbackSelected] : null,\n allowedIds: fallbackSelected ? [fallbackSelected] : null,\n tenantId: normalizeString(auth.tenantId),\n }\n }\n\n const actorTenantField = (auth as { actorTenantId?: string | null }).actorTenantId\n const actorTenant = actorTenantField === undefined\n ? normalizeString(auth.tenantId)\n : actorTenantField === null\n ? null\n : normalizeString(actorTenantField)\n const actorOrgField = (auth as { actorOrgId?: string | null }).actorOrgId\n const actorOrgId = actorOrgField === undefined\n ? normalizeString(auth.orgId)\n : actorOrgField === null\n ? null\n : normalizeString(actorOrgField)\n\n const cookieTenant = request ? getSelectedTenantFromRequest(request) : null\n const requestedTenant =\n tenantOverride !== undefined\n ? tenantOverride\n : cookieTenant !== undefined\n ? cookieTenant\n : undefined\n const requestedTenantId = typeof requestedTenant === 'string' && requestedTenant.trim().length > 0 ? requestedTenant.trim() : null\n const isSuperAdminActor = auth.isSuperAdmin === true\n let effectiveTenantId = requestedTenantId ?? actorTenant ?? null\n if (actorTenant && effectiveTenantId && effectiveTenantId !== actorTenant && !isSuperAdminActor) {\n effectiveTenantId = actorTenant\n }\n if (!effectiveTenantId) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n\n const scopedAuth = {\n ...auth,\n tenantId: effectiveTenantId,\n orgId: actorTenant && actorTenant === effectiveTenantId ? actorOrgId ?? null : null,\n }\n\n const rawSelected = selectedId !== undefined ? selectedId : (request ? getSelectedOrganizationFromRequest(request) : null)\n const normalizedSelectedId = typeof rawSelected === 'string' && rawSelected.trim().length > 0\n ? rawSelected.trim()\n : null\n\n const userId = typeof auth.sub === 'string' && auth.sub.length > 0 ? auth.sub : null\n const ttlMs = resolveOrgScopeTtlMs()\n const cache = ttlMs > 0 ? resolveCacheFromContainer(container) : null\n const cacheKey = userId\n ? buildOrgScopeCacheKey({\n userId,\n effectiveTenantId,\n selectedOrgId: normalizedSelectedId,\n requestedTenantId: requestedTenantId ?? null,\n })\n : null\n\n if (cache && cacheKey && typeof cache.get === 'function') {\n try {\n const cached = await cache.get(cacheKey)\n if (isValidCachedScope(cached)) return cached\n } catch (err) {\n console.warn('[org-scope:cache] read failed', err)\n }\n }\n\n const baseScope = await resolveOrganizationScope({\n em,\n rbac,\n auth: scopedAuth,\n selectedId: rawSelected,\n tenantId: effectiveTenantId,\n })\n\n if (cache && cacheKey && userId && typeof cache.set === 'function') {\n try {\n await cache.set(cacheKey, baseScope, {\n ttl: ttlMs,\n tags: buildOrgScopeCacheTags({ userId, effectiveTenantId }),\n })\n } catch (err) {\n console.warn('[org-scope:cache] write failed', err)\n }\n }\n\n return baseScope\n}\n\nexport type FeatureCheckContext = {\n organizationId: string | null\n scope: OrganizationScope\n allowedOrganizationIds: string[] | null\n}\n\nexport async function resolveFeatureCheckContext({\n container,\n auth,\n request,\n selectedId,\n tenantId,\n}: {\n container: AwilixContainer\n auth: AuthContext | null | undefined\n request?: Request | { cookies?: { get: (name: string) => { value: string } | undefined } }\n selectedId?: string | null\n tenantId?: string | null\n}): Promise<FeatureCheckContext> {\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request, selectedId, tenantId })\n const allowedOrganizationIds = scope.allowedIds ?? null\n const authOrgId = auth?.orgId ?? null\n const organizationId =\n scope.selectedId\n ?? (authOrgId && (!Array.isArray(allowedOrganizationIds) || allowedOrganizationIds.includes(authOrgId)) ? authOrgId : null)\n ?? (Array.isArray(allowedOrganizationIds) && allowedOrganizationIds.length ? allowedOrganizationIds[0] : null)\n\n return { organizationId, scope, allowedOrganizationIds }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.6.4-develop.4113.1.5e87922616",
3
+ "version": "0.6.4-develop.4133.1.48fc6c8f7b",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -243,16 +243,16 @@
243
243
  "zod": "^4.4.3"
244
244
  },
245
245
  "peerDependencies": {
246
- "@open-mercato/ai-assistant": "0.6.4-develop.4113.1.5e87922616",
247
- "@open-mercato/shared": "0.6.4-develop.4113.1.5e87922616",
248
- "@open-mercato/ui": "0.6.4-develop.4113.1.5e87922616",
246
+ "@open-mercato/ai-assistant": "0.6.4-develop.4133.1.48fc6c8f7b",
247
+ "@open-mercato/shared": "0.6.4-develop.4133.1.48fc6c8f7b",
248
+ "@open-mercato/ui": "0.6.4-develop.4133.1.48fc6c8f7b",
249
249
  "react": "^19.0.0",
250
250
  "react-dom": "^19.0.0"
251
251
  },
252
252
  "devDependencies": {
253
- "@open-mercato/ai-assistant": "0.6.4-develop.4113.1.5e87922616",
254
- "@open-mercato/shared": "0.6.4-develop.4113.1.5e87922616",
255
- "@open-mercato/ui": "0.6.4-develop.4113.1.5e87922616",
253
+ "@open-mercato/ai-assistant": "0.6.4-develop.4133.1.48fc6c8f7b",
254
+ "@open-mercato/shared": "0.6.4-develop.4133.1.48fc6c8f7b",
255
+ "@open-mercato/ui": "0.6.4-develop.4133.1.48fc6c8f7b",
256
256
  "@testing-library/dom": "^10.4.1",
257
257
  "@testing-library/jest-dom": "^6.9.1",
258
258
  "@testing-library/react": "^16.3.1",
@@ -64,19 +64,28 @@ export async function resolveCanonicalStaffAuthContext(
64
64
  return null
65
65
  }
66
66
  }
67
- if (sessionId !== null) {
68
- const session = await findOneWithDecryption(em, Session, { id: sessionId, deletedAt: null })
69
- if (!session) return null
70
- if (session.expiresAt.getTime() < Date.now()) return null
71
- }
72
-
73
- const user = await findOneWithDecryption(
67
+ // The session-revocation check and the user load are independent (neither reads
68
+ // the other's result), so they run concurrently to collapse two sequential DB
69
+ // round-trips into one. The `em` here is a fresh request-scoped EntityManager
70
+ // (resolved per request, never inside an explicit transaction), so concurrent
71
+ // reads on it are safe.
72
+ const sessionPromise = sessionId !== null
73
+ ? findOneWithDecryption(em, Session, { id: sessionId, deletedAt: null })
74
+ : Promise.resolve(null)
75
+ const userPromise = findOneWithDecryption(
74
76
  em,
75
77
  User,
76
78
  { id: subjectId, deletedAt: null },
77
79
  undefined,
78
80
  { tenantId: actorTenantId, organizationId: actorOrganizationId },
79
81
  )
82
+ const [session, user] = await Promise.all([sessionPromise, userPromise])
83
+
84
+ if (sessionId !== null) {
85
+ if (!session) return null
86
+ if (session.expiresAt.getTime() < Date.now()) return null
87
+ }
88
+
80
89
  if (!user) return null
81
90
 
82
91
  const currentTenantId = normalizeScopeId(user.tenantId ?? null)
@@ -90,8 +99,12 @@ export async function resolveCanonicalStaffAuthContext(
90
99
  return null
91
100
  }
92
101
 
93
- const links = currentTenantId
94
- ? await findWithDecryption(
102
+ // Role links and the per-user super-admin flag are likewise independent, so they
103
+ // run concurrently. The role-level super-admin lookup depends on the resolved
104
+ // role ids, so it stays sequential after the links resolve (and is skipped
105
+ // entirely when the per-user flag already grants super-admin).
106
+ const linksPromise = currentTenantId
107
+ ? findWithDecryption(
95
108
  em,
96
109
  UserRole,
97
110
  {
@@ -102,7 +115,11 @@ export async function resolveCanonicalStaffAuthContext(
102
115
  { populate: ['role'] },
103
116
  { tenantId: currentTenantId, organizationId: currentOrganizationId },
104
117
  )
105
- : []
118
+ : Promise.resolve([] as UserRole[])
119
+ const userAclSuperAdminPromise = currentTenantId
120
+ ? userAclGrantsSuperAdmin(em, user.id, currentTenantId, currentOrganizationId)
121
+ : Promise.resolve(false)
122
+ const [links, userAclSuperAdmin] = await Promise.all([linksPromise, userAclSuperAdminPromise])
106
123
 
107
124
  const linkedRoles = links
108
125
  .map((link) => link.role)
@@ -113,7 +130,7 @@ export async function resolveCanonicalStaffAuthContext(
113
130
  .filter((name): name is string => typeof name === 'string' && name.trim().length > 0)
114
131
 
115
132
  const isSuperAdmin = currentTenantId
116
- ? await hasSuperAdminFlag(em, user.id, linkedRoles, currentTenantId, currentOrganizationId)
133
+ ? userAclSuperAdmin || (await roleAclGrantsSuperAdmin(em, linkedRoles, currentTenantId, currentOrganizationId))
117
134
  : false
118
135
 
119
136
  return {
@@ -126,10 +143,9 @@ export async function resolveCanonicalStaffAuthContext(
126
143
  }
127
144
  }
128
145
 
129
- async function hasSuperAdminFlag(
146
+ async function userAclGrantsSuperAdmin(
130
147
  em: EntityManager,
131
148
  userId: string,
132
- linkedRoles: Role[],
133
149
  tenantId: string,
134
150
  organizationId: string | null,
135
151
  ): Promise<boolean> {
@@ -145,10 +161,15 @@ async function hasSuperAdminFlag(
145
161
  undefined,
146
162
  { tenantId, organizationId },
147
163
  )
148
- if (userAcl && (userAcl as { isSuperAdmin?: boolean }).isSuperAdmin === true) {
149
- return true
150
- }
164
+ return !!(userAcl && (userAcl as { isSuperAdmin?: boolean }).isSuperAdmin === true)
165
+ }
151
166
 
167
+ async function roleAclGrantsSuperAdmin(
168
+ em: EntityManager,
169
+ linkedRoles: Role[],
170
+ tenantId: string,
171
+ organizationId: string | null,
172
+ ): Promise<boolean> {
152
173
  const roleIds = Array.from(
153
174
  new Set(
154
175
  linkedRoles
@@ -191,18 +191,24 @@ export async function findMatchingEntityIdsBySearchTokensAcrossSources({
191
191
  if (!trimmed) return null
192
192
 
193
193
  const enrichedSources = await enrichSearchSourcesWithCustomFieldTokens(ctx, sources)
194
+ const perSource = await Promise.all(
195
+ enrichedSources.map(async (source) => {
196
+ const rawIds = await findSearchTokenEntityIds({
197
+ ctx,
198
+ entityType: source.entityType,
199
+ fields: source.fields,
200
+ query: trimmed,
201
+ })
202
+ if (rawIds === null) return null
203
+ return source.mapToEntityIds
204
+ ? await mapScopedEntityIds({ ctx, ids: rawIds, config: source.mapToEntityIds })
205
+ : rawIds
206
+ }),
207
+ )
208
+
194
209
  const matchedIds = new Set<string>()
195
- for (const source of enrichedSources) {
196
- const rawIds = await findSearchTokenEntityIds({
197
- ctx,
198
- entityType: source.entityType,
199
- fields: source.fields,
200
- query: trimmed,
201
- })
202
- if (rawIds === null) return null
203
- const entityIds = source.mapToEntityIds
204
- ? await mapScopedEntityIds({ ctx, ids: rawIds, config: source.mapToEntityIds })
205
- : rawIds
210
+ for (const entityIds of perSource) {
211
+ if (entityIds === null) return null
206
212
  entityIds.forEach((id) => matchedIds.add(id))
207
213
  }
208
214
 
@@ -0,0 +1,168 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { z } from 'zod'
3
+ import type { EntityManager } from '@mikro-orm/postgresql'
4
+ import type { CacheStrategy } from '@open-mercato/cache'
5
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
6
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
7
+ import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
8
+ import {
9
+ createWidgetDataService,
10
+ type WidgetDataRequest,
11
+ WidgetDataValidationError,
12
+ } from '../../../../services/widgetDataService'
13
+ import { runWidgetDataBatch } from '../../../../lib/widgetDataBatch'
14
+ import type { AnalyticsRegistry } from '../../../../services/analyticsRegistry'
15
+ import type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
16
+ import { dashboardsTag, dashboardsErrorSchema } from '../../../openapi'
17
+ import { widgetDataRequestSchema, widgetDataResponseSchema } from '../schema'
18
+
19
+ export const metadata = {
20
+ POST: { requireAuth: true, requireFeatures: ['analytics.view'] },
21
+ }
22
+
23
+ const MAX_BATCH_SIZE = 50
24
+
25
+ const widgetDataBatchRequestSchema = z.object({
26
+ requests: z
27
+ .array(
28
+ z.object({
29
+ id: z.string().min(1),
30
+ request: widgetDataRequestSchema,
31
+ }),
32
+ )
33
+ .min(1)
34
+ .max(MAX_BATCH_SIZE),
35
+ })
36
+
37
+ const widgetDataBatchResponseSchema = z.object({
38
+ results: z.array(
39
+ z.discriminatedUnion('ok', [
40
+ z.object({
41
+ id: z.string(),
42
+ ok: z.literal(true),
43
+ data: widgetDataResponseSchema,
44
+ }),
45
+ z.object({
46
+ id: z.string(),
47
+ ok: z.literal(false),
48
+ error: z.string(),
49
+ }),
50
+ ]),
51
+ ),
52
+ })
53
+
54
+ export async function POST(req: Request) {
55
+ const auth = await getAuthFromRequest(req)
56
+ if (!auth) {
57
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
58
+ }
59
+
60
+ let body: unknown
61
+ try {
62
+ body = await req.json()
63
+ } catch {
64
+ return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })
65
+ }
66
+
67
+ const parsed = widgetDataBatchRequestSchema.safeParse(body)
68
+ if (!parsed.success) {
69
+ return NextResponse.json(
70
+ { error: 'Invalid request payload', issues: parsed.error.issues },
71
+ { status: 400 },
72
+ )
73
+ }
74
+
75
+ const tenantId = auth.tenantId ?? null
76
+ if (!tenantId) {
77
+ return NextResponse.json({ error: 'Tenant context is required' }, { status: 400 })
78
+ }
79
+
80
+ // Build the per-request DI/RBAC/org-scope stack exactly once for the whole
81
+ // batch instead of once per widget (see issue #2273).
82
+ const container = await createRequestContainer()
83
+ const analyticsRegistry = container.resolve<AnalyticsRegistry>('analyticsRegistry')
84
+
85
+ const em = (container.resolve('em') as EntityManager).fork({
86
+ clear: true,
87
+ freshEventManager: true,
88
+ useContext: true,
89
+ })
90
+
91
+ const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })
92
+
93
+ const organizationIds = (() => {
94
+ if (scope?.selectedId) return [scope.selectedId]
95
+ if (Array.isArray(scope?.filterIds) && scope.filterIds.length > 0) return scope.filterIds
96
+ if (scope?.allowedIds === null) return undefined
97
+ if (auth.orgId) return [auth.orgId]
98
+ return undefined
99
+ })()
100
+
101
+ const cache = container.resolve<CacheStrategy>('cache')
102
+ const service = createWidgetDataService(em, { tenantId, organizationIds }, analyticsRegistry, cache)
103
+
104
+ const rbacService = container.resolve<{
105
+ userHasAllFeatures: (
106
+ userId: string,
107
+ features: string[],
108
+ scope: { tenantId: string; organizationId?: string | null },
109
+ ) => Promise<boolean>
110
+ }>('rbacService')
111
+
112
+ try {
113
+ const results = await runWidgetDataBatch(parsed.data.requests as Array<{ id: string; request: WidgetDataRequest }>, {
114
+ getRequiredFeatures: (entityType) => analyticsRegistry.getRequiredFeatures(entityType),
115
+ checkFeatures: (features) => {
116
+ if (features.length === 0) return Promise.resolve(true)
117
+ return rbacService.userHasAllFeatures(auth.sub, features, {
118
+ tenantId,
119
+ organizationId: auth.orgId,
120
+ })
121
+ },
122
+ fetchOne: (request) => service.fetchWidgetData(request),
123
+ describeError: (error) =>
124
+ error instanceof WidgetDataValidationError
125
+ ? error.message
126
+ : 'An error occurred while processing your request',
127
+ })
128
+ return NextResponse.json({ results })
129
+ } catch (err) {
130
+ console.error('[widgets/data/batch] Error:', err)
131
+ return NextResponse.json(
132
+ { error: 'An error occurred while processing your request' },
133
+ { status: 500 },
134
+ )
135
+ }
136
+ }
137
+
138
+ const widgetDataBatchPostDoc: OpenApiMethodDoc = {
139
+ summary: 'Fetch aggregated data for multiple dashboard widgets in one request',
140
+ description:
141
+ '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.',
142
+ tags: [dashboardsTag],
143
+ requestBody: {
144
+ contentType: 'application/json',
145
+ schema: widgetDataBatchRequestSchema,
146
+ description: 'A list of id-keyed widget data requests to resolve together.',
147
+ },
148
+ responses: [
149
+ {
150
+ status: 200,
151
+ description: 'Per-widget aggregation results keyed by request id.',
152
+ schema: widgetDataBatchResponseSchema,
153
+ },
154
+ ],
155
+ errors: [
156
+ { status: 400, description: 'Invalid request payload', schema: dashboardsErrorSchema },
157
+ { status: 401, description: 'Authentication required', schema: dashboardsErrorSchema },
158
+ { status: 500, description: 'Internal server error', schema: dashboardsErrorSchema },
159
+ ],
160
+ }
161
+
162
+ export const openApi: OpenApiRouteDoc = {
163
+ tag: dashboardsTag,
164
+ summary: 'Batch widget data aggregation endpoint',
165
+ methods: {
166
+ POST: widgetDataBatchPostDoc,
167
+ },
168
+ }