@open-mercato/core 0.6.7-develop.6725.1.07a748cfc2 → 0.6.7-develop.6744.1.0483b18565
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/generated/entities/resources_resource/index.js +2 -0
- package/dist/generated/entities/resources_resource/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +1 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/helpers/integration/salesFixtures.js +31 -2
- package/dist/helpers/integration/salesFixtures.js.map +2 -2
- package/dist/helpers/integration/salesUi.js +5 -0
- package/dist/helpers/integration/salesUi.js.map +2 -2
- package/dist/modules/communication_channels/data/enrichers.js +4 -4
- package/dist/modules/communication_channels/data/enrichers.js.map +2 -2
- package/dist/modules/customers/analytics.js +1 -0
- package/dist/modules/customers/analytics.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.js +17 -3
- package/dist/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js +7 -1
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
- package/dist/modules/customers/components/detail/DealForm.js +2 -1
- package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
- package/dist/modules/customers/components/detail/DealsSection.js +1 -1
- package/dist/modules/customers/components/detail/DealsSection.js.map +2 -2
- package/dist/modules/customers/data/validators.js +3 -1
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/config.js +52 -2
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/config.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js +42 -26
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.js +2 -2
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.js.map +2 -2
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/route.js +34 -12
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/route.js.map +2 -2
- package/dist/modules/dictionaries/api/openapi.js +15 -2
- package/dist/modules/dictionaries/api/openapi.js.map +2 -2
- package/dist/modules/dictionaries/components/hooks/useDictionaryEntries.js +30 -29
- package/dist/modules/dictionaries/components/hooks/useDictionaryEntries.js.map +2 -2
- package/dist/modules/dictionaries/data/validators.js +7 -0
- package/dist/modules/dictionaries/data/validators.js.map +2 -2
- package/dist/modules/dictionaries/lib/clientEntries.js +4 -3
- package/dist/modules/dictionaries/lib/clientEntries.js.map +2 -2
- package/dist/modules/dictionaries/lib/fetchAllEntries.js +43 -0
- package/dist/modules/dictionaries/lib/fetchAllEntries.js.map +7 -0
- package/dist/modules/entities/api/definitions.js +51 -3
- package/dist/modules/entities/api/definitions.js.map +2 -2
- package/dist/modules/entities/api/entities.js +21 -2
- package/dist/modules/entities/api/entities.js.map +2 -2
- package/dist/modules/entities/api/records.js +3 -24
- package/dist/modules/entities/api/records.js.map +2 -2
- package/dist/modules/entities/api/sidebar-entities.js +34 -12
- package/dist/modules/entities/api/sidebar-entities.js.map +2 -2
- package/dist/modules/entities/lib/entityAcl.js +46 -0
- package/dist/modules/entities/lib/entityAcl.js.map +2 -2
- package/dist/modules/messages/api/route.js +2 -5
- package/dist/modules/messages/api/route.js.map +2 -2
- package/dist/modules/messages/lib/participantScope.js +15 -0
- package/dist/modules/messages/lib/participantScope.js.map +7 -0
- package/dist/modules/planner/components/unavailabilityReasons.js +3 -3
- package/dist/modules/planner/components/unavailabilityReasons.js.map +2 -2
- package/dist/modules/progress/lib/progressService.js +2 -0
- package/dist/modules/progress/lib/progressService.js.map +2 -2
- package/dist/modules/progress/lib/progressServiceImpl.js +322 -104
- package/dist/modules/progress/lib/progressServiceImpl.js.map +2 -2
- package/dist/modules/resources/api/resources.js +2 -0
- package/dist/modules/resources/api/resources.js.map +2 -2
- package/dist/modules/resources/backend/resources/resources/[id]/page.js +3 -2
- package/dist/modules/resources/backend/resources/resources/[id]/page.js.map +2 -2
- package/dist/modules/resources/commands/resources.js +9 -0
- package/dist/modules/resources/commands/resources.js.map +2 -2
- package/dist/modules/resources/components/detail/dictionaries.js +3 -3
- package/dist/modules/resources/components/detail/dictionaries.js.map +2 -2
- package/dist/modules/resources/data/entities.js +3 -0
- package/dist/modules/resources/data/entities.js.map +2 -2
- package/dist/modules/resources/data/validators.js +6 -2
- package/dist/modules/resources/data/validators.js.map +2 -2
- package/dist/modules/resources/lib/seeds.js +16 -9
- package/dist/modules/resources/lib/seeds.js.map +2 -2
- package/dist/modules/resources/migrations/Migration20260608231000.js +13 -0
- package/dist/modules/resources/migrations/Migration20260608231000.js.map +7 -0
- package/dist/modules/sales/backend/sales/documents/create/page.js +11 -14
- package/dist/modules/sales/backend/sales/documents/create/page.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +8 -0
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/components/documents/ItemsSection.js +18 -9
- package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/LineItemDialog.js +12 -5
- package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/SalesDocumentForm.js +48 -2
- package/dist/modules/sales/components/documents/SalesDocumentForm.js.map +2 -2
- package/dist/modules/sales/components/documents/SalesOrderDraftLines.js +167 -0
- package/dist/modules/sales/components/documents/SalesOrderDraftLines.js.map +7 -0
- package/dist/modules/sales/data/validators.js +5 -1
- package/dist/modules/sales/data/validators.js.map +2 -2
- package/dist/modules/staff/components/detail/dictionaries.js +3 -3
- package/dist/modules/staff/components/detail/dictionaries.js.map +2 -2
- package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js +11 -4
- package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js.map +2 -2
- package/generated/entities/resources_resource/index.ts +1 -0
- package/generated/entity-fields-registry.ts +1 -0
- package/package.json +7 -7
- package/src/helpers/integration/salesFixtures.ts +34 -2
- package/src/helpers/integration/salesUi.ts +8 -0
- package/src/modules/communication_channels/data/enrichers.ts +6 -26
- package/src/modules/customers/analytics.ts +1 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.tsx +35 -3
- package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +10 -1
- package/src/modules/customers/components/detail/DealForm.tsx +5 -1
- package/src/modules/customers/components/detail/DealsSection.tsx +1 -1
- package/src/modules/customers/data/validators.ts +5 -1
- package/src/modules/dashboards/i18n/de.json +3 -0
- package/src/modules/dashboards/i18n/en.json +3 -0
- package/src/modules/dashboards/i18n/es.json +3 -0
- package/src/modules/dashboards/i18n/pl.json +3 -0
- package/src/modules/dashboards/widgets/dashboard/pipeline-summary/config.ts +86 -0
- package/src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.tsx +33 -18
- package/src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.ts +2 -2
- package/src/modules/dictionaries/api/[dictionaryId]/entries/route.ts +39 -11
- package/src/modules/dictionaries/api/openapi.ts +19 -0
- package/src/modules/dictionaries/components/hooks/useDictionaryEntries.ts +35 -36
- package/src/modules/dictionaries/data/validators.ts +14 -0
- package/src/modules/dictionaries/lib/clientEntries.ts +4 -3
- package/src/modules/dictionaries/lib/fetchAllEntries.ts +72 -0
- package/src/modules/entities/api/definitions.ts +51 -3
- package/src/modules/entities/api/entities.ts +30 -2
- package/src/modules/entities/api/records.ts +3 -27
- package/src/modules/entities/api/sidebar-entities.ts +43 -14
- package/src/modules/entities/lib/entityAcl.ts +58 -0
- package/src/modules/messages/api/route.ts +4 -5
- package/src/modules/messages/lib/participantScope.ts +64 -0
- package/src/modules/planner/components/unavailabilityReasons.ts +3 -3
- package/src/modules/progress/AGENTS.md +11 -1
- package/src/modules/progress/lib/progressService.ts +1 -0
- package/src/modules/progress/lib/progressServiceImpl.ts +385 -117
- package/src/modules/resources/api/resources.ts +2 -0
- package/src/modules/resources/backend/resources/resources/[id]/page.tsx +9 -3
- package/src/modules/resources/commands/resources.ts +10 -0
- package/src/modules/resources/components/detail/dictionaries.ts +3 -3
- package/src/modules/resources/data/entities.ts +3 -0
- package/src/modules/resources/data/validators.ts +5 -0
- package/src/modules/resources/lib/seeds.ts +16 -9
- package/src/modules/resources/migrations/.snapshot-open-mercato.json +17 -1
- package/src/modules/resources/migrations/Migration20260608231000.ts +13 -0
- package/src/modules/sales/backend/sales/documents/create/page.tsx +2 -4
- package/src/modules/sales/commands/documents.ts +8 -0
- package/src/modules/sales/components/documents/ItemsSection.tsx +19 -10
- package/src/modules/sales/components/documents/LineItemDialog.tsx +13 -5
- package/src/modules/sales/components/documents/SalesDocumentForm.tsx +48 -6
- package/src/modules/sales/components/documents/SalesOrderDraftLines.tsx +198 -0
- package/src/modules/sales/data/validators.ts +7 -1
- package/src/modules/sales/i18n/de.json +2 -0
- package/src/modules/sales/i18n/en.json +2 -0
- package/src/modules/sales/i18n/es.json +2 -0
- package/src/modules/sales/i18n/pl.json +2 -0
- package/src/modules/staff/components/detail/dictionaries.ts +3 -3
- package/src/modules/workflows/widgets/injection/order-approval/widget.client.tsx +12 -5
|
@@ -1,11 +1,58 @@
|
|
|
1
1
|
import { type DateRangePreset, isValidDateRangePreset } from '@open-mercato/ui/backend/date-range'
|
|
2
|
+
import type { WidgetDataRequest } from '../../../services/widgetDataService'
|
|
3
|
+
|
|
4
|
+
// Deal statuses that mean the deal is closed and must stop counting as current pipeline.
|
|
5
|
+
//
|
|
6
|
+
// This is a denylist, not an allowlist: `customer_deals.status` is a lenient `text` column
|
|
7
|
+
// fed by the per-tenant `deal_status` dictionary, so a status unknown to code counts as OPEN
|
|
8
|
+
// and keeps contributing to the chart. That keeps tenant-specific stages visible and mirrors
|
|
9
|
+
// `customers/lib/interactionStatus.ts`, which treats an unknown interaction status as open.
|
|
10
|
+
//
|
|
11
|
+
// Every vocabulary that reaches the column has to be listed here:
|
|
12
|
+
// - `win` / `loose` are written by the deal closure UI and the kanban board.
|
|
13
|
+
// - `won` / `lost` are written verbatim by the `customers.update_deal_stage` AI tool, whose
|
|
14
|
+
// free-form `toStage` is passed straight through to `status`.
|
|
15
|
+
// - `closed` is a seeded `deal_status` dictionary value, persisted by the dashboards
|
|
16
|
+
// analytics seed and treated as terminal by the demo-data generator.
|
|
17
|
+
export const CLOSED_DEAL_STATUSES = ['win', 'loose', 'won', 'lost', 'closed'] as const
|
|
18
|
+
|
|
19
|
+
// A deal is also closed when `closure_outcome` records the outcome, which the deals-summary
|
|
20
|
+
// KPI already treats as terminal: `api/deals/summary/route.ts` counts a deal as won or lost
|
|
21
|
+
// when EITHER `status` OR `closure_outcome` says so. The two columns can disagree — the CRUD
|
|
22
|
+
// API accepts `closureOutcome` on its own, and the AI tool writes `status` without ever setting
|
|
23
|
+
// it — so the chart has to test both signals to agree with those KPI cards (#4668).
|
|
24
|
+
//
|
|
25
|
+
// The `closure_outcome` side is expressed as `IS NULL` rather than as a `neq` denylist for two
|
|
26
|
+
// reasons, and swapping it for `neq` would be a silent regression:
|
|
27
|
+
// - `closure_outcome` is nullable, and every OPEN deal holds NULL there. `neq` renders as
|
|
28
|
+
// `column != ?`, and in SQL `NULL != 'won'` is NULL, not true — a `neq` filter would drop
|
|
29
|
+
// every open deal and empty the chart. `status` has no such problem: it is `text not null
|
|
30
|
+
// default 'open'`, which is why the status side stays a denylist.
|
|
31
|
+
// - Unlike `status`, this column is not fed by a per-tenant dictionary. Every write path
|
|
32
|
+
// validates it against the same closed `z.enum(['won', 'lost'])` (`data/validators.ts`,
|
|
33
|
+
// `api/deals/[id]/route.ts`, `api/deals/[id]/stats/route.ts`), so a non-null value always
|
|
34
|
+
// means closed and there is no tenant-specific vocabulary to keep visible.
|
|
35
|
+
export const OPEN_DEAL_CLOSURE_OUTCOME_FILTER = {
|
|
36
|
+
field: 'closureOutcome',
|
|
37
|
+
operator: 'is_null',
|
|
38
|
+
} as const
|
|
39
|
+
|
|
40
|
+
export const PIPELINE_STATUS_SCOPES = ['open', 'all'] as const
|
|
41
|
+
|
|
42
|
+
export type PipelineStatusScope = typeof PIPELINE_STATUS_SCOPES[number]
|
|
2
43
|
|
|
3
44
|
export type PipelineSummarySettings = {
|
|
4
45
|
dateRange: DateRangePreset
|
|
46
|
+
statusScope: PipelineStatusScope
|
|
5
47
|
}
|
|
6
48
|
|
|
7
49
|
export const DEFAULT_SETTINGS: PipelineSummarySettings = {
|
|
8
50
|
dateRange: 'this_month',
|
|
51
|
+
statusScope: 'open',
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isValidStatusScope(value: unknown): value is PipelineStatusScope {
|
|
55
|
+
return typeof value === 'string' && (PIPELINE_STATUS_SCOPES as readonly string[]).includes(value)
|
|
9
56
|
}
|
|
10
57
|
|
|
11
58
|
export function hydrateSettings(raw: unknown): PipelineSummarySettings {
|
|
@@ -13,5 +60,44 @@ export function hydrateSettings(raw: unknown): PipelineSummarySettings {
|
|
|
13
60
|
const obj = raw as Record<string, unknown>
|
|
14
61
|
return {
|
|
15
62
|
dateRange: isValidDateRangePreset(obj.dateRange) ? obj.dateRange : DEFAULT_SETTINGS.dateRange,
|
|
63
|
+
statusScope: isValidStatusScope(obj.statusScope) ? obj.statusScope : DEFAULT_SETTINGS.statusScope,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function dehydrateSettings(settings: PipelineSummarySettings): Record<string, unknown> {
|
|
68
|
+
return {
|
|
69
|
+
dateRange: settings.dateRange,
|
|
70
|
+
statusScope: settings.statusScope,
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function buildPipelineDataRequest(settings: PipelineSummarySettings): WidgetDataRequest {
|
|
75
|
+
const request: WidgetDataRequest = {
|
|
76
|
+
entityType: 'customers:deals',
|
|
77
|
+
metric: {
|
|
78
|
+
field: 'valueAmount',
|
|
79
|
+
aggregate: 'sum',
|
|
80
|
+
},
|
|
81
|
+
groupBy: {
|
|
82
|
+
field: 'pipelineStage',
|
|
83
|
+
resolveLabels: true,
|
|
84
|
+
},
|
|
85
|
+
dateRange: {
|
|
86
|
+
field: 'createdAt',
|
|
87
|
+
preset: settings.dateRange,
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (settings.statusScope === 'open') {
|
|
92
|
+
request.filters = [
|
|
93
|
+
...CLOSED_DEAL_STATUSES.map((status) => ({
|
|
94
|
+
field: 'status',
|
|
95
|
+
operator: 'neq' as const,
|
|
96
|
+
value: status,
|
|
97
|
+
})),
|
|
98
|
+
{ ...OPEN_DEAL_CLOSURE_OUTCOME_FILTER },
|
|
99
|
+
]
|
|
16
100
|
}
|
|
101
|
+
|
|
102
|
+
return request
|
|
17
103
|
}
|
|
@@ -10,7 +10,14 @@ import {
|
|
|
10
10
|
InlineDateRangeSelect,
|
|
11
11
|
type DateRangePreset,
|
|
12
12
|
} from '@open-mercato/ui/backend/date-range'
|
|
13
|
-
import {
|
|
13
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@open-mercato/ui/primitives/select'
|
|
14
|
+
import {
|
|
15
|
+
DEFAULT_SETTINGS,
|
|
16
|
+
buildPipelineDataRequest,
|
|
17
|
+
hydrateSettings,
|
|
18
|
+
type PipelineStatusScope,
|
|
19
|
+
type PipelineSummarySettings,
|
|
20
|
+
} from './config'
|
|
14
21
|
import type { WidgetDataResponse } from '../../../services/widgetDataService'
|
|
15
22
|
import { formatCurrencyCompact } from '../../../lib/formatters'
|
|
16
23
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
@@ -18,23 +25,7 @@ import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
|
18
25
|
const logger = createLogger('dashboards').child({ component: 'pipeline-summary' })
|
|
19
26
|
|
|
20
27
|
async function fetchPipelineData(settings: PipelineSummarySettings, fetchWidgetData: WidgetDataFetcher): Promise<WidgetDataResponse> {
|
|
21
|
-
|
|
22
|
-
entityType: 'customers:deals',
|
|
23
|
-
metric: {
|
|
24
|
-
field: 'valueAmount',
|
|
25
|
-
aggregate: 'sum',
|
|
26
|
-
},
|
|
27
|
-
groupBy: {
|
|
28
|
-
field: 'pipelineStage',
|
|
29
|
-
resolveLabels: true,
|
|
30
|
-
},
|
|
31
|
-
dateRange: {
|
|
32
|
-
field: 'createdAt',
|
|
33
|
-
preset: settings.dateRange,
|
|
34
|
-
},
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return fetchWidgetData<WidgetDataResponse>(body)
|
|
28
|
+
return fetchWidgetData<WidgetDataResponse>(buildPipelineDataRequest(settings))
|
|
38
29
|
}
|
|
39
30
|
|
|
40
31
|
function formatStageLabel(stage: unknown, t: (key: string, fallback: string) => string): string {
|
|
@@ -97,6 +88,30 @@ const PipelineSummaryWidget: React.FC<DashboardWidgetComponentProps<PipelineSumm
|
|
|
97
88
|
value={hydrated.dateRange}
|
|
98
89
|
onChange={(dateRange: DateRangePreset) => onSettingsChange({ ...hydrated, dateRange })}
|
|
99
90
|
/>
|
|
91
|
+
<div className="space-y-1.5">
|
|
92
|
+
<label
|
|
93
|
+
htmlFor="pipeline-summary-status-scope"
|
|
94
|
+
className="text-xs font-semibold uppercase text-muted-foreground"
|
|
95
|
+
>
|
|
96
|
+
{t('dashboards.analytics.settings.dealStatusScope', 'Deals included')}
|
|
97
|
+
</label>
|
|
98
|
+
<Select
|
|
99
|
+
value={hydrated.statusScope}
|
|
100
|
+
onValueChange={(value) => onSettingsChange({ ...hydrated, statusScope: value as PipelineStatusScope })}
|
|
101
|
+
>
|
|
102
|
+
<SelectTrigger id="pipeline-summary-status-scope" size="sm">
|
|
103
|
+
<SelectValue />
|
|
104
|
+
</SelectTrigger>
|
|
105
|
+
<SelectContent>
|
|
106
|
+
<SelectItem value="open">
|
|
107
|
+
{t('dashboards.analytics.settings.dealStatusScopeOpen', 'Open deals only')}
|
|
108
|
+
</SelectItem>
|
|
109
|
+
<SelectItem value="all">
|
|
110
|
+
{t('dashboards.analytics.settings.dealStatusScopeAll', 'All deals, including won and lost')}
|
|
111
|
+
</SelectItem>
|
|
112
|
+
</SelectContent>
|
|
113
|
+
</Select>
|
|
114
|
+
</div>
|
|
100
115
|
</div>
|
|
101
116
|
)
|
|
102
117
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { lazyDashboardWidget, type DashboardWidgetModule } from '@open-mercato/shared/modules/dashboard/widgets'
|
|
2
|
-
import { DEFAULT_SETTINGS, hydrateSettings, type PipelineSummarySettings } from './config'
|
|
2
|
+
import { DEFAULT_SETTINGS, dehydrateSettings, hydrateSettings, type PipelineSummarySettings } from './config'
|
|
3
3
|
const PipelineSummaryWidget = lazyDashboardWidget(() => import('./widget.client'))
|
|
4
4
|
|
|
5
5
|
const widget: DashboardWidgetModule<PipelineSummarySettings> = {
|
|
@@ -18,7 +18,7 @@ const widget: DashboardWidgetModule<PipelineSummarySettings> = {
|
|
|
18
18
|
},
|
|
19
19
|
Widget: PipelineSummaryWidget,
|
|
20
20
|
hydrateSettings,
|
|
21
|
-
dehydrateSettings
|
|
21
|
+
dehydrateSettings,
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export default widget
|
|
@@ -3,7 +3,11 @@ import { z } from 'zod'
|
|
|
3
3
|
import { runWithCacheTenant } from '@open-mercato/cache'
|
|
4
4
|
import { Dictionary, DictionaryEntry } from '@open-mercato/core/modules/dictionaries/data/entities'
|
|
5
5
|
import { resolveDictionariesRouteContext, resolveDictionaryActorId } from '@open-mercato/core/modules/dictionaries/api/context'
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
createDictionaryEntrySchema,
|
|
8
|
+
listDictionaryEntriesQuerySchema,
|
|
9
|
+
DICTIONARY_ENTRIES_MAX_LIMIT,
|
|
10
|
+
} from '@open-mercato/core/modules/dictionaries/data/validators'
|
|
7
11
|
import { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
8
12
|
import {
|
|
9
13
|
runCrudMutationGuardAfterSuccess,
|
|
@@ -14,6 +18,7 @@ import { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/op
|
|
|
14
18
|
import type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
15
19
|
import {
|
|
16
20
|
createDictionaryEntrySchema as createEntryDocSchema,
|
|
21
|
+
dictionaryEntriesQuerySchema,
|
|
17
22
|
dictionaryEntryListResponseSchema,
|
|
18
23
|
dictionaryEntryResponseSchema,
|
|
19
24
|
dictionaryIdParamsSchema,
|
|
@@ -45,8 +50,10 @@ function buildEntriesCacheKey(params: {
|
|
|
45
50
|
dictionaryId: string
|
|
46
51
|
organizationId: string | null
|
|
47
52
|
sortMode: string
|
|
53
|
+
limit: number
|
|
54
|
+
offset: number
|
|
48
55
|
}): string {
|
|
49
|
-
return `dictionaries:entries:${params.dictionaryId}:org=${params.organizationId ?? 'null'}:sort=${params.sortMode}`
|
|
56
|
+
return `dictionaries:entries:${params.dictionaryId}:org=${params.organizationId ?? 'null'}:sort=${params.sortMode}:limit=${params.limit}:offset=${params.offset}`
|
|
50
57
|
}
|
|
51
58
|
|
|
52
59
|
async function loadDictionary(
|
|
@@ -93,6 +100,11 @@ export async function GET(req: Request, ctx: { params?: { dictionaryId?: string
|
|
|
93
100
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
94
101
|
}
|
|
95
102
|
const { dictionaryId } = paramsSchema.parse({ dictionaryId: ctx.params?.dictionaryId })
|
|
103
|
+
const requestUrl = new URL(req.url)
|
|
104
|
+
const { limit, offset } = listDictionaryEntriesQuerySchema.parse({
|
|
105
|
+
limit: requestUrl.searchParams.get('limit') ?? undefined,
|
|
106
|
+
offset: requestUrl.searchParams.get('offset') ?? undefined,
|
|
107
|
+
})
|
|
96
108
|
const dictionary = await loadDictionary(context, dictionaryId, { allowInherited: true })
|
|
97
109
|
const dictionaryTenantId = dictionary.tenantId
|
|
98
110
|
const dictionaryOrgId = dictionary.organizationId ?? null
|
|
@@ -102,9 +114,12 @@ export async function GET(req: Request, ctx: { params?: { dictionaryId?: string
|
|
|
102
114
|
// decrypted+sorted options payload. The entry writes flow through the
|
|
103
115
|
// command bus (resourceKind dictionaries.entry), which already flushes the
|
|
104
116
|
// matching collection tag post-commit — no new invalidation wiring needed.
|
|
105
|
-
|
|
117
|
+
// Only the default page is cached: keying the cache on caller-supplied
|
|
118
|
+
// pagination would let any reader mint unbounded distinct cache entries.
|
|
119
|
+
const isDefaultPage = limit === DICTIONARY_ENTRIES_MAX_LIMIT && offset === 0
|
|
120
|
+
const cache = isCrudCacheEnabled() && isDefaultPage ? resolveCrudCache(context.container) : null
|
|
106
121
|
const cacheKey = cache
|
|
107
|
-
? buildEntriesCacheKey({ dictionaryId, organizationId: dictionaryOrgId, sortMode })
|
|
122
|
+
? buildEntriesCacheKey({ dictionaryId, organizationId: dictionaryOrgId, sortMode, limit, offset })
|
|
108
123
|
: null
|
|
109
124
|
|
|
110
125
|
if (cache && cacheKey) {
|
|
@@ -114,15 +129,19 @@ export async function GET(req: Request, ctx: { params?: { dictionaryId?: string
|
|
|
114
129
|
}
|
|
115
130
|
}
|
|
116
131
|
|
|
132
|
+
const entriesFilter = {
|
|
133
|
+
dictionary,
|
|
134
|
+
organizationId: dictionary.organizationId,
|
|
135
|
+
tenantId: dictionary.tenantId,
|
|
136
|
+
}
|
|
137
|
+
const total = await context.em.count(DictionaryEntry, entriesFilter)
|
|
138
|
+
// Bound the decrypt+sort work per request: page membership is decided by a
|
|
139
|
+
// stable database ordering, and the dictionary's sort mode orders the page.
|
|
117
140
|
const entries = await findWithDecryption(
|
|
118
141
|
context.em,
|
|
119
142
|
DictionaryEntry,
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
organizationId: dictionary.organizationId,
|
|
123
|
-
tenantId: dictionary.tenantId,
|
|
124
|
-
},
|
|
125
|
-
{},
|
|
143
|
+
entriesFilter,
|
|
144
|
+
{ orderBy: { position: 'asc', id: 'asc' }, limit, offset },
|
|
126
145
|
{ tenantId: dictionary.tenantId, organizationId: dictionary.organizationId },
|
|
127
146
|
)
|
|
128
147
|
const sortedEntries = sortDictionaryEntries(entries, sortMode)
|
|
@@ -139,6 +158,14 @@ export async function GET(req: Request, ctx: { params?: { dictionaryId?: string
|
|
|
139
158
|
createdAt: entry.createdAt,
|
|
140
159
|
updatedAt: entry.updatedAt,
|
|
141
160
|
})),
|
|
161
|
+
total,
|
|
162
|
+
limit,
|
|
163
|
+
offset,
|
|
164
|
+
hasMore: offset + sortedEntries.length < total,
|
|
165
|
+
// Page membership comes from the database ordering, while the page itself
|
|
166
|
+
// is ordered in memory. A client assembling several pages has to re-apply
|
|
167
|
+
// the same comparator, so it needs the mode that produced this ordering.
|
|
168
|
+
sortMode,
|
|
142
169
|
}
|
|
143
170
|
|
|
144
171
|
if (cache && cacheKey) {
|
|
@@ -265,8 +292,9 @@ export async function POST(req: Request, ctx: { params?: { dictionaryId?: string
|
|
|
265
292
|
|
|
266
293
|
const dictionaryEntriesGetDoc: OpenApiMethodDoc = {
|
|
267
294
|
summary: 'List dictionary entries',
|
|
268
|
-
description:
|
|
295
|
+
description: `Returns entries for the specified dictionary ordered by its configured entry sort mode. The number of entries returned per request is capped at ${DICTIONARY_ENTRIES_MAX_LIMIT}; use \`limit\` and \`offset\` with the \`total\`/\`hasMore\` response fields to page through larger dictionaries.`,
|
|
269
296
|
tags: [dictionariesTag],
|
|
297
|
+
query: dictionaryEntriesQuerySchema,
|
|
270
298
|
responses: [
|
|
271
299
|
{ status: 200, description: 'Dictionary entries.', schema: dictionaryEntryListResponseSchema },
|
|
272
300
|
],
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
updateDictionaryEntrySchema,
|
|
6
6
|
reorderDictionaryEntriesSchema,
|
|
7
7
|
setDefaultDictionaryEntrySchema,
|
|
8
|
+
DICTIONARY_ENTRIES_MAX_LIMIT,
|
|
8
9
|
} from '@open-mercato/core/modules/dictionaries/data/validators'
|
|
9
10
|
import { dictionaryEntrySortModeSchema } from '@open-mercato/core/modules/dictionaries/lib/entrySort'
|
|
10
11
|
|
|
@@ -57,6 +58,24 @@ export const dictionaryEntrySchema = z.object({
|
|
|
57
58
|
|
|
58
59
|
export const dictionaryEntryListResponseSchema = z.object({
|
|
59
60
|
items: z.array(dictionaryEntrySchema),
|
|
61
|
+
total: z.number().int(),
|
|
62
|
+
limit: z.number().int(),
|
|
63
|
+
offset: z.number().int(),
|
|
64
|
+
hasMore: z.boolean(),
|
|
65
|
+
sortMode: dictionaryEntrySortModeSchema.describe(
|
|
66
|
+
'Sort mode that ordered this page. Clients assembling multiple pages must re-apply it across the combined set.',
|
|
67
|
+
),
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
export const dictionaryEntriesQuerySchema = z.object({
|
|
71
|
+
limit: z.coerce
|
|
72
|
+
.number()
|
|
73
|
+
.int()
|
|
74
|
+
.positive()
|
|
75
|
+
.max(DICTIONARY_ENTRIES_MAX_LIMIT)
|
|
76
|
+
.optional()
|
|
77
|
+
.describe(`Maximum entries to return. Defaults to and is capped at ${DICTIONARY_ENTRIES_MAX_LIMIT}.`),
|
|
78
|
+
offset: z.coerce.number().int().min(0).optional().describe('Number of entries to skip. Defaults to 0.'),
|
|
60
79
|
})
|
|
61
80
|
|
|
62
81
|
export const dictionaryEntryResponseSchema = dictionaryEntrySchema
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
4
3
|
import { useQuery, type QueryClient, type UseQueryResult } from '@tanstack/react-query'
|
|
5
4
|
import {
|
|
6
5
|
createDictionaryMap,
|
|
@@ -8,6 +7,7 @@ import {
|
|
|
8
7
|
type DictionaryDisplayEntry,
|
|
9
8
|
type DictionaryMap,
|
|
10
9
|
} from '../dictionaryAppearance'
|
|
10
|
+
import { fetchAllDictionaryEntries } from '../../lib/fetchAllEntries'
|
|
11
11
|
|
|
12
12
|
export type DictionaryEntryRecord = DictionaryDisplayEntry & {
|
|
13
13
|
id: string
|
|
@@ -28,47 +28,46 @@ const BASE_DICTIONARY_ENTRIES_KEY = ['dictionaries', 'entries'] as const
|
|
|
28
28
|
export const dictionaryEntriesQueryKey = (dictionaryId: string, scopeVersion = 0) =>
|
|
29
29
|
[...BASE_DICTIONARY_ENTRIES_KEY, dictionaryId, `scope:${scopeVersion}`] as const
|
|
30
30
|
|
|
31
|
+
function parseDictionaryEntryItems(items: unknown[]): DictionaryEntryRecord[] {
|
|
32
|
+
const parsed: DictionaryEntryRecord[] = []
|
|
33
|
+
for (const item of items) {
|
|
34
|
+
if (!item || typeof item !== 'object') continue
|
|
35
|
+
const data = item as Record<string, unknown>
|
|
36
|
+
const id = typeof data.id === 'string' ? data.id : ''
|
|
37
|
+
const value = typeof data.value === 'string' ? data.value.trim() : ''
|
|
38
|
+
if (!id || !value) continue
|
|
39
|
+
const label =
|
|
40
|
+
typeof data.label === 'string' && data.label.trim().length ? data.label.trim() : value
|
|
41
|
+
const color =
|
|
42
|
+
typeof data.color === 'string' && /^#([0-9a-fA-F]{6})$/.test(data.color)
|
|
43
|
+
? `#${data.color.slice(1).toLowerCase()}`
|
|
44
|
+
: null
|
|
45
|
+
const icon = typeof data.icon === 'string' && data.icon.trim().length ? data.icon.trim() : null
|
|
46
|
+
const createdAt = typeof data.createdAt === 'string' ? data.createdAt : null
|
|
47
|
+
const updatedAt = typeof data.updatedAt === 'string' ? data.updatedAt : null
|
|
48
|
+
parsed.push({
|
|
49
|
+
id,
|
|
50
|
+
value,
|
|
51
|
+
label,
|
|
52
|
+
color,
|
|
53
|
+
icon,
|
|
54
|
+
createdAt,
|
|
55
|
+
updatedAt,
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
return parsed
|
|
59
|
+
}
|
|
60
|
+
|
|
31
61
|
export const dictionaryEntriesQueryOptions = (dictionaryId: string, scopeVersion = 0) => ({
|
|
32
62
|
queryKey: dictionaryEntriesQueryKey(dictionaryId, scopeVersion),
|
|
33
63
|
staleTime: DICTIONARY_ENTRIES_STALE_TIME,
|
|
34
64
|
gcTime: DICTIONARY_ENTRIES_STALE_TIME,
|
|
35
65
|
queryFn: async (): Promise<DictionaryEntriesQueryData> => {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const message =
|
|
40
|
-
typeof payload.error === 'string'
|
|
41
|
-
? payload.error
|
|
42
|
-
: 'Failed to load dictionary entries.'
|
|
43
|
-
throw new Error(message)
|
|
44
|
-
}
|
|
45
|
-
const items = Array.isArray(payload.items) ? payload.items : []
|
|
46
|
-
const parsed: DictionaryEntryRecord[] = []
|
|
47
|
-
for (const item of items) {
|
|
48
|
-
if (!item || typeof item !== 'object') continue
|
|
49
|
-
const data = item as Record<string, unknown>
|
|
50
|
-
const id = typeof data.id === 'string' ? data.id : ''
|
|
51
|
-
const value = typeof data.value === 'string' ? data.value.trim() : ''
|
|
52
|
-
if (!id || !value) continue
|
|
53
|
-
const label =
|
|
54
|
-
typeof data.label === 'string' && data.label.trim().length ? data.label.trim() : value
|
|
55
|
-
const color =
|
|
56
|
-
typeof data.color === 'string' && /^#([0-9a-fA-F]{6})$/.test(data.color)
|
|
57
|
-
? `#${data.color.slice(1).toLowerCase()}`
|
|
58
|
-
: null
|
|
59
|
-
const icon = typeof data.icon === 'string' && data.icon.trim().length ? data.icon.trim() : null
|
|
60
|
-
const createdAt = typeof data.createdAt === 'string' ? data.createdAt : null
|
|
61
|
-
const updatedAt = typeof data.updatedAt === 'string' ? data.updatedAt : null
|
|
62
|
-
parsed.push({
|
|
63
|
-
id,
|
|
64
|
-
value,
|
|
65
|
-
label,
|
|
66
|
-
color,
|
|
67
|
-
icon,
|
|
68
|
-
createdAt,
|
|
69
|
-
updatedAt,
|
|
70
|
-
})
|
|
66
|
+
const pages = await fetchAllDictionaryEntries(dictionaryId)
|
|
67
|
+
if (!pages.ok) {
|
|
68
|
+
throw new Error(pages.errorMessage ?? 'Failed to load dictionary entries.')
|
|
71
69
|
}
|
|
70
|
+
const parsed = parseDictionaryEntryItems(pages.items)
|
|
72
71
|
const normalized = normalizeDictionaryEntries(
|
|
73
72
|
parsed.map(({ value, label, color, icon }) => ({ value, label, color, icon })),
|
|
74
73
|
{ sort: false },
|
|
@@ -26,6 +26,20 @@ export const upsertDictionarySchema = z.object({
|
|
|
26
26
|
|
|
27
27
|
export type UpsertDictionaryInput = z.infer<typeof upsertDictionarySchema>
|
|
28
28
|
|
|
29
|
+
export const DICTIONARY_ENTRIES_MAX_LIMIT = 500
|
|
30
|
+
|
|
31
|
+
export const listDictionaryEntriesQuerySchema = z.object({
|
|
32
|
+
limit: z.coerce
|
|
33
|
+
.number()
|
|
34
|
+
.int()
|
|
35
|
+
.positive()
|
|
36
|
+
.max(DICTIONARY_ENTRIES_MAX_LIMIT)
|
|
37
|
+
.catch(DICTIONARY_ENTRIES_MAX_LIMIT),
|
|
38
|
+
offset: z.coerce.number().int().min(0).catch(0),
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
export type ListDictionaryEntriesQuery = z.infer<typeof listDictionaryEntriesQuerySchema>
|
|
42
|
+
|
|
29
43
|
export const createDictionaryEntrySchema = z.object({
|
|
30
44
|
value: z.string().trim().min(1).max(150),
|
|
31
45
|
label: z.string().trim().min(1).max(150).optional(),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
4
|
+
import { fetchAllDictionaryEntries } from './fetchAllEntries'
|
|
4
5
|
|
|
5
6
|
type DictionarySummary = {
|
|
6
7
|
id: string
|
|
@@ -59,8 +60,8 @@ export async function loadDictionaryEntriesByKey(key: string): Promise<Dictionar
|
|
|
59
60
|
const dictionary = normalizeDictionaryList(dictionariesCall.result?.items).find((item) => item.key === key)
|
|
60
61
|
if (!dictionary) return []
|
|
61
62
|
|
|
62
|
-
const
|
|
63
|
-
if (!
|
|
63
|
+
const entries = await fetchAllDictionaryEntries(dictionary.id)
|
|
64
|
+
if (!entries.ok) return []
|
|
64
65
|
|
|
65
|
-
return normalizeDictionaryEntries(
|
|
66
|
+
return normalizeDictionaryEntries(entries.items)
|
|
66
67
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { fetchAllOffsetPages } from '@open-mercato/ui/backend/utils/fetchAllPages'
|
|
4
|
+
import {
|
|
5
|
+
resolveDictionaryEntrySortMode,
|
|
6
|
+
sortDictionaryEntries,
|
|
7
|
+
type DictionaryEntrySortItem,
|
|
8
|
+
} from './entrySort'
|
|
9
|
+
|
|
10
|
+
export type DictionaryEntryItem = Record<string, unknown>
|
|
11
|
+
|
|
12
|
+
export type DictionaryEntriesResult = {
|
|
13
|
+
ok: boolean
|
|
14
|
+
items: DictionaryEntryItem[]
|
|
15
|
+
pageCount: number
|
|
16
|
+
errorMessage: string | null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type DictionaryEntrySortView = DictionaryEntrySortItem & { entry: DictionaryEntryItem }
|
|
20
|
+
|
|
21
|
+
function toRecords(items: unknown[]): DictionaryEntryItem[] {
|
|
22
|
+
return items.filter((item): item is DictionaryEntryItem => !!item && typeof item === 'object')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function toSortView(entry: DictionaryEntryItem): DictionaryEntrySortView {
|
|
26
|
+
const value = typeof entry.value === 'string' ? entry.value.trim() : ''
|
|
27
|
+
const label = typeof entry.label === 'string' ? entry.label.trim() : ''
|
|
28
|
+
return {
|
|
29
|
+
entry,
|
|
30
|
+
id: typeof entry.id === 'string' ? entry.id : null,
|
|
31
|
+
value: value.length ? value : null,
|
|
32
|
+
label: label.length ? label : null,
|
|
33
|
+
createdAt: typeof entry.createdAt === 'string' ? entry.createdAt : null,
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Load every entry of a dictionary, walking the entries route's page cap.
|
|
39
|
+
*
|
|
40
|
+
* The route bounds each response, so any consumer that needs the full option
|
|
41
|
+
* list must page — reading `items` from a single request silently drops
|
|
42
|
+
* everything past the ceiling and makes those values unselectable.
|
|
43
|
+
*/
|
|
44
|
+
export async function fetchAllDictionaryEntries(
|
|
45
|
+
dictionaryId: string,
|
|
46
|
+
): Promise<DictionaryEntriesResult> {
|
|
47
|
+
const pages = await fetchAllOffsetPages(`/api/dictionaries/${dictionaryId}/entries`)
|
|
48
|
+
const records = toRecords(pages.items)
|
|
49
|
+
|
|
50
|
+
if (!pages.ok) {
|
|
51
|
+
const error = pages.errorPayload?.error
|
|
52
|
+
return {
|
|
53
|
+
ok: false,
|
|
54
|
+
items: records,
|
|
55
|
+
pageCount: pages.pageCount,
|
|
56
|
+
errorMessage: typeof error === 'string' ? error : null,
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// The route bounds each response and orders it in memory, so entries
|
|
61
|
+
// assembled from several pages are only sorted within each page. Re-apply the
|
|
62
|
+
// mode that produced them across the whole set. A single-page dictionary —
|
|
63
|
+
// every dictionary at or below the route ceiling — keeps the server ordering
|
|
64
|
+
// byte-for-byte.
|
|
65
|
+
if (pages.pageCount <= 1) {
|
|
66
|
+
return { ok: true, items: records, pageCount: pages.pageCount, errorMessage: null }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const sortMode = resolveDictionaryEntrySortMode(pages.firstPayload.sortMode)
|
|
70
|
+
const sorted = sortDictionaryEntries(records.map(toSortView), sortMode).map((view) => view.entry)
|
|
71
|
+
return { ok: true, items: sorted, pageCount: pages.pageCount, errorMessage: null }
|
|
72
|
+
}
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from './definitions.cache'
|
|
15
15
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
16
16
|
import { filterSelectableSystemEntityIds, isSystemEntitySelectable } from '@open-mercato/shared/lib/entities/system-entities'
|
|
17
|
+
import { isOrmBackedSystemEntityId } from '@open-mercato/shared/lib/data/engine'
|
|
17
18
|
import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
|
|
18
19
|
import { loadEntityFieldsetConfigs, CustomFieldsetDefinition } from '../lib/fieldsets'
|
|
19
20
|
import { installCustomEntitiesFromModules } from '../lib/install-from-ce'
|
|
@@ -24,6 +25,12 @@ import {
|
|
|
24
25
|
beginEntitiesMutationGuard,
|
|
25
26
|
FIELD_DEFINITION_RESOURCE_KIND,
|
|
26
27
|
} from './definitions.mutation-guard'
|
|
28
|
+
import {
|
|
29
|
+
canReadAllEntityMetadata,
|
|
30
|
+
canReadEntityMetadata,
|
|
31
|
+
getDeclaredCustomEntityRestriction,
|
|
32
|
+
resolveEntityAclRequirement,
|
|
33
|
+
} from '../lib/entityAcl'
|
|
27
34
|
import {
|
|
28
35
|
createExactDefinitionWhere,
|
|
29
36
|
createScopedDefinitionTombstone,
|
|
@@ -226,8 +233,8 @@ export async function GET(req: Request) {
|
|
|
226
233
|
const auth = await getAuthFromRequest(req)
|
|
227
234
|
if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
228
235
|
|
|
229
|
-
const
|
|
230
|
-
if (!
|
|
236
|
+
const selectableEntityIds = filterSelectableSystemEntityIds(requestedEntityIds)
|
|
237
|
+
if (!selectableEntityIds.length) {
|
|
231
238
|
return NextResponse.json({ items: [] })
|
|
232
239
|
}
|
|
233
240
|
|
|
@@ -241,6 +248,48 @@ export async function GET(req: Request) {
|
|
|
241
248
|
const organizationId = definitionScope.organizationId
|
|
242
249
|
const { resolve } = container
|
|
243
250
|
const em = resolve('em') as any
|
|
251
|
+
const rbac = resolve('rbacService') as RbacService
|
|
252
|
+
const acl = await rbac.loadAcl(auth.sub ?? '', {
|
|
253
|
+
tenantId,
|
|
254
|
+
organizationId,
|
|
255
|
+
})
|
|
256
|
+
const designerCanRead = canReadAllEntityMetadata(acl)
|
|
257
|
+
const customEntityRestrictions = new Map<string, boolean>()
|
|
258
|
+
for (const entityId of selectableEntityIds) {
|
|
259
|
+
if (isOrmBackedSystemEntityId(em, entityId)) continue
|
|
260
|
+
const declaredRestriction = getDeclaredCustomEntityRestriction(entityId)
|
|
261
|
+
if (declaredRestriction !== undefined) customEntityRestrictions.set(entityId, declaredRestriction)
|
|
262
|
+
}
|
|
263
|
+
if (!designerCanRead) {
|
|
264
|
+
const unresolvedEntityIds = selectableEntityIds.filter(
|
|
265
|
+
(entityId) => !resolveEntityAclRequirement(entityId) && !customEntityRestrictions.has(entityId),
|
|
266
|
+
)
|
|
267
|
+
if (unresolvedEntityIds.length) {
|
|
268
|
+
const registrations = await em.find(CustomEntity as any, {
|
|
269
|
+
entityId: { $in: unresolvedEntityIds },
|
|
270
|
+
isActive: true,
|
|
271
|
+
$and: [
|
|
272
|
+
{ $or: [{ tenantId }, { tenantId: null }] },
|
|
273
|
+
{ $or: [{ organizationId }, { organizationId: null }] },
|
|
274
|
+
],
|
|
275
|
+
} as any)
|
|
276
|
+
for (const registration of registrations as any[]) {
|
|
277
|
+
const entityId = String(registration.entityId)
|
|
278
|
+
if (!isOrmBackedSystemEntityId(em, entityId)) {
|
|
279
|
+
customEntityRestrictions.set(entityId, registration.accessRestricted === true)
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const entityIds = selectableEntityIds.filter((entityId) => canReadEntityMetadata({
|
|
285
|
+
entityId,
|
|
286
|
+
isCustomEntity: customEntityRestrictions.has(entityId),
|
|
287
|
+
isRestricted: customEntityRestrictions.get(entityId) === true,
|
|
288
|
+
acl,
|
|
289
|
+
}))
|
|
290
|
+
if (!entityIds.length) {
|
|
291
|
+
return NextResponse.json({ items: [], fieldsetsByEntity: {}, entitySettings: {} })
|
|
292
|
+
}
|
|
244
293
|
let cache: CacheStrategy | undefined
|
|
245
294
|
try {
|
|
246
295
|
cache = resolve('cache') as CacheStrategy
|
|
@@ -249,7 +298,6 @@ export async function GET(req: Request) {
|
|
|
249
298
|
let canManageDefinitions = false
|
|
250
299
|
if (typeof auth.sub === 'string' && auth.sub.length > 0) {
|
|
251
300
|
try {
|
|
252
|
-
const rbac = resolve('rbacService') as RbacService
|
|
253
301
|
canManageDefinitions = await rbac.userHasAllFeatures(auth.sub, ['entities.definitions.manage'], {
|
|
254
302
|
tenantId,
|
|
255
303
|
organizationId,
|