@open-mercato/core 0.6.6-develop.6205.1.109e4b6a84 → 0.6.6-develop.6227.1.2695efff30
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/currencies/backend/currencies/[id]/page.js +33 -8
- package/dist/modules/currencies/backend/currencies/[id]/page.js.map +2 -2
- package/dist/modules/currencies/backend/currencies/page.js +63 -27
- package/dist/modules/currencies/backend/currencies/page.js.map +2 -2
- package/dist/modules/currencies/backend/exchange-rates/page.js +35 -14
- package/dist/modules/currencies/backend/exchange-rates/page.js.map +2 -2
- package/dist/modules/currencies/components/CurrencyFetchingConfig.js +104 -41
- package/dist/modules/currencies/components/CurrencyFetchingConfig.js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/roles/[id]/acl.js +57 -8
- package/dist/modules/customer_accounts/api/admin/roles/[id]/acl.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js +5 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/page.js +30 -14
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/page.js.map +2 -2
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js +29 -19
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js.map +2 -2
- package/dist/modules/customers/backend/config/customers/pipeline-stages/page.js +166 -64
- package/dist/modules/customers/backend/config/customers/pipeline-stages/page.js.map +2 -2
- package/dist/modules/customers/components/AddressFormatSettings.js +31 -16
- package/dist/modules/customers/components/AddressFormatSettings.js.map +2 -2
- package/dist/modules/customers/components/DictionarySettings.js +57 -31
- package/dist/modules/customers/components/DictionarySettings.js.map +2 -2
- package/dist/modules/customers/components/PipelineSettings.js +156 -76
- package/dist/modules/customers/components/PipelineSettings.js.map +2 -2
- package/dist/modules/customers/components/calendar/AgendaList.js +10 -4
- package/dist/modules/customers/components/calendar/AgendaList.js.map +2 -2
- package/dist/modules/customers/components/calendar/CalendarSettingsModal.js +18 -2
- package/dist/modules/customers/components/calendar/CalendarSettingsModal.js.map +2 -2
- package/dist/modules/customers/components/calendar/MonthGrid.js +5 -2
- package/dist/modules/customers/components/calendar/MonthGrid.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/PeopleField.js +6 -0
- package/dist/modules/customers/components/calendar/editor/PeopleField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/RelatedToField.js +2 -0
- package/dist/modules/customers/components/calendar/editor/RelatedToField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/ScheduleSection.js +4 -1
- package/dist/modules/customers/components/calendar/editor/ScheduleSection.js.map +2 -2
- package/dist/modules/customers/lib/calendar/labels.js +33 -0
- package/dist/modules/customers/lib/calendar/labels.js.map +7 -0
- package/dist/modules/dashboards/components/WidgetVisibilityEditor.js +32 -11
- package/dist/modules/dashboards/components/WidgetVisibilityEditor.js.map +2 -2
- package/dist/modules/data_sync/api/mappings/[id]/route.js +59 -0
- package/dist/modules/data_sync/api/mappings/[id]/route.js.map +2 -2
- package/dist/modules/data_sync/api/mappings/route.js +44 -0
- package/dist/modules/data_sync/api/mappings/route.js.map +2 -2
- package/dist/modules/data_sync/api/run.js +31 -0
- package/dist/modules/data_sync/api/run.js.map +2 -2
- package/dist/modules/data_sync/api/runs/[id]/cancel.js +31 -0
- package/dist/modules/data_sync/api/runs/[id]/cancel.js.map +2 -2
- package/dist/modules/data_sync/api/runs/[id]/retry.js +31 -0
- package/dist/modules/data_sync/api/runs/[id]/retry.js.map +2 -2
- package/dist/modules/data_sync/api/schedules/[id]/route.js +59 -3
- package/dist/modules/data_sync/api/schedules/[id]/route.js.map +2 -2
- package/dist/modules/data_sync/api/schedules/route.js +33 -4
- package/dist/modules/data_sync/api/schedules/route.js.map +2 -2
- package/dist/modules/directory/api/organization-branding/route.js +54 -2
- package/dist/modules/directory/api/organization-branding/route.js.map +2 -2
- package/dist/modules/directory/backend/directory/branding/page.js +15 -9
- package/dist/modules/directory/backend/directory/branding/page.js.map +2 -2
- package/dist/modules/directory/backend/directory/organizations/page.js +28 -9
- package/dist/modules/directory/backend/directory/organizations/page.js.map +2 -2
- package/dist/modules/directory/backend/directory/tenants/page.js +29 -13
- package/dist/modules/directory/backend/directory/tenants/page.js.map +2 -2
- package/dist/modules/entities/api/definitions.batch.js +15 -0
- package/dist/modules/entities/api/definitions.batch.js.map +2 -2
- package/dist/modules/entities/api/definitions.js +26 -0
- package/dist/modules/entities/api/definitions.js.map +2 -2
- package/dist/modules/entities/api/definitions.mutation-guard.js +57 -0
- package/dist/modules/entities/api/definitions.mutation-guard.js.map +7 -0
- package/dist/modules/entities/api/definitions.restore.js +15 -0
- package/dist/modules/entities/api/definitions.restore.js.map +2 -2
- package/dist/modules/entities/api/entities.js +31 -3
- package/dist/modules/entities/api/entities.js.map +2 -2
- package/dist/modules/entities/api/records.js +18 -0
- package/dist/modules/entities/api/records.js.map +2 -2
- package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js +28 -10
- package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js.map +2 -2
- package/dist/modules/feature_toggles/api/overrides/route.js +38 -1
- package/dist/modules/feature_toggles/api/overrides/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/emails/[id]/route.js +30 -0
- package/dist/modules/inbox_ops/api/emails/[id]/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/[id]/accept-all/route.js +30 -0
- package/dist/modules/inbox_ops/api/proposals/[id]/accept-all/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.js +31 -0
- package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/[id]/categorize/route.js +31 -0
- package/dist/modules/inbox_ops/api/proposals/[id]/categorize/route.js.map +2 -2
- package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js +68 -39
- package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js.map +2 -2
- package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js +14 -4
- package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js.map +2 -2
- package/dist/modules/payment_gateways/api/cancel/route.js +37 -0
- package/dist/modules/payment_gateways/api/cancel/route.js.map +2 -2
- package/dist/modules/payment_gateways/api/capture/route.js +37 -0
- package/dist/modules/payment_gateways/api/capture/route.js.map +2 -2
- package/dist/modules/payment_gateways/api/guards.js +31 -0
- package/dist/modules/payment_gateways/api/guards.js.map +7 -0
- package/dist/modules/payment_gateways/api/refund/route.js +37 -0
- package/dist/modules/payment_gateways/api/refund/route.js.map +2 -2
- package/dist/modules/payment_gateways/api/sessions/route.js +37 -0
- package/dist/modules/payment_gateways/api/sessions/route.js.map +2 -2
- package/dist/modules/planner/api/availability-date-specific.js +11 -1
- package/dist/modules/planner/api/availability-date-specific.js.map +2 -2
- package/dist/modules/planner/backend/planner/availability-rulesets/page.js +20 -3
- package/dist/modules/planner/backend/planner/availability-rulesets/page.js.map +2 -2
- package/dist/modules/planner/commands/availability-date-specific.js +16 -0
- package/dist/modules/planner/commands/availability-date-specific.js.map +2 -2
- package/dist/modules/planner/components/AvailabilityRulesEditor.js +109 -40
- package/dist/modules/planner/components/AvailabilityRulesEditor.js.map +2 -2
- package/dist/modules/query_index/api/purge.js +35 -3
- package/dist/modules/query_index/api/purge.js.map +2 -2
- package/dist/modules/query_index/api/reindex.js +41 -3
- package/dist/modules/query_index/api/reindex.js.map +2 -2
- package/dist/modules/query_index/components/QueryIndexesTable.js +57 -24
- package/dist/modules/query_index/components/QueryIndexesTable.js.map +2 -2
- package/dist/modules/shipping_carriers/api/shipments/route.js +31 -0
- package/dist/modules/shipping_carriers/api/shipments/route.js.map +2 -2
- package/dist/modules/shipping_carriers/api/tracking/refresh/route.js +55 -0
- package/dist/modules/shipping_carriers/api/tracking/refresh/route.js.map +7 -0
- package/dist/modules/shipping_carriers/data/validators.js +6 -1
- package/dist/modules/shipping_carriers/data/validators.js.map +2 -2
- package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.js +28 -8
- package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.js.map +2 -2
- package/dist/modules/shipping_carriers/lib/shipping-service.js +37 -7
- package/dist/modules/shipping_carriers/lib/shipping-service.js.map +2 -2
- package/dist/modules/shipping_carriers/workers/status-poller.js +1 -1
- package/dist/modules/shipping_carriers/workers/status-poller.js.map +2 -2
- package/dist/modules/translations/components/TranslationManager.js +49 -20
- package/dist/modules/translations/components/TranslationManager.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/currencies/backend/currencies/[id]/page.tsx +40 -10
- package/src/modules/currencies/backend/currencies/page.tsx +68 -29
- package/src/modules/currencies/backend/exchange-rates/page.tsx +40 -15
- package/src/modules/currencies/components/CurrencyFetchingConfig.tsx +110 -41
- package/src/modules/customer_accounts/api/admin/roles/[id]/acl.ts +69 -7
- package/src/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.tsx +16 -8
- package/src/modules/customer_accounts/backend/customer_accounts/roles/page.tsx +32 -14
- package/src/modules/customer_accounts/widgets/injection/account-status/widget.client.tsx +32 -20
- package/src/modules/customers/backend/config/customers/pipeline-stages/page.tsx +171 -64
- package/src/modules/customers/components/AddressFormatSettings.tsx +39 -19
- package/src/modules/customers/components/DictionarySettings.tsx +63 -29
- package/src/modules/customers/components/PipelineSettings.tsx +165 -80
- package/src/modules/customers/components/calendar/AgendaList.tsx +13 -8
- package/src/modules/customers/components/calendar/CalendarSettingsModal.tsx +16 -2
- package/src/modules/customers/components/calendar/MonthGrid.tsx +5 -2
- package/src/modules/customers/components/calendar/editor/PeopleField.tsx +6 -0
- package/src/modules/customers/components/calendar/editor/RelatedToField.tsx +2 -0
- package/src/modules/customers/components/calendar/editor/ScheduleSection.tsx +7 -0
- package/src/modules/customers/i18n/de.json +11 -7
- package/src/modules/customers/i18n/en.json +6 -2
- package/src/modules/customers/i18n/es.json +11 -7
- package/src/modules/customers/i18n/pl.json +11 -7
- package/src/modules/customers/lib/calendar/labels.ts +42 -0
- package/src/modules/dashboards/components/WidgetVisibilityEditor.tsx +39 -11
- package/src/modules/data_sync/api/mappings/[id]/route.ts +63 -0
- package/src/modules/data_sync/api/mappings/route.ts +48 -0
- package/src/modules/data_sync/api/run.ts +33 -0
- package/src/modules/data_sync/api/runs/[id]/cancel.ts +34 -0
- package/src/modules/data_sync/api/runs/[id]/retry.ts +33 -0
- package/src/modules/data_sync/api/schedules/[id]/route.ts +64 -2
- package/src/modules/data_sync/api/schedules/route.ts +36 -4
- package/src/modules/directory/api/organization-branding/route.ts +61 -0
- package/src/modules/directory/backend/directory/branding/page.tsx +16 -10
- package/src/modules/directory/backend/directory/organizations/page.tsx +35 -8
- package/src/modules/directory/backend/directory/tenants/page.tsx +37 -13
- package/src/modules/entities/api/definitions.batch.ts +17 -0
- package/src/modules/entities/api/definitions.mutation-guard.ts +80 -0
- package/src/modules/entities/api/definitions.restore.ts +17 -0
- package/src/modules/entities/api/definitions.ts +30 -0
- package/src/modules/entities/api/entities.ts +35 -3
- package/src/modules/entities/api/records.ts +20 -0
- package/src/modules/entities/backend/entities/user/[entityId]/records/page.tsx +33 -10
- package/src/modules/feature_toggles/api/overrides/route.ts +44 -1
- package/src/modules/inbox_ops/api/emails/[id]/route.ts +32 -0
- package/src/modules/inbox_ops/api/proposals/[id]/accept-all/route.ts +32 -0
- package/src/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.ts +33 -0
- package/src/modules/inbox_ops/api/proposals/[id]/categorize/route.ts +33 -0
- package/src/modules/messages/components/message-detail/hooks/useMessageDetailsActions.ts +80 -42
- package/src/modules/notifications/frontend/NotificationSettingsPageClient.tsx +21 -4
- package/src/modules/payment_gateways/api/cancel/route.ts +39 -0
- package/src/modules/payment_gateways/api/capture/route.ts +39 -0
- package/src/modules/payment_gateways/api/guards.ts +59 -0
- package/src/modules/payment_gateways/api/refund/route.ts +39 -0
- package/src/modules/payment_gateways/api/sessions/route.ts +40 -0
- package/src/modules/planner/api/availability-date-specific.ts +10 -0
- package/src/modules/planner/backend/planner/availability-rulesets/page.tsx +26 -5
- package/src/modules/planner/commands/availability-date-specific.ts +24 -0
- package/src/modules/planner/components/AvailabilityRulesEditor.tsx +122 -41
- package/src/modules/query_index/api/purge.ts +37 -3
- package/src/modules/query_index/api/reindex.ts +43 -3
- package/src/modules/query_index/components/QueryIndexesTable.tsx +66 -24
- package/src/modules/shipping_carriers/api/shipments/route.ts +31 -0
- package/src/modules/shipping_carriers/api/tracking/refresh/route.ts +53 -0
- package/src/modules/shipping_carriers/data/validators.ts +5 -0
- package/src/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.ts +29 -8
- package/src/modules/shipping_carriers/lib/shipping-service.ts +43 -7
- package/src/modules/shipping_carriers/workers/status-poller.ts +1 -1
- package/src/modules/translations/components/TranslationManager.tsx +65 -21
|
@@ -7,6 +7,10 @@ import type { SyncScheduleService } from '../../lib/sync-schedule-service'
|
|
|
7
7
|
import { serializeSchedule } from './serialize'
|
|
8
8
|
import { readOptimisticLockExpected } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
|
|
9
9
|
import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
10
|
+
import {
|
|
11
|
+
runCrudMutationGuardAfterSuccess,
|
|
12
|
+
validateCrudMutationGuard,
|
|
13
|
+
} from '@open-mercato/shared/lib/crud/mutation-guard'
|
|
10
14
|
|
|
11
15
|
export const metadata = {
|
|
12
16
|
GET: { requireAuth: true, requireFeatures: ['data_sync.configure'] },
|
|
@@ -65,15 +69,43 @@ export async function POST(req: Request) {
|
|
|
65
69
|
|
|
66
70
|
const container = await createRequestContainer()
|
|
67
71
|
const scheduleService = container.resolve('dataSyncScheduleService') as SyncScheduleService
|
|
72
|
+
const scope = { organizationId: auth.orgId as string, tenantId: auth.tenantId }
|
|
73
|
+
|
|
74
|
+
const guardResult = await validateCrudMutationGuard(container, {
|
|
75
|
+
tenantId: auth.tenantId,
|
|
76
|
+
organizationId: scope.organizationId,
|
|
77
|
+
userId: auth.sub,
|
|
78
|
+
resourceKind: 'data_sync.schedule',
|
|
79
|
+
resourceId: scope.organizationId,
|
|
80
|
+
operation: 'create',
|
|
81
|
+
requestMethod: req.method,
|
|
82
|
+
requestHeaders: req.headers,
|
|
83
|
+
mutationPayload: parsed.data,
|
|
84
|
+
})
|
|
85
|
+
if (guardResult && !guardResult.ok) {
|
|
86
|
+
return NextResponse.json(guardResult.body, { status: guardResult.status })
|
|
87
|
+
}
|
|
68
88
|
|
|
69
89
|
try {
|
|
70
90
|
const schedule = await scheduleService.saveSchedule({
|
|
71
91
|
...parsed.data,
|
|
72
92
|
expectedUpdatedAt: readOptimisticLockExpected(req),
|
|
73
|
-
},
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
93
|
+
}, scope)
|
|
94
|
+
|
|
95
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
96
|
+
await runCrudMutationGuardAfterSuccess(container, {
|
|
97
|
+
tenantId: auth.tenantId,
|
|
98
|
+
organizationId: scope.organizationId,
|
|
99
|
+
userId: auth.sub,
|
|
100
|
+
resourceKind: 'data_sync.schedule',
|
|
101
|
+
resourceId: schedule.id,
|
|
102
|
+
operation: 'create',
|
|
103
|
+
requestMethod: req.method,
|
|
104
|
+
requestHeaders: req.headers,
|
|
105
|
+
metadata: guardResult.metadata ?? null,
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
77
109
|
return NextResponse.json(serializeSchedule(schedule), { status: 201 })
|
|
78
110
|
} catch (error) {
|
|
79
111
|
if (isCrudHttpError(error)) {
|
|
@@ -7,6 +7,11 @@ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
|
7
7
|
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
8
8
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
9
9
|
import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
10
|
+
import {
|
|
11
|
+
runCrudMutationGuardAfterSuccess,
|
|
12
|
+
validateCrudMutationGuard,
|
|
13
|
+
} from '@open-mercato/shared/lib/crud/mutation-guard'
|
|
14
|
+
import { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
|
|
10
15
|
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
11
16
|
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
12
17
|
import { Organization } from '@open-mercato/core/modules/directory/data/entities'
|
|
@@ -24,6 +29,7 @@ const brandingResponseSchema = z.object({
|
|
|
24
29
|
organizationName: z.string(),
|
|
25
30
|
tenantId: z.string().uuid(),
|
|
26
31
|
logoUrl: z.string().nullable(),
|
|
32
|
+
updatedAt: z.string().nullable(),
|
|
27
33
|
})
|
|
28
34
|
|
|
29
35
|
const brandingUpdateSchema = z.object({
|
|
@@ -105,12 +111,25 @@ async function resolveCurrentOrganization(req: Request) {
|
|
|
105
111
|
return { auth, container, organization, organizationId, tenantId, translate }
|
|
106
112
|
}
|
|
107
113
|
|
|
114
|
+
function toIsoOrNull(value: Date | string | null | undefined): string | null {
|
|
115
|
+
if (value == null) return null
|
|
116
|
+
if (value instanceof Date) {
|
|
117
|
+
const ms = value.getTime()
|
|
118
|
+
return Number.isFinite(ms) ? new Date(ms).toISOString() : null
|
|
119
|
+
}
|
|
120
|
+
const trimmed = String(value).trim()
|
|
121
|
+
if (!trimmed) return null
|
|
122
|
+
const parsed = new Date(trimmed)
|
|
123
|
+
return Number.isFinite(parsed.getTime()) ? parsed.toISOString() : null
|
|
124
|
+
}
|
|
125
|
+
|
|
108
126
|
function toResponsePayload(organization: Organization, tenantId: string) {
|
|
109
127
|
return {
|
|
110
128
|
organizationId: String(organization.id),
|
|
111
129
|
organizationName: organization.name,
|
|
112
130
|
tenantId,
|
|
113
131
|
logoUrl: organization.logoUrl ?? null,
|
|
132
|
+
updatedAt: toIsoOrNull(organization.updatedAt),
|
|
114
133
|
}
|
|
115
134
|
}
|
|
116
135
|
|
|
@@ -169,6 +188,32 @@ export async function PUT(req: Request) {
|
|
|
169
188
|
}
|
|
170
189
|
|
|
171
190
|
try {
|
|
191
|
+
// Refuse a stale overwrite when two tabs edit the same organization branding.
|
|
192
|
+
// Strictly additive — a no-op when the client omits the expected-version header.
|
|
193
|
+
enforceCommandOptimisticLock({
|
|
194
|
+
resourceKind: 'directory.organization',
|
|
195
|
+
resourceId: resolved.organizationId,
|
|
196
|
+
current: resolved.organization.updatedAt ?? null,
|
|
197
|
+
request: req,
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
// Mutation-guard contract for custom write routes: validate before the write,
|
|
201
|
+
// then run the after-success hook once the command persists.
|
|
202
|
+
const guardResult = await validateCrudMutationGuard(resolved.container, {
|
|
203
|
+
tenantId: resolved.tenantId,
|
|
204
|
+
organizationId: resolved.organizationId,
|
|
205
|
+
userId: resolved.auth.sub,
|
|
206
|
+
resourceKind: 'directory.organization',
|
|
207
|
+
resourceId: resolved.organizationId,
|
|
208
|
+
operation: 'update',
|
|
209
|
+
requestMethod: req.method,
|
|
210
|
+
requestHeaders: req.headers,
|
|
211
|
+
mutationPayload: { logoUrl: parsed.data.logoUrl ?? null },
|
|
212
|
+
})
|
|
213
|
+
if (guardResult && !guardResult.ok) {
|
|
214
|
+
return NextResponse.json(guardResult.body, { status: guardResult.status })
|
|
215
|
+
}
|
|
216
|
+
|
|
172
217
|
const commandBus = resolved.container.resolve('commandBus') as CommandBus
|
|
173
218
|
const ctx = buildCommandContext(
|
|
174
219
|
resolved.container,
|
|
@@ -188,6 +233,21 @@ export async function PUT(req: Request) {
|
|
|
188
233
|
ctx,
|
|
189
234
|
},
|
|
190
235
|
)
|
|
236
|
+
|
|
237
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
238
|
+
await runCrudMutationGuardAfterSuccess(resolved.container, {
|
|
239
|
+
tenantId: resolved.tenantId,
|
|
240
|
+
organizationId: resolved.organizationId,
|
|
241
|
+
userId: resolved.auth.sub,
|
|
242
|
+
resourceKind: 'directory.organization',
|
|
243
|
+
resourceId: resolved.organizationId,
|
|
244
|
+
operation: 'update',
|
|
245
|
+
requestMethod: req.method,
|
|
246
|
+
requestHeaders: req.headers,
|
|
247
|
+
metadata: guardResult.metadata ?? null,
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
|
|
191
251
|
await invalidateSidebarBrandingCache(resolved.container, resolved.organizationId, resolved.tenantId)
|
|
192
252
|
return NextResponse.json(toResponsePayload(result, resolved.tenantId))
|
|
193
253
|
} catch (err) {
|
|
@@ -231,6 +291,7 @@ export const openApi: OpenApiRouteDoc = {
|
|
|
231
291
|
errors: [
|
|
232
292
|
{ status: 400, description: 'Save failed', schema: errorSchema },
|
|
233
293
|
{ status: 401, description: 'Unauthorized', schema: errorSchema },
|
|
294
|
+
{ status: 409, description: 'Organization branding changed since it was loaded', schema: errorSchema },
|
|
234
295
|
{ status: 422, description: 'Invalid logo URL', schema: errorSchema },
|
|
235
296
|
],
|
|
236
297
|
},
|
|
@@ -7,7 +7,9 @@ import { Page, PageBody, PageHeader } from '@open-mercato/ui/backend/Page'
|
|
|
7
7
|
import { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'
|
|
8
8
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
9
9
|
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
10
|
-
import { apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
10
|
+
import { apiCallOrThrow, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
|
|
11
|
+
import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
12
|
+
import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
|
|
11
13
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
12
14
|
import { Input } from '@open-mercato/ui/primitives/input'
|
|
13
15
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
@@ -17,6 +19,7 @@ type BrandingPayload = {
|
|
|
17
19
|
organizationName: string
|
|
18
20
|
tenantId: string
|
|
19
21
|
logoUrl: string | null
|
|
22
|
+
updatedAt: string | null
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
type UploadPayload = {
|
|
@@ -98,15 +101,17 @@ export default function OrganizationBrandingPage() {
|
|
|
98
101
|
operation: async () => {
|
|
99
102
|
const uploadedLogoUrl = shouldUpload ? await uploadLogo(data.organizationId) : null
|
|
100
103
|
const resolvedLogoUrl = uploadedLogoUrl ?? nextLogoUrl ?? logoUrl.trim()
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
104
|
+
const response = await withScopedApiRequestHeaders(
|
|
105
|
+
buildOptimisticLockHeader(data.updatedAt),
|
|
106
|
+
() => apiCallOrThrow<BrandingPayload>(
|
|
107
|
+
BRANDING_API,
|
|
108
|
+
{
|
|
109
|
+
method: 'PUT',
|
|
110
|
+
headers: { 'content-type': 'application/json' },
|
|
111
|
+
body: JSON.stringify({ logoUrl: resolvedLogoUrl || null }),
|
|
112
|
+
},
|
|
113
|
+
{ errorMessage: t('directory.branding.errors.save', 'Failed to update organization branding') },
|
|
114
|
+
),
|
|
110
115
|
)
|
|
111
116
|
return response.result
|
|
112
117
|
},
|
|
@@ -127,6 +132,7 @@ export default function OrganizationBrandingPage() {
|
|
|
127
132
|
if (fileInputRef.current) fileInputRef.current.value = ''
|
|
128
133
|
flash(t('directory.branding.flash.saved', 'Organization branding updated'), 'success')
|
|
129
134
|
} catch (err: unknown) {
|
|
135
|
+
if (surfaceRecordConflict(err, t)) return
|
|
130
136
|
const fallback = t('directory.branding.errors.save', 'Failed to update organization branding')
|
|
131
137
|
const message = err instanceof Error ? err.message : fallback
|
|
132
138
|
flash(message, 'error')
|
|
@@ -13,6 +13,8 @@ import { Button } from '@open-mercato/ui/primitives/button'
|
|
|
13
13
|
import { apiCall, apiCallOrThrow, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
|
|
14
14
|
import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
15
15
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
16
|
+
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
17
|
+
import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
|
|
16
18
|
import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
|
|
17
19
|
import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
|
|
18
20
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
@@ -177,6 +179,17 @@ export default function DirectoryOrganizationsPage() {
|
|
|
177
179
|
const total = data?.total ?? 0
|
|
178
180
|
const totalPages = data?.totalPages ?? 0
|
|
179
181
|
|
|
182
|
+
const deleteMutationContextId = 'directory-organizations-list:single-delete'
|
|
183
|
+
const { runMutation: runDeleteMutation, retryLastMutation: retryDeleteMutation } = useGuardedMutation<{
|
|
184
|
+
formId: string
|
|
185
|
+
resourceKind: string
|
|
186
|
+
resourceId: string
|
|
187
|
+
retryLastMutation: () => Promise<boolean>
|
|
188
|
+
}>({
|
|
189
|
+
contextId: deleteMutationContextId,
|
|
190
|
+
blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
|
|
191
|
+
})
|
|
192
|
+
|
|
180
193
|
const handleDelete = React.useCallback(async (org: OrganizationRow) => {
|
|
181
194
|
const confirmLabel = t('directory.organizations.list.confirmDelete', 'Archive organization "{{name}}"?', { name: org.name })
|
|
182
195
|
const confirmed = await confirm({
|
|
@@ -187,22 +200,36 @@ export default function DirectoryOrganizationsPage() {
|
|
|
187
200
|
if (!confirmed) return
|
|
188
201
|
|
|
189
202
|
try {
|
|
190
|
-
await
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
203
|
+
await runDeleteMutation({
|
|
204
|
+
operation: () => withScopedApiRequestHeaders(
|
|
205
|
+
buildOptimisticLockHeader(org.updatedAt),
|
|
206
|
+
() => apiCallOrThrow(
|
|
207
|
+
`/api/directory/organizations?id=${encodeURIComponent(org.id)}`,
|
|
208
|
+
{ method: 'DELETE' },
|
|
209
|
+
{ errorMessage: t('directory.organizations.list.error.delete', 'Failed to delete organization') },
|
|
210
|
+
),
|
|
196
211
|
),
|
|
197
|
-
|
|
212
|
+
context: {
|
|
213
|
+
formId: deleteMutationContextId,
|
|
214
|
+
resourceKind: 'directory.organization',
|
|
215
|
+
resourceId: org.id,
|
|
216
|
+
retryLastMutation: retryDeleteMutation,
|
|
217
|
+
},
|
|
218
|
+
})
|
|
198
219
|
await queryClient.invalidateQueries({ queryKey: ['directory-organizations'] })
|
|
199
220
|
flash(t('directory.organizations.flash.deleted', 'Organization deleted'), 'success')
|
|
200
221
|
} catch (err: unknown) {
|
|
222
|
+
// A stale delete surfaces the unified conflict bar (via the guarded
|
|
223
|
+
// mutation) — skip the generic error flash to avoid a double message.
|
|
224
|
+
if (surfaceRecordConflict(err, t)) {
|
|
225
|
+
await queryClient.invalidateQueries({ queryKey: ['directory-organizations'] })
|
|
226
|
+
return
|
|
227
|
+
}
|
|
201
228
|
const fallback = t('directory.organizations.list.error.delete', 'Failed to delete organization')
|
|
202
229
|
const message = err instanceof Error ? err.message : fallback
|
|
203
230
|
flash(message, 'error')
|
|
204
231
|
}
|
|
205
|
-
}, [confirm, queryClient, t])
|
|
232
|
+
}, [confirm, deleteMutationContextId, queryClient, retryDeleteMutation, runDeleteMutation, t])
|
|
206
233
|
|
|
207
234
|
return (
|
|
208
235
|
<Page>
|
|
@@ -10,10 +10,11 @@ import { RowActions } from '@open-mercato/ui/backend/RowActions'
|
|
|
10
10
|
import { BooleanIcon } from '@open-mercato/ui/backend/ValueIcons'
|
|
11
11
|
import type { FilterValues } from '@open-mercato/ui/backend/FilterBar'
|
|
12
12
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
13
|
-
import { apiCall, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
|
|
13
|
+
import { apiCall, apiCallOrThrow, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
|
|
14
14
|
import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
15
15
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
16
|
-
import {
|
|
16
|
+
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
17
|
+
import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
|
|
17
18
|
import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
|
|
18
19
|
import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
|
|
19
20
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
@@ -117,6 +118,17 @@ export default function DirectoryTenantsPage() {
|
|
|
117
118
|
const total = data?.total ?? 0
|
|
118
119
|
const totalPages = data?.totalPages ?? 0
|
|
119
120
|
|
|
121
|
+
const deleteMutationContextId = 'directory-tenants-list:single-delete'
|
|
122
|
+
const { runMutation: runDeleteMutation, retryLastMutation: retryDeleteMutation } = useGuardedMutation<{
|
|
123
|
+
formId: string
|
|
124
|
+
resourceKind: string
|
|
125
|
+
resourceId: string
|
|
126
|
+
retryLastMutation: () => Promise<boolean>
|
|
127
|
+
}>({
|
|
128
|
+
contextId: deleteMutationContextId,
|
|
129
|
+
blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
|
|
130
|
+
})
|
|
131
|
+
|
|
120
132
|
const handleDelete = React.useCallback(async (tenant: TenantRow) => {
|
|
121
133
|
const confirmMessage = t('directory.tenants.list.confirmDelete', 'Delete tenant "{{name}}"? This will archive it.').replace('{{name}}', tenant.name)
|
|
122
134
|
const confirmed = await confirm({
|
|
@@ -127,23 +139,35 @@ export default function DirectoryTenantsPage() {
|
|
|
127
139
|
if (!confirmed) return
|
|
128
140
|
|
|
129
141
|
try {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
142
|
+
await runDeleteMutation({
|
|
143
|
+
operation: () => withScopedApiRequestHeaders(
|
|
144
|
+
buildOptimisticLockHeader(tenant.updatedAt),
|
|
145
|
+
() => apiCallOrThrow(
|
|
146
|
+
`/api/directory/tenants?id=${encodeURIComponent(tenant.id)}`,
|
|
147
|
+
{ method: 'DELETE' },
|
|
148
|
+
{ errorMessage: t('directory.tenants.list.error.delete', 'Failed to delete tenant') },
|
|
149
|
+
),
|
|
135
150
|
),
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
151
|
+
context: {
|
|
152
|
+
formId: deleteMutationContextId,
|
|
153
|
+
resourceKind: 'directory.tenant',
|
|
154
|
+
resourceId: tenant.id,
|
|
155
|
+
retryLastMutation: retryDeleteMutation,
|
|
156
|
+
},
|
|
157
|
+
})
|
|
140
158
|
await queryClient.invalidateQueries({ queryKey: ['directory-tenants'] })
|
|
141
159
|
flash(t('directory.tenants.list.success.delete', 'Tenant deleted'), 'success')
|
|
142
|
-
} catch (err:
|
|
160
|
+
} catch (err: unknown) {
|
|
161
|
+
// A stale delete surfaces the unified conflict bar (via the guarded
|
|
162
|
+
// mutation) — skip the generic error flash to avoid a double message.
|
|
163
|
+
if (surfaceRecordConflict(err, t)) {
|
|
164
|
+
await queryClient.invalidateQueries({ queryKey: ['directory-tenants'] })
|
|
165
|
+
return
|
|
166
|
+
}
|
|
143
167
|
const message = err instanceof Error ? err.message : t('directory.tenants.list.error.delete', 'Failed to delete tenant')
|
|
144
168
|
flash(message, 'error')
|
|
145
169
|
}
|
|
146
|
-
}, [confirm, queryClient, t])
|
|
170
|
+
}, [confirm, deleteMutationContextId, queryClient, retryDeleteMutation, runDeleteMutation, t])
|
|
147
171
|
|
|
148
172
|
return (
|
|
149
173
|
<Page>
|
|
@@ -8,6 +8,10 @@ import { z } from 'zod'
|
|
|
8
8
|
import { invalidateDefinitionsCache } from './definitions.cache'
|
|
9
9
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
10
10
|
import { mergeEntityFieldsetConfig, normalizeEntityFieldsetConfig } from '../lib/fieldsets'
|
|
11
|
+
import {
|
|
12
|
+
beginEntitiesMutationGuard,
|
|
13
|
+
FIELD_DEFINITION_RESOURCE_KIND,
|
|
14
|
+
} from './definitions.mutation-guard'
|
|
11
15
|
|
|
12
16
|
export const metadata = {
|
|
13
17
|
POST: { requireAuth: true, requireFeatures: ['entities.definitions.manage'] },
|
|
@@ -67,6 +71,17 @@ export async function POST(req: Request) {
|
|
|
67
71
|
cache = resolve('cache') as CacheStrategy
|
|
68
72
|
} catch {}
|
|
69
73
|
|
|
74
|
+
const guard = await beginEntitiesMutationGuard({
|
|
75
|
+
container,
|
|
76
|
+
auth,
|
|
77
|
+
req,
|
|
78
|
+
resourceKind: FIELD_DEFINITION_RESOURCE_KIND,
|
|
79
|
+
resourceId: entityId,
|
|
80
|
+
operation: 'custom',
|
|
81
|
+
mutationPayload: { entityId, definitionCount: definitions.length },
|
|
82
|
+
})
|
|
83
|
+
if (guard.blockedResponse) return guard.blockedResponse
|
|
84
|
+
|
|
70
85
|
await em.begin()
|
|
71
86
|
try {
|
|
72
87
|
// Prefetch every existing definition for this entity in a single query, then index
|
|
@@ -134,6 +149,8 @@ export async function POST(req: Request) {
|
|
|
134
149
|
return NextResponse.json({ error: 'Failed to save definitions batch' }, { status: 500 })
|
|
135
150
|
}
|
|
136
151
|
|
|
152
|
+
await guard.runAfterSuccess()
|
|
153
|
+
|
|
137
154
|
await invalidateDefinitionsCache(cache, {
|
|
138
155
|
tenantId: auth.tenantId ?? null,
|
|
139
156
|
organizationId: auth.orgId ?? null,
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import type { AwilixContainer } from 'awilix'
|
|
3
|
+
import type { AuthContext } from '@open-mercato/shared/lib/auth/server'
|
|
4
|
+
import {
|
|
5
|
+
runCrudMutationGuardAfterSuccess,
|
|
6
|
+
validateCrudMutationGuard,
|
|
7
|
+
} from '@open-mercato/shared/lib/crud/mutation-guard'
|
|
8
|
+
|
|
9
|
+
export const ENTITY_DEFINITION_RESOURCE_KIND = 'entities.entity'
|
|
10
|
+
export const FIELD_DEFINITION_RESOURCE_KIND = 'entities.field_definition'
|
|
11
|
+
|
|
12
|
+
type AuthenticatedContext = NonNullable<AuthContext>
|
|
13
|
+
|
|
14
|
+
type GuardOperation = 'create' | 'update' | 'delete' | 'custom'
|
|
15
|
+
|
|
16
|
+
type EntitiesMutationGuardContext = {
|
|
17
|
+
container: AwilixContainer
|
|
18
|
+
auth: AuthenticatedContext
|
|
19
|
+
req: Request
|
|
20
|
+
resourceKind: string
|
|
21
|
+
resourceId: string
|
|
22
|
+
operation: GuardOperation
|
|
23
|
+
mutationPayload?: Record<string, unknown> | null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type EntitiesMutationGuardHandle = {
|
|
27
|
+
blockedResponse: NextResponse | null
|
|
28
|
+
runAfterSuccess: () => Promise<void>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function resolveActorUserId(auth: AuthenticatedContext): string {
|
|
32
|
+
return auth.userId ?? auth.sub
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function beginEntitiesMutationGuard(
|
|
36
|
+
ctx: EntitiesMutationGuardContext,
|
|
37
|
+
): Promise<EntitiesMutationGuardHandle> {
|
|
38
|
+
const tenantId = ctx.auth.tenantId
|
|
39
|
+
const userId = resolveActorUserId(ctx.auth)
|
|
40
|
+
if (!tenantId) {
|
|
41
|
+
return { blockedResponse: null, runAfterSuccess: async () => undefined }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const guardResult = await validateCrudMutationGuard(ctx.container, {
|
|
45
|
+
tenantId,
|
|
46
|
+
organizationId: ctx.auth.orgId ?? null,
|
|
47
|
+
userId,
|
|
48
|
+
resourceKind: ctx.resourceKind,
|
|
49
|
+
resourceId: ctx.resourceId,
|
|
50
|
+
operation: ctx.operation,
|
|
51
|
+
requestMethod: ctx.req.method,
|
|
52
|
+
requestHeaders: ctx.req.headers,
|
|
53
|
+
mutationPayload: ctx.mutationPayload ?? null,
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
if (guardResult && !guardResult.ok) {
|
|
57
|
+
return {
|
|
58
|
+
blockedResponse: NextResponse.json(guardResult.body, { status: guardResult.status }),
|
|
59
|
+
runAfterSuccess: async () => undefined,
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
blockedResponse: null,
|
|
65
|
+
runAfterSuccess: async () => {
|
|
66
|
+
if (!guardResult?.ok || !guardResult.shouldRunAfterSuccess) return
|
|
67
|
+
await runCrudMutationGuardAfterSuccess(ctx.container, {
|
|
68
|
+
tenantId,
|
|
69
|
+
organizationId: ctx.auth.orgId ?? null,
|
|
70
|
+
userId,
|
|
71
|
+
resourceKind: ctx.resourceKind,
|
|
72
|
+
resourceId: ctx.resourceId,
|
|
73
|
+
operation: ctx.operation,
|
|
74
|
+
requestMethod: ctx.req.method,
|
|
75
|
+
requestHeaders: ctx.req.headers,
|
|
76
|
+
metadata: guardResult.metadata ?? null,
|
|
77
|
+
})
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -6,6 +6,10 @@ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
|
6
6
|
import { CustomFieldDef } from '@open-mercato/core/modules/entities/data/entities'
|
|
7
7
|
import { invalidateDefinitionsCache } from './definitions.cache'
|
|
8
8
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
9
|
+
import {
|
|
10
|
+
beginEntitiesMutationGuard,
|
|
11
|
+
FIELD_DEFINITION_RESOURCE_KIND,
|
|
12
|
+
} from './definitions.mutation-guard'
|
|
9
13
|
|
|
10
14
|
export const metadata = {
|
|
11
15
|
POST: { requireAuth: true, requireFeatures: ['entities.definitions.manage'] },
|
|
@@ -30,11 +34,24 @@ export async function POST(req: Request) {
|
|
|
30
34
|
const where: any = { entityId, key, organizationId: auth.orgId ?? null, tenantId: auth.tenantId ?? null }
|
|
31
35
|
const def = await em.findOne(CustomFieldDef, where)
|
|
32
36
|
if (!def) return NextResponse.json({ error: 'Not found' }, { status: 404 })
|
|
37
|
+
|
|
38
|
+
const guard = await beginEntitiesMutationGuard({
|
|
39
|
+
container,
|
|
40
|
+
auth,
|
|
41
|
+
req,
|
|
42
|
+
resourceKind: FIELD_DEFINITION_RESOURCE_KIND,
|
|
43
|
+
resourceId: def.id,
|
|
44
|
+
operation: 'custom',
|
|
45
|
+
mutationPayload: { entityId, key },
|
|
46
|
+
})
|
|
47
|
+
if (guard.blockedResponse) return guard.blockedResponse
|
|
48
|
+
|
|
33
49
|
;(def as any).deletedAt = null
|
|
34
50
|
;(def as any).isActive = true
|
|
35
51
|
;(def as any).updatedAt = new Date()
|
|
36
52
|
em.persist(def)
|
|
37
53
|
await em.flush()
|
|
54
|
+
await guard.runAfterSuccess()
|
|
38
55
|
await invalidateDefinitionsCache(cache, {
|
|
39
56
|
tenantId: auth.tenantId ?? null,
|
|
40
57
|
organizationId: auth.orgId ?? null,
|
|
@@ -20,6 +20,10 @@ import { installCustomEntitiesFromModules } from '../lib/install-from-ce'
|
|
|
20
20
|
import { normalizeCustomFieldOptions } from '@open-mercato/shared/modules/entities/options'
|
|
21
21
|
import { CURRENCY_OPTIONS_URL } from '@open-mercato/shared/modules/entities/kinds'
|
|
22
22
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
23
|
+
import {
|
|
24
|
+
beginEntitiesMutationGuard,
|
|
25
|
+
FIELD_DEFINITION_RESOURCE_KIND,
|
|
26
|
+
} from './definitions.mutation-guard'
|
|
23
27
|
|
|
24
28
|
/**
|
|
25
29
|
* Validate defaultValue against the field kind. Returns an error message string
|
|
@@ -473,6 +477,18 @@ export async function POST(req: Request) {
|
|
|
473
477
|
|
|
474
478
|
const where: any = { entityId: input.entityId, key: input.key, organizationId: auth.orgId ?? null, tenantId: auth.tenantId ?? null }
|
|
475
479
|
let def = await em.findOne(CustomFieldDef, where)
|
|
480
|
+
|
|
481
|
+
const guard = await beginEntitiesMutationGuard({
|
|
482
|
+
container,
|
|
483
|
+
auth,
|
|
484
|
+
req,
|
|
485
|
+
resourceKind: FIELD_DEFINITION_RESOURCE_KIND,
|
|
486
|
+
resourceId: def ? def.id : `${input.entityId}:${input.key}`,
|
|
487
|
+
operation: def ? 'update' : 'create',
|
|
488
|
+
mutationPayload: input as unknown as Record<string, unknown>,
|
|
489
|
+
})
|
|
490
|
+
if (guard.blockedResponse) return guard.blockedResponse
|
|
491
|
+
|
|
476
492
|
if (!def) def = em.create(CustomFieldDef, { ...where, createdAt: new Date() })
|
|
477
493
|
def.kind = input.kind
|
|
478
494
|
const inCfg = (input as any).configJson ?? {}
|
|
@@ -508,6 +524,7 @@ export async function POST(req: Request) {
|
|
|
508
524
|
def.updatedAt = new Date()
|
|
509
525
|
em.persist(def)
|
|
510
526
|
await em.flush()
|
|
527
|
+
await guard.runAfterSuccess()
|
|
511
528
|
await invalidateDefinitionsCache(cache, {
|
|
512
529
|
tenantId: auth.tenantId ?? null,
|
|
513
530
|
organizationId: auth.orgId ?? null,
|
|
@@ -535,11 +552,24 @@ export async function DELETE(req: Request) {
|
|
|
535
552
|
const where: any = { entityId, key, organizationId: auth.orgId ?? null, tenantId: auth.tenantId ?? null }
|
|
536
553
|
const def = await em.findOne(CustomFieldDef, where)
|
|
537
554
|
if (!def) return NextResponse.json({ error: 'Not found' }, { status: 404 })
|
|
555
|
+
|
|
556
|
+
const guard = await beginEntitiesMutationGuard({
|
|
557
|
+
container,
|
|
558
|
+
auth,
|
|
559
|
+
req,
|
|
560
|
+
resourceKind: FIELD_DEFINITION_RESOURCE_KIND,
|
|
561
|
+
resourceId: def.id,
|
|
562
|
+
operation: 'delete',
|
|
563
|
+
mutationPayload: { entityId, key },
|
|
564
|
+
})
|
|
565
|
+
if (guard.blockedResponse) return guard.blockedResponse
|
|
566
|
+
|
|
538
567
|
def.isActive = false
|
|
539
568
|
def.updatedAt = new Date()
|
|
540
569
|
def.deletedAt = def.deletedAt ?? new Date()
|
|
541
570
|
em.persist(def)
|
|
542
571
|
await em.flush()
|
|
572
|
+
await guard.runAfterSuccess()
|
|
543
573
|
await invalidateDefinitionsCache(cache, {
|
|
544
574
|
tenantId: auth.tenantId ?? null,
|
|
545
575
|
organizationId: auth.orgId ?? null,
|
|
@@ -10,8 +10,12 @@ import { isSystemEntitySelectable } from '@open-mercato/shared/lib/entities/syst
|
|
|
10
10
|
import { SYSTEM_ENTITY_RECORDS_BLOCKED_CODE, isOrmBackedSystemEntityId } from '@open-mercato/shared/lib/data/engine'
|
|
11
11
|
import { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
|
|
12
12
|
import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
13
|
+
import {
|
|
14
|
+
beginEntitiesMutationGuard,
|
|
15
|
+
ENTITY_DEFINITION_RESOURCE_KIND,
|
|
16
|
+
} from './definitions.mutation-guard'
|
|
13
17
|
|
|
14
|
-
const CUSTOM_ENTITY_DEFINITION_RESOURCE_KIND =
|
|
18
|
+
const CUSTOM_ENTITY_DEFINITION_RESOURCE_KIND = ENTITY_DEFINITION_RESOURCE_KIND
|
|
15
19
|
|
|
16
20
|
export const metadata = {
|
|
17
21
|
GET: { requireAuth: true },
|
|
@@ -110,7 +114,8 @@ export async function POST(req: Request) {
|
|
|
110
114
|
}
|
|
111
115
|
const input = parsed.data
|
|
112
116
|
|
|
113
|
-
const
|
|
117
|
+
const container = await createRequestContainer()
|
|
118
|
+
const { resolve } = container
|
|
114
119
|
const em = resolve('em') as any
|
|
115
120
|
|
|
116
121
|
// A registration for a module-declared, table-backed system entity would flip
|
|
@@ -140,6 +145,18 @@ export async function POST(req: Request) {
|
|
|
140
145
|
throw lockError
|
|
141
146
|
}
|
|
142
147
|
}
|
|
148
|
+
|
|
149
|
+
const guard = await beginEntitiesMutationGuard({
|
|
150
|
+
container,
|
|
151
|
+
auth,
|
|
152
|
+
req,
|
|
153
|
+
resourceKind: ENTITY_DEFINITION_RESOURCE_KIND,
|
|
154
|
+
resourceId: ent ? ent.id : input.entityId,
|
|
155
|
+
operation: ent ? 'update' : 'create',
|
|
156
|
+
mutationPayload: input as unknown as Record<string, unknown>,
|
|
157
|
+
})
|
|
158
|
+
if (guard.blockedResponse) return guard.blockedResponse
|
|
159
|
+
|
|
143
160
|
if (!ent) ent = em.create(CustomEntity, { ...where, createdAt: new Date() })
|
|
144
161
|
ent.label = input.label
|
|
145
162
|
ent.description = input.description ?? null
|
|
@@ -150,6 +167,7 @@ export async function POST(req: Request) {
|
|
|
150
167
|
ent.updatedAt = new Date()
|
|
151
168
|
em.persist(ent)
|
|
152
169
|
await em.flush()
|
|
170
|
+
await guard.runAfterSuccess()
|
|
153
171
|
// Invalidate sidebar/nav cache for tenant scope (also when tenantId is null)
|
|
154
172
|
try {
|
|
155
173
|
const cache = (await createRequestContainer()).resolve('cache') as any
|
|
@@ -168,17 +186,31 @@ export async function DELETE(req: Request) {
|
|
|
168
186
|
const entityId = body?.entityId
|
|
169
187
|
if (!entityId) return NextResponse.json({ error: 'entityId is required' }, { status: 400 })
|
|
170
188
|
|
|
171
|
-
const
|
|
189
|
+
const container = await createRequestContainer()
|
|
190
|
+
const { resolve } = container
|
|
172
191
|
const em = resolve('em') as any
|
|
173
192
|
|
|
174
193
|
const where: any = { entityId, organizationId: auth.orgId ?? null, tenantId: auth.tenantId ?? null }
|
|
175
194
|
const ent = await em.findOne(CustomEntity, where)
|
|
176
195
|
if (!ent) return NextResponse.json({ error: 'Not found' }, { status: 404 })
|
|
196
|
+
|
|
197
|
+
const guard = await beginEntitiesMutationGuard({
|
|
198
|
+
container,
|
|
199
|
+
auth,
|
|
200
|
+
req,
|
|
201
|
+
resourceKind: ENTITY_DEFINITION_RESOURCE_KIND,
|
|
202
|
+
resourceId: ent.id,
|
|
203
|
+
operation: 'delete',
|
|
204
|
+
mutationPayload: { entityId },
|
|
205
|
+
})
|
|
206
|
+
if (guard.blockedResponse) return guard.blockedResponse
|
|
207
|
+
|
|
177
208
|
ent.isActive = false
|
|
178
209
|
ent.updatedAt = new Date()
|
|
179
210
|
ent.deletedAt = ent.deletedAt ?? new Date()
|
|
180
211
|
em.persist(ent)
|
|
181
212
|
await em.flush()
|
|
213
|
+
await guard.runAfterSuccess()
|
|
182
214
|
// Invalidate sidebar/nav cache for tenant scope (also when tenantId is null)
|
|
183
215
|
try {
|
|
184
216
|
const cache = (await createRequestContainer()).resolve('cache') as any
|