@open-mercato/core 0.8.1-develop.7274.1.21a0083828 → 0.8.1-develop.7294.1.0ef99fc518
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/auth/api/users/acl/route.js.map +2 -2
- package/dist/modules/auth/components/AclEditor.js +21 -3
- package/dist/modules/auth/components/AclEditor.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies/page.js +6 -4
- package/dist/modules/customers/backend/customers/companies/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/map/components/DealsMapView.js +6 -4
- package/dist/modules/customers/backend/customers/deals/map/components/DealsMapView.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/page.js +6 -4
- package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ChangeOwnerDialog.js +5 -3
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ChangeOwnerDialog.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js +6 -4
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people/page.js +6 -4
- package/dist/modules/customers/backend/customers/people/page.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/PeopleField.js +9 -2
- package/dist/modules/customers/components/calendar/editor/PeopleField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/hooks.js +4 -2
- package/dist/modules/customers/components/calendar/editor/hooks.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/lookups.js +7 -3
- package/dist/modules/customers/components/calendar/editor/lookups.js.map +2 -2
- package/dist/modules/customers/components/detail/AssignRoleDialog.js +5 -2
- package/dist/modules/customers/components/detail/AssignRoleDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/RoleAssignmentRow.js +4 -2
- package/dist/modules/customers/components/detail/RoleAssignmentRow.js.map +2 -2
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +8 -5
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/DateTimeFields.js +10 -7
- package/dist/modules/customers/components/detail/schedule/DateTimeFields.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/ParticipantsField.js +4 -2
- package/dist/modules/customers/components/detail/schedule/ParticipantsField.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/fieldConfig.js +9 -0
- package/dist/modules/customers/components/detail/schedule/fieldConfig.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/index.js +3 -1
- package/dist/modules/customers/components/detail/schedule/index.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js +11 -4
- package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js.map +2 -2
- package/dist/modules/customers/components/formConfig.js +17 -9
- package/dist/modules/customers/components/formConfig.js.map +2 -2
- package/dist/modules/customers/data/validators.js +12 -7
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/customers/lib/assignableStaff.js +103 -22
- package/dist/modules/customers/lib/assignableStaff.js.map +2 -2
- package/dist/modules/customers/setup.js +10 -2
- package/dist/modules/customers/setup.js.map +2 -2
- package/dist/modules/data_sync/api/schedules/[id]/route.js +5 -0
- package/dist/modules/data_sync/api/schedules/[id]/route.js.map +2 -2
- package/dist/modules/data_sync/api/schedules/route.js +5 -0
- package/dist/modules/data_sync/api/schedules/route.js.map +2 -2
- package/dist/modules/data_sync/backend/data-sync/page.js +29 -5
- package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
- package/dist/modules/data_sync/components/IntegrationScheduleTab.js +48 -11
- package/dist/modules/data_sync/components/IntegrationScheduleTab.js.map +2 -2
- package/dist/modules/data_sync/data/validators.js +13 -2
- package/dist/modules/data_sync/data/validators.js.map +2 -2
- package/dist/modules/data_sync/lib/schedule-value.js +30 -0
- package/dist/modules/data_sync/lib/schedule-value.js.map +7 -0
- package/dist/modules/eudr/data/validators.js.map +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js +3 -4
- package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js.map +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.js +3 -1
- package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.js.map +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/login/page.js +2 -1
- package/dist/modules/portal/frontend/[orgSlug]/portal/login/page.js.map +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/page.js +3 -4
- package/dist/modules/portal/frontend/[orgSlug]/portal/page.js.map +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/profile/page.js +3 -4
- package/dist/modules/portal/frontend/[orgSlug]/portal/profile/page.js.map +2 -2
- package/dist/modules/portal/lib/navigation.js +3 -4
- package/dist/modules/portal/lib/navigation.js.map +2 -2
- package/dist/modules/query_index/lib/engine.js +79 -14
- package/dist/modules/query_index/lib/engine.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/page.js +122 -86
- package/dist/modules/staff/backend/staff/time-tracking/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js +38 -17
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/reports/[id]/page.js +45 -18
- package/dist/modules/staff/backend/staff/time-tracking/reports/[id]/page.js.map +2 -2
- package/dist/modules/staff/extension-points.js +16 -0
- package/dist/modules/staff/extension-points.js.map +2 -2
- package/dist/modules/staff/frontend/[orgSlug]/portal/time-reports/[id]/page.js +3 -4
- package/dist/modules/staff/frontend/[orgSlug]/portal/time-reports/[id]/page.js.map +2 -2
- package/dist/modules/staff/frontend/[orgSlug]/portal/time-reports/page.js +3 -4
- package/dist/modules/staff/frontend/[orgSlug]/portal/time-reports/page.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/ReportSheet.js +14 -13
- package/dist/modules/staff/lib/time-tracking-ui/ReportSheet.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/TaskListView.js +60 -41
- package/dist/modules/staff/lib/time-tracking-ui/TaskListView.js.map +2 -2
- package/dist/modules/warranty_claims/api/ai/assess/route.js +2 -2
- package/dist/modules/warranty_claims/api/ai/assess/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/ai/draft-reply/route.js +3 -4
- package/dist/modules/warranty_claims/api/ai/draft-reply/route.js.map +3 -3
- package/dist/modules/warranty_claims/api/ai/suggest/route.js +3 -3
- package/dist/modules/warranty_claims/api/ai/suggest/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/assign/route.js +3 -3
- package/dist/modules/warranty_claims/api/assign/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/credit-memo/route.js +2 -2
- package/dist/modules/warranty_claims/api/credit-memo/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/entitlement/route.js +2 -2
- package/dist/modules/warranty_claims/api/entitlement/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/events/route.js +4 -4
- package/dist/modules/warranty_claims/api/events/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/external/claims/route.js +3 -3
- package/dist/modules/warranty_claims/api/external/claims/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/portal/claims/[id]/submit/route.js +2 -2
- package/dist/modules/warranty_claims/api/portal/claims/[id]/submit/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/portal/claims/[id]/withdraw/route.js +2 -2
- package/dist/modules/warranty_claims/api/portal/claims/[id]/withdraw/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/portal/claims/route.js +2 -2
- package/dist/modules/warranty_claims/api/portal/claims/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/portal/events/route.js +2 -2
- package/dist/modules/warranty_claims/api/portal/events/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/receiving/route.js +2 -2
- package/dist/modules/warranty_claims/api/receiving/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/replacement-order/route.js +2 -2
- package/dist/modules/warranty_claims/api/replacement-order/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/return-label/route.js +2 -2
- package/dist/modules/warranty_claims/api/return-label/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/risk/route.js +2 -2
- package/dist/modules/warranty_claims/api/risk/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/sales-return/route.js +2 -2
- package/dist/modules/warranty_claims/api/sales-return/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/settings-general/route.js +4 -4
- package/dist/modules/warranty_claims/api/settings-general/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/stats/route.js +2 -2
- package/dist/modules/warranty_claims/api/stats/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/submit/route.js +3 -3
- package/dist/modules/warranty_claims/api/submit/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/transition/route.js +3 -3
- package/dist/modules/warranty_claims/api/transition/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/vendor-recovery/route.js +3 -3
- package/dist/modules/warranty_claims/api/vendor-recovery/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/vendor-recovery-suggestions/route.js +2 -2
- package/dist/modules/warranty_claims/api/vendor-recovery-suggestions/route.js.map +2 -2
- package/dist/modules/warranty_claims/backend/page.js +4 -2
- package/dist/modules/warranty_claims/backend/page.js.map +2 -2
- package/dist/modules/warranty_claims/backend/warranty_claims/[id]/page.js +9 -2
- package/dist/modules/warranty_claims/backend/warranty_claims/[id]/page.js.map +2 -2
- package/dist/modules/warranty_claims/backend/warranty_claims/settings/page.js +4 -2
- package/dist/modules/warranty_claims/backend/warranty_claims/settings/page.js.map +2 -2
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.js +3 -4
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.js.map +2 -2
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/new/page.js +3 -2
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/new/page.js.map +2 -2
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.js +3 -2
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.js.map +2 -2
- package/dist/modules/workflows/components/DefinitionTriggersEditor.js +1 -1
- package/dist/modules/workflows/components/DefinitionTriggersEditor.js.map +2 -2
- package/dist/modules/workflows/frontend/[orgSlug]/portal/tasks/[id]/page.js +2 -2
- package/dist/modules/workflows/frontend/[orgSlug]/portal/tasks/[id]/page.js.map +2 -2
- package/dist/modules/workflows/lib/event-trigger-service.js +56 -1
- package/dist/modules/workflows/lib/event-trigger-service.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/api/users/acl/route.ts +4 -3
- package/src/modules/auth/components/AclEditor.tsx +38 -2
- package/src/modules/auth/i18n/de.json +5 -1
- package/src/modules/auth/i18n/en.json +5 -1
- package/src/modules/auth/i18n/es.json +5 -1
- package/src/modules/auth/i18n/ko.json +5 -1
- package/src/modules/auth/i18n/pl.json +5 -1
- package/src/modules/customers/backend/customers/companies/page.tsx +6 -4
- package/src/modules/customers/backend/customers/deals/map/components/DealsMapView.tsx +6 -4
- package/src/modules/customers/backend/customers/deals/page.tsx +6 -4
- package/src/modules/customers/backend/customers/deals/pipeline/components/ChangeOwnerDialog.tsx +5 -3
- package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +6 -4
- package/src/modules/customers/backend/customers/people/page.tsx +6 -4
- package/src/modules/customers/components/calendar/editor/PeopleField.tsx +9 -2
- package/src/modules/customers/components/calendar/editor/hooks.ts +4 -2
- package/src/modules/customers/components/calendar/editor/lookups.ts +12 -4
- package/src/modules/customers/components/detail/AssignRoleDialog.tsx +4 -1
- package/src/modules/customers/components/detail/RoleAssignmentRow.tsx +4 -2
- package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +15 -7
- package/src/modules/customers/components/detail/schedule/DateTimeFields.tsx +16 -7
- package/src/modules/customers/components/detail/schedule/ParticipantsField.tsx +4 -2
- package/src/modules/customers/components/detail/schedule/fieldConfig.ts +15 -0
- package/src/modules/customers/components/detail/schedule/index.ts +1 -1
- package/src/modules/customers/components/detail/schedule/useScheduleFormState.ts +16 -9
- package/src/modules/customers/components/formConfig.tsx +35 -12
- package/src/modules/customers/data/validators.ts +14 -8
- package/src/modules/customers/i18n/de.json +1 -0
- package/src/modules/customers/i18n/en.json +1 -0
- package/src/modules/customers/i18n/es.json +1 -0
- package/src/modules/customers/i18n/ko.json +1 -0
- package/src/modules/customers/i18n/pl.json +1 -0
- package/src/modules/customers/lib/assignableStaff.ts +172 -34
- package/src/modules/customers/setup.ts +8 -0
- package/src/modules/data_sync/api/schedules/[id]/route.ts +5 -0
- package/src/modules/data_sync/api/schedules/route.ts +5 -0
- package/src/modules/data_sync/backend/data-sync/page.tsx +48 -4
- package/src/modules/data_sync/components/IntegrationScheduleTab.tsx +61 -2
- package/src/modules/data_sync/data/validators.ts +26 -2
- package/src/modules/data_sync/i18n/de.json +2 -0
- package/src/modules/data_sync/i18n/en.json +2 -0
- package/src/modules/data_sync/i18n/es.json +2 -0
- package/src/modules/data_sync/i18n/ko.json +2 -0
- package/src/modules/data_sync/i18n/pl.json +2 -0
- package/src/modules/data_sync/lib/schedule-value.ts +45 -0
- package/src/modules/eudr/data/validators.ts +3 -4
- package/src/modules/portal/frontend/[orgSlug]/portal/dashboard/page.tsx +5 -4
- package/src/modules/portal/frontend/[orgSlug]/portal/invite/page.tsx +3 -1
- package/src/modules/portal/frontend/[orgSlug]/portal/login/page.tsx +2 -1
- package/src/modules/portal/frontend/[orgSlug]/portal/page.tsx +5 -5
- package/src/modules/portal/frontend/[orgSlug]/portal/profile/page.tsx +5 -4
- package/src/modules/portal/i18n/de.json +1 -0
- package/src/modules/portal/i18n/en.json +1 -0
- package/src/modules/portal/i18n/es.json +1 -0
- package/src/modules/portal/i18n/ko.json +1 -0
- package/src/modules/portal/i18n/pl.json +1 -0
- package/src/modules/portal/lib/navigation.ts +8 -3
- package/src/modules/query_index/lib/engine.ts +111 -16
- package/src/modules/staff/backend/staff/time-tracking/page.tsx +161 -130
- package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +55 -43
- package/src/modules/staff/backend/staff/time-tracking/reports/[id]/page.tsx +61 -38
- package/src/modules/staff/extension-points.ts +16 -0
- package/src/modules/staff/frontend/[orgSlug]/portal/time-reports/[id]/page.tsx +3 -4
- package/src/modules/staff/frontend/[orgSlug]/portal/time-reports/page.tsx +3 -4
- package/src/modules/staff/i18n/de.json +3 -0
- package/src/modules/staff/i18n/en.json +3 -0
- package/src/modules/staff/i18n/es.json +3 -0
- package/src/modules/staff/i18n/ko.json +3 -0
- package/src/modules/staff/i18n/pl.json +3 -0
- package/src/modules/staff/lib/time-tracking-ui/ReportSheet.tsx +32 -29
- package/src/modules/staff/lib/time-tracking-ui/TaskListView.tsx +89 -79
- package/src/modules/warranty_claims/api/ai/assess/route.ts +2 -2
- package/src/modules/warranty_claims/api/ai/draft-reply/route.ts +2 -3
- package/src/modules/warranty_claims/api/ai/suggest/route.ts +3 -3
- package/src/modules/warranty_claims/api/assign/route.ts +3 -3
- package/src/modules/warranty_claims/api/credit-memo/route.ts +2 -2
- package/src/modules/warranty_claims/api/entitlement/route.ts +2 -2
- package/src/modules/warranty_claims/api/events/route.ts +4 -4
- package/src/modules/warranty_claims/api/external/claims/route.ts +3 -3
- package/src/modules/warranty_claims/api/portal/claims/[id]/submit/route.ts +2 -2
- package/src/modules/warranty_claims/api/portal/claims/[id]/withdraw/route.ts +2 -2
- package/src/modules/warranty_claims/api/portal/claims/route.ts +2 -2
- package/src/modules/warranty_claims/api/portal/events/route.ts +2 -2
- package/src/modules/warranty_claims/api/receiving/route.ts +2 -2
- package/src/modules/warranty_claims/api/replacement-order/route.ts +2 -2
- package/src/modules/warranty_claims/api/return-label/route.ts +2 -2
- package/src/modules/warranty_claims/api/risk/route.ts +2 -2
- package/src/modules/warranty_claims/api/sales-return/route.ts +2 -2
- package/src/modules/warranty_claims/api/settings-general/route.ts +4 -4
- package/src/modules/warranty_claims/api/stats/route.ts +2 -2
- package/src/modules/warranty_claims/api/submit/route.ts +3 -3
- package/src/modules/warranty_claims/api/transition/route.ts +3 -3
- package/src/modules/warranty_claims/api/vendor-recovery/route.ts +3 -3
- package/src/modules/warranty_claims/api/vendor-recovery-suggestions/route.ts +2 -2
- package/src/modules/warranty_claims/backend/page.tsx +4 -2
- package/src/modules/warranty_claims/backend/warranty_claims/[id]/page.tsx +9 -2
- package/src/modules/warranty_claims/backend/warranty_claims/settings/page.tsx +4 -2
- package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.tsx +5 -4
- package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/new/page.tsx +5 -2
- package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.tsx +5 -2
- package/src/modules/workflows/components/DefinitionTriggersEditor.tsx +1 -1
- package/src/modules/workflows/frontend/[orgSlug]/portal/tasks/[id]/page.tsx +2 -2
- package/src/modules/workflows/i18n/de.json +1 -1
- package/src/modules/workflows/i18n/en.json +1 -1
- package/src/modules/workflows/i18n/es.json +1 -1
- package/src/modules/workflows/i18n/ko.json +1 -1
- package/src/modules/workflows/i18n/pl.json +1 -1
- package/src/modules/workflows/lib/event-trigger-service.ts +139 -0
|
@@ -31,6 +31,8 @@
|
|
|
31
31
|
"data_sync.dashboard.schedule.interval": "간격",
|
|
32
32
|
"data_sync.dashboard.schedule.intervalHelp": "예: 반복 간격으로 `1h`, `6h`, `24h`를 사용하세요.",
|
|
33
33
|
"data_sync.dashboard.schedule.intervalValue": "간격",
|
|
34
|
+
"data_sync.dashboard.schedule.invalidCron": "스케줄러가 해석할 수 있는 cron 표현식을 입력하세요. 예: `0 * * * *`.",
|
|
35
|
+
"data_sync.dashboard.schedule.invalidInterval": "`15m`, `1h`, `24h`처럼 정수 뒤에 s, m, h 또는 d를 붙여 1분 이상으로 입력하세요.",
|
|
34
36
|
"data_sync.dashboard.schedule.invalidValue": "저장하기 전에 일정 값을 입력하세요.",
|
|
35
37
|
"data_sync.dashboard.schedule.lastRun": "마지막 예약 실행: {value}",
|
|
36
38
|
"data_sync.dashboard.schedule.neverRun": "저장되었지만 아직 완료된 예약 실행이 없습니다.",
|
|
@@ -31,6 +31,8 @@
|
|
|
31
31
|
"data_sync.dashboard.schedule.interval": "Interwał",
|
|
32
32
|
"data_sync.dashboard.schedule.intervalHelp": "Przykład: `1h`, `6h` lub `24h` dla powtarzalnych interwałów.",
|
|
33
33
|
"data_sync.dashboard.schedule.intervalValue": "Interwał",
|
|
34
|
+
"data_sync.dashboard.schedule.invalidCron": "Podaj wyrażenie cron, które scheduler potrafi przetworzyć, na przykład `0 * * * *`.",
|
|
35
|
+
"data_sync.dashboard.schedule.invalidInterval": "Podaj liczbę całkowitą z jednostką s, m, h lub d (na przykład `15m`, `1h` lub `24h`), o długości co najmniej jednej minuty.",
|
|
34
36
|
"data_sync.dashboard.schedule.invalidValue": "Podaj wartość harmonogramu przed zapisaniem.",
|
|
35
37
|
"data_sync.dashboard.schedule.lastRun": "Ostatni zaplanowany przebieg: {value}",
|
|
36
38
|
"data_sync.dashboard.schedule.neverRun": "Zapisano, ale żadne zaplanowane wykonanie jeszcze się nie zakończyło.",
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ScheduleValueKind } from '@open-mercato/shared/lib/schedule/invalidScheduleValue'
|
|
2
|
+
|
|
3
|
+
export const SCHEDULE_VALUE_FIELD = 'scheduleValue'
|
|
4
|
+
|
|
5
|
+
export const SCHEDULE_VALUE_FORMAT_MESSAGES: Record<ScheduleValueKind, string> = {
|
|
6
|
+
interval: 'Interval must be a whole number followed by s, m, h or d (for example 15m, 1h or 24h) and cover at least one minute.',
|
|
7
|
+
cron: 'Cron expression could not be parsed. Use the five-field format, for example "0 * * * *".',
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type ScheduleValueErrorBody = {
|
|
11
|
+
error: string
|
|
12
|
+
details: {
|
|
13
|
+
formErrors: string[]
|
|
14
|
+
fieldErrors: Record<string, string[]>
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Mirrors zod's `flatten()` shape so a scheduler-reported format failure and a
|
|
20
|
+
* schema-reported one reach the client as the same structured field error
|
|
21
|
+
* instead of an internal scheduler message.
|
|
22
|
+
*/
|
|
23
|
+
export function buildScheduleValueErrorBody(scheduleType: ScheduleValueKind): ScheduleValueErrorBody {
|
|
24
|
+
return {
|
|
25
|
+
error: 'Invalid payload',
|
|
26
|
+
details: {
|
|
27
|
+
formErrors: [],
|
|
28
|
+
fieldErrors: { [SCHEDULE_VALUE_FIELD]: [SCHEDULE_VALUE_FORMAT_MESSAGES[scheduleType]] },
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Lets a form render its own translated hint next to the field instead of
|
|
35
|
+
* flashing the API's English message as a toast.
|
|
36
|
+
*/
|
|
37
|
+
export function hasScheduleValueFieldError(payload: unknown): boolean {
|
|
38
|
+
if (typeof payload !== 'object' || payload === null) return false
|
|
39
|
+
const details = (payload as { details?: unknown }).details
|
|
40
|
+
if (typeof details !== 'object' || details === null) return false
|
|
41
|
+
const fieldErrors = (details as { fieldErrors?: unknown }).fieldErrors
|
|
42
|
+
if (typeof fieldErrors !== 'object' || fieldErrors === null) return false
|
|
43
|
+
const messages = (fieldErrors as Record<string, unknown>)[SCHEDULE_VALUE_FIELD]
|
|
44
|
+
return Array.isArray(messages) && messages.length > 0
|
|
45
|
+
}
|
|
@@ -127,10 +127,9 @@ const pastOrNowIsoDateTimeSchema = () => isoDateTimeSchema().refine((value) => v
|
|
|
127
127
|
const isoDateOrDateTimeSchema = () => z.union([isoDateSchema(), isoDateTimeSchema()])
|
|
128
128
|
|
|
129
129
|
// Country codes are validated through ICU rather than the shared country helper.
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
// keep using the helper because bundlers resolve the JSON themselves.
|
|
130
|
+
// ICU keeps this schema self-contained (no dependency on the generated country
|
|
131
|
+
// list) and accepts any region code Node's DisplayNames can resolve, including
|
|
132
|
+
// codes the shared helper supplements separately (e.g. XK).
|
|
134
133
|
const countryDisplayNames = typeof Intl !== 'undefined' && typeof Intl.DisplayNames !== 'undefined'
|
|
135
134
|
? new Intl.DisplayNames(['en'], { type: 'region' })
|
|
136
135
|
: null
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
import React, { useEffect, useMemo, useState, useCallback } from 'react'
|
|
3
3
|
import { extensionPoints } from '@open-mercato/core/modules/portal/extension-points'
|
|
4
|
-
import {
|
|
4
|
+
import { replaceWithPageReload } from '@open-mercato/core/modules/portal/lib/navigation'
|
|
5
5
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
6
6
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
7
7
|
import { Spinner } from '@open-mercato/ui/primitives/spinner'
|
|
@@ -29,7 +29,6 @@ function WidgetIcon({ className }: { className?: string }) {
|
|
|
29
29
|
|
|
30
30
|
export default function PortalDashboardPage({ params }: Props) {
|
|
31
31
|
const t = useT()
|
|
32
|
-
const router = useRouter()
|
|
33
32
|
const { auth } = usePortalContext()
|
|
34
33
|
const { user, loading } = auth
|
|
35
34
|
|
|
@@ -47,11 +46,13 @@ export default function PortalDashboardPage({ params }: Props) {
|
|
|
47
46
|
setHiddenWidgets(user ? loadHiddenWidgets(params.orgSlug, user.id) : new Set())
|
|
48
47
|
}
|
|
49
48
|
|
|
49
|
+
// Leaving an authenticated page for the login page crosses the public/authenticated
|
|
50
|
+
// boundary, so it must be a full page load — see `replaceWithPageReload`.
|
|
50
51
|
useEffect(() => {
|
|
51
52
|
if (!loading && !user) {
|
|
52
|
-
|
|
53
|
+
replaceWithPageReload(`/${params.orgSlug}/portal/login`)
|
|
53
54
|
}
|
|
54
|
-
}, [loading, user,
|
|
55
|
+
}, [loading, user, params.orgSlug])
|
|
55
56
|
|
|
56
57
|
useEffect(() => {
|
|
57
58
|
clearLegacyHiddenWidgetsKey()
|
|
@@ -79,7 +79,9 @@ export default function PortalInvitePage({ params }: Props) {
|
|
|
79
79
|
return
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
if (result.status ===
|
|
82
|
+
if (result.status === 409) {
|
|
83
|
+
setError(t('portal.invite.error.accountExists', 'This email address already has a portal account. Sign in below instead of accepting the invitation.'))
|
|
84
|
+
} else if (result.status === 400) {
|
|
83
85
|
setError(t('portal.invite.error.invalidToken', 'Invalid or expired invitation.'))
|
|
84
86
|
} else {
|
|
85
87
|
setError(result.result?.error || t('portal.invite.error.generic', 'Invitation acceptance failed. Please try again.'))
|
|
@@ -12,6 +12,7 @@ import { EmptyState } from '@open-mercato/ui/primitives/empty-state'
|
|
|
12
12
|
import { SearchX } from 'lucide-react'
|
|
13
13
|
import { Spinner } from '@open-mercato/ui/primitives/spinner'
|
|
14
14
|
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
15
|
+
import { navigateWithPageReload } from '@open-mercato/core/modules/portal/lib/navigation'
|
|
15
16
|
import { usePortalContext } from '@open-mercato/ui/portal/PortalContext'
|
|
16
17
|
import { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'
|
|
17
18
|
|
|
@@ -47,7 +48,7 @@ export default function PortalLoginPage({ params }: Props) {
|
|
|
47
48
|
})
|
|
48
49
|
|
|
49
50
|
if (result.ok && result.result?.ok) {
|
|
50
|
-
|
|
51
|
+
navigateWithPageReload(`/${orgSlug}/portal/dashboard`)
|
|
51
52
|
return
|
|
52
53
|
}
|
|
53
54
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { useEffect, useMemo } from 'react'
|
|
3
3
|
import { extensionPoints } from '@open-mercato/core/modules/portal/extension-points'
|
|
4
4
|
import Link from 'next/link'
|
|
5
|
-
import {
|
|
5
|
+
import { replaceWithPageReload } from '@open-mercato/core/modules/portal/lib/navigation'
|
|
6
6
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
7
7
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
8
8
|
import { Spinner } from '@open-mercato/ui/primitives/spinner'
|
|
@@ -16,16 +16,16 @@ type Props = { params: { orgSlug: string } }
|
|
|
16
16
|
|
|
17
17
|
export default function PortalLandingPage({ params }: Props) {
|
|
18
18
|
const t = useT()
|
|
19
|
-
const router = useRouter()
|
|
20
19
|
const orgSlug = params.orgSlug
|
|
21
20
|
const { auth, tenant } = usePortalContext()
|
|
22
21
|
|
|
23
|
-
// Redirect authenticated users to dashboard
|
|
22
|
+
// Redirect authenticated users to dashboard. This crosses the public/authenticated
|
|
23
|
+
// boundary, so it must be a full page load — see `replaceWithPageReload`.
|
|
24
24
|
useEffect(() => {
|
|
25
25
|
if (!auth.loading && auth.user) {
|
|
26
|
-
|
|
26
|
+
replaceWithPageReload(`/${orgSlug}/portal/dashboard`)
|
|
27
27
|
}
|
|
28
|
-
}, [auth.loading, auth.user,
|
|
28
|
+
}, [auth.loading, auth.user, orgSlug])
|
|
29
29
|
|
|
30
30
|
const injectionContext = useMemo(
|
|
31
31
|
() => ({ orgSlug }),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
import { useEffect, useMemo } from 'react'
|
|
3
3
|
import { extensionPoints } from '@open-mercato/core/modules/portal/extension-points'
|
|
4
|
-
import {
|
|
4
|
+
import { replaceWithPageReload } from '@open-mercato/core/modules/portal/lib/navigation'
|
|
5
5
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
6
6
|
import { Badge } from '@open-mercato/ui/primitives/badge'
|
|
7
7
|
import { Spinner } from '@open-mercato/ui/primitives/spinner'
|
|
@@ -14,15 +14,16 @@ type Props = { params: { orgSlug: string } }
|
|
|
14
14
|
|
|
15
15
|
export default function PortalProfilePage({ params }: Props) {
|
|
16
16
|
const t = useT()
|
|
17
|
-
const router = useRouter()
|
|
18
17
|
const { auth } = usePortalContext()
|
|
19
18
|
const { user, roles, resolvedFeatures, isPortalAdmin, loading } = auth
|
|
20
19
|
|
|
20
|
+
// Leaving an authenticated page for the login page crosses the public/authenticated
|
|
21
|
+
// boundary, so it must be a full page load — see `replaceWithPageReload`.
|
|
21
22
|
useEffect(() => {
|
|
22
23
|
if (!loading && !user) {
|
|
23
|
-
|
|
24
|
+
replaceWithPageReload(`/${params.orgSlug}/portal/login`)
|
|
24
25
|
}
|
|
25
|
-
}, [loading, user,
|
|
26
|
+
}, [loading, user, params.orgSlug])
|
|
26
27
|
|
|
27
28
|
const injectionContext = useMemo(
|
|
28
29
|
() => ({ orgSlug: params.orgSlug, user, roles, resolvedFeatures, isPortalAdmin }),
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"portal.invite.confirmPassword.placeholder": "••••••••",
|
|
28
28
|
"portal.invite.description": "Erstellen Sie Ihr Portalkonto, um diese Einladung anzunehmen.",
|
|
29
29
|
"portal.invite.displayName": "Anzeigename",
|
|
30
|
+
"portal.invite.error.accountExists": "Diese E-Mail-Adresse hat bereits ein Portalkonto. Melden Sie sich unten an, anstatt die Einladung anzunehmen.",
|
|
30
31
|
"portal.invite.error.displayNameRequired": "Der Anzeigename ist erforderlich.",
|
|
31
32
|
"portal.invite.error.generic": "Annahme der Einladung fehlgeschlagen. Bitte versuchen Sie es erneut.",
|
|
32
33
|
"portal.invite.error.invalidToken": "Ungültige oder abgelaufene Einladung.",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"portal.invite.confirmPassword.placeholder": "••••••••",
|
|
28
28
|
"portal.invite.description": "Create your portal account to accept this invitation.",
|
|
29
29
|
"portal.invite.displayName": "Display Name",
|
|
30
|
+
"portal.invite.error.accountExists": "This email address already has a portal account. Sign in below instead of accepting the invitation.",
|
|
30
31
|
"portal.invite.error.displayNameRequired": "Display name is required.",
|
|
31
32
|
"portal.invite.error.generic": "Invitation acceptance failed. Please try again.",
|
|
32
33
|
"portal.invite.error.invalidToken": "Invalid or expired invitation.",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"portal.invite.confirmPassword.placeholder": "••••••••",
|
|
28
28
|
"portal.invite.description": "Crea tu cuenta del portal para aceptar esta invitación.",
|
|
29
29
|
"portal.invite.displayName": "Nombre para mostrar",
|
|
30
|
+
"portal.invite.error.accountExists": "Esta dirección de correo electrónico ya tiene una cuenta del portal. Inicia sesión abajo en lugar de aceptar la invitación.",
|
|
30
31
|
"portal.invite.error.displayNameRequired": "El nombre para mostrar es obligatorio.",
|
|
31
32
|
"portal.invite.error.generic": "Error al aceptar la invitación. Inténtalo de nuevo.",
|
|
32
33
|
"portal.invite.error.invalidToken": "Invitación no válida o expirada.",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"portal.invite.confirmPassword.placeholder": "••••••••",
|
|
28
28
|
"portal.invite.description": "이 초대를 수락하려면 포털 계정을 만드세요.",
|
|
29
29
|
"portal.invite.displayName": "표시 이름",
|
|
30
|
+
"portal.invite.error.accountExists": "이 이메일 주소에는 이미 포털 계정이 있습니다. 초대를 수락하는 대신 아래에서 로그인하세요.",
|
|
30
31
|
"portal.invite.error.displayNameRequired": "표시 이름이 필요합니다.",
|
|
31
32
|
"portal.invite.error.generic": "초대 수락에 실패했습니다. 다시 시도해 주세요.",
|
|
32
33
|
"portal.invite.error.invalidToken": "초대가 유효하지 않거나 만료되었습니다.",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"portal.invite.confirmPassword.placeholder": "••••••••",
|
|
28
28
|
"portal.invite.description": "Utwórz konto w portalu, aby przyjąć to zaproszenie.",
|
|
29
29
|
"portal.invite.displayName": "Nazwa wyświetlana",
|
|
30
|
+
"portal.invite.error.accountExists": "Ten adres e-mail ma już konto w portalu. Zaloguj się poniżej zamiast przyjmować zaproszenie.",
|
|
30
31
|
"portal.invite.error.displayNameRequired": "Nazwa wyświetlana jest wymagana.",
|
|
31
32
|
"portal.invite.error.generic": "Nie udało się przyjąć zaproszenia. Spróbuj ponownie.",
|
|
32
33
|
"portal.invite.error.invalidToken": "Nieprawidłowe lub wygasłe zaproszenie.",
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Portal-local import path for the boundary-crossing navigation helpers.
|
|
3
|
+
*
|
|
4
|
+
* The implementation lives in `@open-mercato/shared/lib/navigation/pageReload` so `@open-mercato/ui`
|
|
5
|
+
* — which does not depend on `@open-mercato/core` — can reach it from the portal shell and hooks.
|
|
6
|
+
* This re-export keeps the published import path stable for existing callers.
|
|
7
|
+
*/
|
|
8
|
+
export { navigateWithPageReload, replaceWithPageReload } from '@open-mercato/shared/lib/navigation/pageReload'
|
|
@@ -10,6 +10,14 @@ import { readCoverageSnapshot, refreshCoverageSnapshot } from './coverage'
|
|
|
10
10
|
import { createProfiler, shouldEnableProfiler, type Profiler } from '@open-mercato/shared/lib/profiler'
|
|
11
11
|
import type { VectorIndexService } from '@open-mercato/search/vector'
|
|
12
12
|
import { decryptIndexDocCustomFields } from '@open-mercato/shared/lib/encryption/indexDoc'
|
|
13
|
+
import {
|
|
14
|
+
buildCustomFieldKindIndex,
|
|
15
|
+
createCustomFieldKindMapResolver,
|
|
16
|
+
emptyCustomFieldKindIndex,
|
|
17
|
+
mergeCustomFieldKindIndexes,
|
|
18
|
+
type CustomFieldKindIndex,
|
|
19
|
+
type CustomFieldKindMapResolver,
|
|
20
|
+
} from '@open-mercato/shared/lib/custom-fields/kinds'
|
|
13
21
|
import { parseBooleanToken, parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'
|
|
14
22
|
import {
|
|
15
23
|
applyJoinFilters,
|
|
@@ -42,6 +50,22 @@ import { createBoundedTtlMemo } from '@open-mercato/shared/lib/query/bounded-ttl
|
|
|
42
50
|
|
|
43
51
|
const logger = createLogger('query_index').child({ component: 'engine' })
|
|
44
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Plain-row shape of the `custom_field_defs` columns the key and kind lookups read. The
|
|
55
|
+
* scope columns are load-bearing: a key can carry an organization-specific definition that
|
|
56
|
+
* overrides the tenant-wide or global one with a different `kind`, so the winner can only be
|
|
57
|
+
* picked once the row being decrypted is known.
|
|
58
|
+
*/
|
|
59
|
+
type CustomFieldDefRow = {
|
|
60
|
+
entityId: string
|
|
61
|
+
key: string
|
|
62
|
+
kind: unknown
|
|
63
|
+
organizationId: string | null
|
|
64
|
+
tenantId: string | null
|
|
65
|
+
configJson: unknown
|
|
66
|
+
updatedAt: unknown
|
|
67
|
+
}
|
|
68
|
+
|
|
45
69
|
/** Operators `buildCfFilterExpression` compiles; anything else yields no predicate. */
|
|
46
70
|
const CF_FILTER_SUPPORTED_OPS = new Set<FilterOp>([
|
|
47
71
|
'eq', 'ne', 'in', 'nin', 'like', 'ilike', 'exists', 'gt', 'gte', 'lt', 'lte',
|
|
@@ -219,7 +243,7 @@ function createQueryProfiler(entity: string): Profiler {
|
|
|
219
243
|
|
|
220
244
|
export class HybridQueryEngine implements QueryEngine {
|
|
221
245
|
private coverageStatsTtlMs: number
|
|
222
|
-
private
|
|
246
|
+
private customFieldDefsCache = new Map<string, { expiresAt: number; value: CustomFieldDefRow[] }>()
|
|
223
247
|
private customFieldKeysTtlMs: number
|
|
224
248
|
private columnCache = new Map<string, boolean>()
|
|
225
249
|
private customEntityCache = new Map<string, boolean>()
|
|
@@ -1217,6 +1241,20 @@ export class HybridQueryEngine implements QueryEngine {
|
|
|
1217
1241
|
const listCountCapWarning: ListCountCapWarning | undefined = counted.warning
|
|
1218
1242
|
|
|
1219
1243
|
const dekKeyCache = new Map<string | null, string | null>()
|
|
1244
|
+
// Resolved lazily and only once per query, then memoized per scope: rows of one
|
|
1245
|
+
// organization share a kind map. `resolveCustomFieldKinds()` still runs for every
|
|
1246
|
+
// row once encryption is active, but the underlying `custom_field_defs` lookup is
|
|
1247
|
+
// TTL-cached, so a warm cache adds no extra query.
|
|
1248
|
+
let customFieldKinds: Promise<CustomFieldKindMapResolver> | null = null
|
|
1249
|
+
const resolveCustomFieldKinds = (): Promise<CustomFieldKindMapResolver> => {
|
|
1250
|
+
if (!customFieldKinds) {
|
|
1251
|
+
customFieldKinds = this.resolveCustomFieldKindIndex(
|
|
1252
|
+
Array.from(new Set(indexSources.map((src) => String(src.entityId)))),
|
|
1253
|
+
opts.tenantId ?? null,
|
|
1254
|
+
).then((index) => createCustomFieldKindMapResolver(index))
|
|
1255
|
+
}
|
|
1256
|
+
return customFieldKinds
|
|
1257
|
+
}
|
|
1220
1258
|
|
|
1221
1259
|
const decryptRow = async (item: Record<string, unknown>): Promise<Record<string, unknown>> => {
|
|
1222
1260
|
let next = item
|
|
@@ -1237,13 +1275,16 @@ export class HybridQueryEngine implements QueryEngine {
|
|
|
1237
1275
|
}
|
|
1238
1276
|
if (encSvc) {
|
|
1239
1277
|
try {
|
|
1278
|
+
// The kind is resolved against the very scope the value is decrypted with, so an
|
|
1279
|
+
// organization-specific definition can never type another organization's value.
|
|
1280
|
+
const cfScope = {
|
|
1281
|
+
tenantId: (next?.tenant_id ?? next?.tenantId ?? opts.tenantId ?? null) as string | null,
|
|
1282
|
+
organizationId: (next?.organization_id ?? next?.organizationId ?? fallbackOrgId ?? null) as string | null,
|
|
1283
|
+
}
|
|
1284
|
+
const resolveKinds = await resolveCustomFieldKinds()
|
|
1240
1285
|
next = await decryptIndexDocCustomFields(
|
|
1241
|
-
next,
|
|
1242
|
-
|
|
1243
|
-
tenantId: (next?.tenant_id ?? next?.tenantId ?? opts.tenantId ?? null) as string | null,
|
|
1244
|
-
organizationId: (next?.organization_id ?? next?.organizationId ?? null) as string | null,
|
|
1245
|
-
},
|
|
1246
|
-
encSvc as any, dekKeyCache,
|
|
1286
|
+
next, cfScope, encSvc as any, dekKeyCache,
|
|
1287
|
+
resolveKinds(cfScope.organizationId, cfScope.tenantId),
|
|
1247
1288
|
)
|
|
1248
1289
|
} catch { /* keep next as-is */ }
|
|
1249
1290
|
}
|
|
@@ -2193,37 +2234,91 @@ export class HybridQueryEngine implements QueryEngine {
|
|
|
2193
2234
|
return present
|
|
2194
2235
|
}
|
|
2195
2236
|
|
|
2196
|
-
private async
|
|
2237
|
+
private async resolveCustomFieldDefs(
|
|
2238
|
+
entityIds: string[],
|
|
2239
|
+
tenantId: string | null,
|
|
2240
|
+
): Promise<CustomFieldDefRow[]> {
|
|
2197
2241
|
if (!entityIds.length) return []
|
|
2198
2242
|
const cacheKey = this.customFieldKeysCacheKey(entityIds, tenantId)
|
|
2199
2243
|
const now = Date.now()
|
|
2200
|
-
const cached = this.
|
|
2244
|
+
const cached = this.customFieldDefsCache.get(cacheKey)
|
|
2201
2245
|
if (cached && cached.expiresAt > now) return cached.value.slice()
|
|
2202
2246
|
|
|
2203
2247
|
const db = this.getDb() as any
|
|
2204
2248
|
const rows = await db
|
|
2205
2249
|
.selectFrom('custom_field_defs')
|
|
2206
|
-
.select('key')
|
|
2250
|
+
.select(['entity_id', 'key', 'kind', 'organization_id', 'tenant_id', 'config_json', 'updated_at'])
|
|
2207
2251
|
.where('entity_id', 'in', entityIds)
|
|
2208
2252
|
.where('is_active', '=', true)
|
|
2209
2253
|
.where((eb: any) => eb.or([
|
|
2210
2254
|
eb('tenant_id', '=', tenantId),
|
|
2211
2255
|
eb('tenant_id', 'is', null),
|
|
2212
2256
|
]))
|
|
2213
|
-
.execute() as Array<
|
|
2214
|
-
const
|
|
2257
|
+
.execute() as Array<Record<string, unknown>>
|
|
2258
|
+
const result: CustomFieldDefRow[] = []
|
|
2215
2259
|
for (const row of rows) {
|
|
2216
2260
|
const key = row.key
|
|
2217
|
-
|
|
2218
|
-
|
|
2261
|
+
const normalized = typeof key === 'string' ? key.trim() : key == null ? '' : String(key)
|
|
2262
|
+
if (!normalized.length) continue
|
|
2263
|
+
const entityId = typeof row.entity_id === 'string' ? row.entity_id : String(row.entity_id ?? '')
|
|
2264
|
+
result.push({
|
|
2265
|
+
entityId,
|
|
2266
|
+
key: normalized,
|
|
2267
|
+
kind: row.kind,
|
|
2268
|
+
organizationId: typeof row.organization_id === 'string' ? row.organization_id : null,
|
|
2269
|
+
tenantId: typeof row.tenant_id === 'string' ? row.tenant_id : null,
|
|
2270
|
+
configJson: row.config_json ?? null,
|
|
2271
|
+
updatedAt: row.updated_at ?? null,
|
|
2272
|
+
})
|
|
2219
2273
|
}
|
|
2220
|
-
const result = Array.from(keys)
|
|
2221
2274
|
if (this.customFieldKeysTtlMs > 0) {
|
|
2222
|
-
this.
|
|
2275
|
+
this.customFieldDefsCache.set(cacheKey, { expiresAt: now + this.customFieldKeysTtlMs, value: result })
|
|
2223
2276
|
}
|
|
2224
2277
|
return result.slice()
|
|
2225
2278
|
}
|
|
2226
2279
|
|
|
2280
|
+
private async resolveAvailableCustomFieldKeys(entityIds: string[], tenantId: string | null): Promise<string[]> {
|
|
2281
|
+
const defs = await this.resolveCustomFieldDefs(entityIds, tenantId)
|
|
2282
|
+
return Array.from(new Set(defs.map((def) => def.key)))
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
/**
|
|
2286
|
+
* Kind lookup for the `cf:`/`cf_` keys of an index document, so encrypted string-typed
|
|
2287
|
+
* custom fields keep their type on read instead of being `JSON.parse`d back into numbers
|
|
2288
|
+
* or booleans (issue #5968). Backed by the same TTL cache as the key lookup, so this adds
|
|
2289
|
+
* no round trip on a warm cache and fails open to an empty index on a cold-cache error.
|
|
2290
|
+
*
|
|
2291
|
+
* The index keeps every scope's definition per key rather than collapsing to one kind:
|
|
2292
|
+
* the caller resolves the winner against each row's own `organization_id`, the same scope
|
|
2293
|
+
* the row is decrypted with, so an organization's override never types another
|
|
2294
|
+
* organization's value.
|
|
2295
|
+
*
|
|
2296
|
+
* `entityIds` MUST arrive in `indexSources` order: `buildCfJsonExprSql` reads a `cf` value
|
|
2297
|
+
* with `coalesce(source0, source1, …)`, so a key defined by several sources has to be typed
|
|
2298
|
+
* by the same source the value came from — hence the priority-ordered merge rather than
|
|
2299
|
+
* whatever order the rows happen to come back in.
|
|
2300
|
+
*/
|
|
2301
|
+
private async resolveCustomFieldKindIndex(
|
|
2302
|
+
entityIds: string[],
|
|
2303
|
+
tenantId: string | null,
|
|
2304
|
+
): Promise<CustomFieldKindIndex> {
|
|
2305
|
+
try {
|
|
2306
|
+
const defs = await this.resolveCustomFieldDefs(entityIds, tenantId)
|
|
2307
|
+
const byEntityId = new Map<string, CustomFieldDefRow[]>()
|
|
2308
|
+
for (const def of defs) {
|
|
2309
|
+
const group = byEntityId.get(def.entityId) ?? []
|
|
2310
|
+
group.push(def)
|
|
2311
|
+
byEntityId.set(def.entityId, group)
|
|
2312
|
+
}
|
|
2313
|
+
return mergeCustomFieldKindIndexes(
|
|
2314
|
+
entityIds.map((entityId) => buildCustomFieldKindIndex(byEntityId.get(entityId) ?? [])),
|
|
2315
|
+
)
|
|
2316
|
+
} catch (err) {
|
|
2317
|
+
logger.warn('Failed to resolve custom field kinds', { entityIds, err })
|
|
2318
|
+
return emptyCustomFieldKindIndex()
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2227
2322
|
private async entityHasActiveCustomFields(entityId: string, tenantId: string | null): Promise<boolean> {
|
|
2228
2323
|
try {
|
|
2229
2324
|
const keys = await this.resolveAvailableCustomFieldKeys([entityId], tenantId)
|