@open-mercato/core 0.6.6-develop.6300.1.08ff40c897 → 0.6.6-develop.6304.1.4cf2b975cb
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/dist/modules/customers/api/people/[id]/route.js.map +2 -2
- package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js +32 -10
- package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js.map +2 -2
- package/dist/modules/data_sync/components/IntegrationScheduleTab.js +77 -32
- package/dist/modules/data_sync/components/IntegrationScheduleTab.js.map +2 -2
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.js +6 -0
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.js.map +2 -2
- package/dist/modules/dictionaries/api/[dictionaryId]/route.js +6 -0
- package/dist/modules/dictionaries/api/[dictionaryId]/route.js.map +2 -2
- package/dist/modules/messages/api/route.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/customers/api/people/[id]/route.ts +7 -0
- package/src/modules/data_sync/backend/data-sync/runs/[id]/page.tsx +32 -10
- package/src/modules/data_sync/components/IntegrationScheduleTab.tsx +77 -33
- package/src/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.ts +7 -0
- package/src/modules/dictionaries/api/[dictionaryId]/route.ts +7 -0
- package/src/modules/messages/api/route.ts +8 -0
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
|
|
5
5
|
import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
6
|
+
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
6
7
|
import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
|
|
7
8
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
8
9
|
import { Badge } from '@open-mercato/ui/primitives/badge'
|
|
@@ -146,6 +147,9 @@ function buildScheduleEditors(
|
|
|
146
147
|
export function IntegrationScheduleTab(props: IntegrationScheduleTabProps) {
|
|
147
148
|
const t = useT()
|
|
148
149
|
const scopeVersion = useOrganizationScopeVersion()
|
|
150
|
+
const { runMutation } = useGuardedMutation<Record<string, unknown>>({
|
|
151
|
+
contextId: 'data_sync.integrationTab',
|
|
152
|
+
})
|
|
149
153
|
const [option, setOption] = React.useState<SyncOption | null>(null)
|
|
150
154
|
const [schedules, setSchedules] = React.useState<Record<string, SyncScheduleEditorState>>({})
|
|
151
155
|
const [isLoading, setIsLoading] = React.useState(true)
|
|
@@ -222,18 +226,32 @@ export function IntegrationScheduleTab(props: IntegrationScheduleTabProps) {
|
|
|
222
226
|
setRunningKey(scheduleKey)
|
|
223
227
|
try {
|
|
224
228
|
const scheduleState = schedules[scheduleKey] ?? buildDefaultScheduleState(entityType)
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
229
|
+
const call = await runMutation({
|
|
230
|
+
// optimistic-lock-exempt: starts a new sync run (create), not a concurrent record edit
|
|
231
|
+
operation: () => apiCall<{ id: string }>('/api/data_sync/run', {
|
|
232
|
+
method: 'POST',
|
|
233
|
+
headers: { 'content-type': 'application/json' },
|
|
234
|
+
body: JSON.stringify({
|
|
235
|
+
integrationId: props.integrationId,
|
|
236
|
+
entityType,
|
|
237
|
+
direction,
|
|
238
|
+
fullSync: scheduleState.fullSync,
|
|
239
|
+
batchSize: 100,
|
|
240
|
+
}),
|
|
241
|
+
}, { fallback: null }),
|
|
242
|
+
mutationPayload: {
|
|
230
243
|
integrationId: props.integrationId,
|
|
231
244
|
entityType,
|
|
232
245
|
direction,
|
|
233
246
|
fullSync: scheduleState.fullSync,
|
|
234
247
|
batchSize: 100,
|
|
235
|
-
}
|
|
236
|
-
|
|
248
|
+
},
|
|
249
|
+
context: {
|
|
250
|
+
operation: 'create',
|
|
251
|
+
actionId: 'start-sync-run',
|
|
252
|
+
integrationId: props.integrationId,
|
|
253
|
+
},
|
|
254
|
+
})
|
|
237
255
|
|
|
238
256
|
if (!call.ok) {
|
|
239
257
|
throw new Error((call.result as { error?: string } | null)?.error ?? 'Failed to start sync')
|
|
@@ -246,7 +264,7 @@ export function IntegrationScheduleTab(props: IntegrationScheduleTabProps) {
|
|
|
246
264
|
} finally {
|
|
247
265
|
setRunningKey(null)
|
|
248
266
|
}
|
|
249
|
-
}, [option?.canStartRun, props.hasCredentials, props.integrationId, props.isEnabled, schedules, t])
|
|
267
|
+
}, [option?.canStartRun, props.hasCredentials, props.integrationId, props.isEnabled, runMutation, schedules, t])
|
|
250
268
|
|
|
251
269
|
const handleSaveSchedule = React.useCallback(async (entityType: string, direction: 'import' | 'export', scheduleKey: string) => {
|
|
252
270
|
const scheduleState = schedules[scheduleKey] ?? buildDefaultScheduleState(entityType)
|
|
@@ -255,23 +273,40 @@ export function IntegrationScheduleTab(props: IntegrationScheduleTabProps) {
|
|
|
255
273
|
// Keyed upsert (POST). When the server resolves an existing row the save
|
|
256
274
|
// version-checks against this schedule's loaded `updatedAt`; for a brand
|
|
257
275
|
// new row the header is empty so the create path is unaffected.
|
|
258
|
-
const call = await
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
276
|
+
const call = await runMutation({
|
|
277
|
+
operation: () => withScopedApiRequestHeaders(
|
|
278
|
+
buildOptimisticLockHeader(scheduleState.updatedAt),
|
|
279
|
+
() => apiCall<SyncScheduleRecord>('/api/data_sync/schedules', {
|
|
280
|
+
method: 'POST',
|
|
281
|
+
headers: { 'content-type': 'application/json' },
|
|
282
|
+
body: JSON.stringify({
|
|
283
|
+
integrationId: props.integrationId,
|
|
284
|
+
entityType,
|
|
285
|
+
direction,
|
|
286
|
+
scheduleType: scheduleState.scheduleType,
|
|
287
|
+
scheduleValue: scheduleState.scheduleValue,
|
|
288
|
+
timezone: scheduleState.timezone,
|
|
289
|
+
fullSync: scheduleState.fullSync,
|
|
290
|
+
isEnabled: scheduleState.isEnabled,
|
|
291
|
+
}),
|
|
292
|
+
}, { fallback: null }),
|
|
293
|
+
),
|
|
294
|
+
mutationPayload: {
|
|
295
|
+
integrationId: props.integrationId,
|
|
296
|
+
entityType,
|
|
297
|
+
direction,
|
|
298
|
+
scheduleType: scheduleState.scheduleType,
|
|
299
|
+
scheduleValue: scheduleState.scheduleValue,
|
|
300
|
+
timezone: scheduleState.timezone,
|
|
301
|
+
fullSync: scheduleState.fullSync,
|
|
302
|
+
isEnabled: scheduleState.isEnabled,
|
|
303
|
+
},
|
|
304
|
+
context: {
|
|
305
|
+
operation: 'update',
|
|
306
|
+
actionId: 'save-sync-schedule',
|
|
307
|
+
integrationId: props.integrationId,
|
|
308
|
+
},
|
|
309
|
+
})
|
|
275
310
|
|
|
276
311
|
if (!call.ok || !call.result) {
|
|
277
312
|
const conflictError = Object.assign(
|
|
@@ -304,7 +339,7 @@ export function IntegrationScheduleTab(props: IntegrationScheduleTabProps) {
|
|
|
304
339
|
} finally {
|
|
305
340
|
setSavingKey(null)
|
|
306
341
|
}
|
|
307
|
-
}, [props.integrationId, schedules, t, updateScheduleEditor])
|
|
342
|
+
}, [props.integrationId, runMutation, schedules, t, updateScheduleEditor])
|
|
308
343
|
|
|
309
344
|
const handleDeleteSchedule = React.useCallback(async (entityType: string, scheduleKey: string) => {
|
|
310
345
|
const scheduleState = schedules[scheduleKey]
|
|
@@ -315,12 +350,21 @@ export function IntegrationScheduleTab(props: IntegrationScheduleTabProps) {
|
|
|
315
350
|
|
|
316
351
|
setDeletingKey(scheduleKey)
|
|
317
352
|
try {
|
|
318
|
-
const call = await
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
353
|
+
const call = await runMutation({
|
|
354
|
+
operation: () => withScopedApiRequestHeaders(
|
|
355
|
+
buildOptimisticLockHeader(scheduleState.updatedAt),
|
|
356
|
+
() => apiCall(`/api/data_sync/schedules/${encodeURIComponent(scheduleState.id as string)}`, {
|
|
357
|
+
method: 'DELETE',
|
|
358
|
+
}, { fallback: null }),
|
|
359
|
+
),
|
|
360
|
+
mutationPayload: {
|
|
361
|
+
scheduleId: scheduleState.id,
|
|
362
|
+
},
|
|
363
|
+
context: {
|
|
364
|
+
operation: 'delete',
|
|
365
|
+
actionId: 'delete-sync-schedule',
|
|
366
|
+
},
|
|
367
|
+
})
|
|
324
368
|
|
|
325
369
|
if (!call.ok) {
|
|
326
370
|
throw new Error((call.result as { error?: string } | null)?.error ?? 'Failed to delete schedule')
|
|
@@ -337,7 +381,7 @@ export function IntegrationScheduleTab(props: IntegrationScheduleTabProps) {
|
|
|
337
381
|
} finally {
|
|
338
382
|
setDeletingKey(null)
|
|
339
383
|
}
|
|
340
|
-
}, [schedules, t, updateScheduleEditor])
|
|
384
|
+
}, [runMutation, schedules, t, updateScheduleEditor])
|
|
341
385
|
|
|
342
386
|
if (isLoading) {
|
|
343
387
|
return (
|
|
@@ -178,6 +178,13 @@ export async function DELETE(req: Request, ctx: { params?: { dictionaryId?: stri
|
|
|
178
178
|
})
|
|
179
179
|
const dictionary = await loadDictionary(context, dictionaryId)
|
|
180
180
|
const entry = await loadEntry(context, dictionary, entryId)
|
|
181
|
+
enforceCommandOptimisticLock({
|
|
182
|
+
resourceKind: 'dictionaries.entry',
|
|
183
|
+
resourceId: entry.id,
|
|
184
|
+
current: entry.updatedAt ?? null,
|
|
185
|
+
request: req,
|
|
186
|
+
})
|
|
187
|
+
|
|
181
188
|
const guardUserId = resolveDictionaryActorId(context.auth)
|
|
182
189
|
const guardResult = await validateCrudMutationGuard(context.container, {
|
|
183
190
|
tenantId: context.tenantId,
|
|
@@ -236,6 +236,13 @@ export async function DELETE(req: Request, ctx: { params?: { dictionaryId?: stri
|
|
|
236
236
|
const { dictionaryId } = paramsSchema.parse({ dictionaryId: ctx.params?.dictionaryId })
|
|
237
237
|
const dictionary = await loadDictionary(context, dictionaryId)
|
|
238
238
|
|
|
239
|
+
enforceCommandOptimisticLock({
|
|
240
|
+
resourceKind: 'dictionaries.dictionary',
|
|
241
|
+
resourceId: dictionary.id,
|
|
242
|
+
current: dictionary.updatedAt ?? null,
|
|
243
|
+
request: req,
|
|
244
|
+
})
|
|
245
|
+
|
|
239
246
|
const guardUserId = resolveDictionaryActorId(context.auth)
|
|
240
247
|
const guardResult = await validateCrudMutationGuard(context.container, {
|
|
241
248
|
tenantId: context.tenantId,
|
|
@@ -189,6 +189,14 @@ export async function GET(req: Request) {
|
|
|
189
189
|
return q
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
// Audited for #3386 rollout (P3): sort is on m.sent_at (a plain timestamp —
|
|
193
|
+
// not in the messages:message encryption map whose encrypted fields are:
|
|
194
|
+
// subject, body, external_email, external_name, action_data, action_result).
|
|
195
|
+
// The handler already uses the correct two-phase shape: Kysely SQL
|
|
196
|
+
// ORDER BY + LIMIT/OFFSET produces a bounded page of IDs, then
|
|
197
|
+
// findWithDecryption is called only for those IDs — never for the full
|
|
198
|
+
// result set. The #3278 unbounded-decrypt hazard does not apply here.
|
|
199
|
+
// Covered by __tests__/list.test.ts.
|
|
192
200
|
const countResult = await buildBaseQuery()
|
|
193
201
|
.select(sql<number>`count(*)`.as('count'))
|
|
194
202
|
.executeTakeFirst() as { count: string | number } | undefined
|