@open-mercato/core 0.6.5-develop.4620.1.c20bc7e4bb → 0.6.5-develop.4639.1.0416d895fa
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/customers/commands/deals.js +20 -4
- package/dist/modules/customers/commands/deals.js.map +2 -2
- package/dist/modules/entities/lib/helpers.js +4 -3
- package/dist/modules/entities/lib/helpers.js.map +2 -2
- package/dist/modules/notifications/api/[id]/action/route.js +12 -2
- package/dist/modules/notifications/api/[id]/action/route.js.map +2 -2
- package/dist/modules/notifications/api/route.js +17 -4
- package/dist/modules/notifications/api/route.js.map +2 -2
- package/dist/modules/notifications/lib/notificationService.js +26 -21
- package/dist/modules/notifications/lib/notificationService.js.map +2 -2
- package/dist/modules/notifications/lib/routeHelpers.js +46 -8
- package/dist/modules/notifications/lib/routeHelpers.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/customers/commands/deals.ts +24 -6
- package/src/modules/entities/lib/helpers.ts +4 -4
- package/src/modules/notifications/api/[id]/action/route.ts +13 -2
- package/src/modules/notifications/api/route.ts +17 -4
- package/src/modules/notifications/lib/notificationService.ts +31 -21
- package/src/modules/notifications/lib/routeHelpers.ts +49 -8
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
2
|
import { type Kysely, sql } from 'kysely'
|
|
3
|
+
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
3
4
|
import { Notification, type NotificationStatus } from '../data/entities'
|
|
4
5
|
import type { CreateNotificationInput, CreateBatchNotificationInput, CreateRoleNotificationInput, CreateFeatureNotificationInput, ExecuteActionInput } from '../data/validators'
|
|
5
6
|
import type { NotificationPollData } from '@open-mercato/shared/modules/notifications/types'
|
|
6
7
|
import { NOTIFICATION_EVENTS, NOTIFICATION_SSE_EVENTS } from './events'
|
|
7
|
-
import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
8
|
+
import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
8
9
|
import {
|
|
9
10
|
buildNotificationEntity,
|
|
10
11
|
emitNotificationCreated,
|
|
@@ -76,6 +77,31 @@ function applyNotificationContent(
|
|
|
76
77
|
notification.createdAt = new Date()
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
async function findScopedNotificationOrThrow(
|
|
81
|
+
em: EntityManager,
|
|
82
|
+
notificationId: string,
|
|
83
|
+
ctx: NotificationServiceContext,
|
|
84
|
+
): Promise<Notification> {
|
|
85
|
+
const notification = await findOneWithDecryption(
|
|
86
|
+
em,
|
|
87
|
+
Notification,
|
|
88
|
+
{
|
|
89
|
+
id: notificationId,
|
|
90
|
+
recipientUserId: ctx.userId,
|
|
91
|
+
tenantId: ctx.tenantId,
|
|
92
|
+
},
|
|
93
|
+
undefined,
|
|
94
|
+
{
|
|
95
|
+
tenantId: ctx.tenantId,
|
|
96
|
+
organizationId: ctx.organizationId ?? null,
|
|
97
|
+
},
|
|
98
|
+
)
|
|
99
|
+
if (!notification) {
|
|
100
|
+
throw new CrudHttpError(404, { error: 'Notification not found' })
|
|
101
|
+
}
|
|
102
|
+
return notification
|
|
103
|
+
}
|
|
104
|
+
|
|
79
105
|
async function emitNotificationSseEvents(
|
|
80
106
|
eventBus: { emit: (event: string, payload: unknown) => Promise<void> },
|
|
81
107
|
notifications: Notification[],
|
|
@@ -286,11 +312,7 @@ export function createNotificationService(deps: NotificationServiceDeps): Notifi
|
|
|
286
312
|
|
|
287
313
|
async markAsRead(notificationId, ctx) {
|
|
288
314
|
const em = rootEm.fork()
|
|
289
|
-
const notification = await em
|
|
290
|
-
id: notificationId,
|
|
291
|
-
recipientUserId: ctx.userId,
|
|
292
|
-
tenantId: ctx.tenantId,
|
|
293
|
-
})
|
|
315
|
+
const notification = await findScopedNotificationOrThrow(em, notificationId, ctx)
|
|
294
316
|
|
|
295
317
|
if (notification.status === 'unread') {
|
|
296
318
|
notification.status = 'read'
|
|
@@ -370,11 +392,7 @@ export function createNotificationService(deps: NotificationServiceDeps): Notifi
|
|
|
370
392
|
|
|
371
393
|
async dismiss(notificationId, ctx) {
|
|
372
394
|
const em = rootEm.fork()
|
|
373
|
-
const notification = await em
|
|
374
|
-
id: notificationId,
|
|
375
|
-
recipientUserId: ctx.userId,
|
|
376
|
-
tenantId: ctx.tenantId,
|
|
377
|
-
})
|
|
395
|
+
const notification = await findScopedNotificationOrThrow(em, notificationId, ctx)
|
|
378
396
|
|
|
379
397
|
notification.status = 'dismissed'
|
|
380
398
|
notification.dismissedAt = new Date()
|
|
@@ -391,11 +409,7 @@ export function createNotificationService(deps: NotificationServiceDeps): Notifi
|
|
|
391
409
|
|
|
392
410
|
async restoreDismissed(notificationId, status, ctx) {
|
|
393
411
|
const em = rootEm.fork()
|
|
394
|
-
const notification = await em
|
|
395
|
-
id: notificationId,
|
|
396
|
-
recipientUserId: ctx.userId,
|
|
397
|
-
tenantId: ctx.tenantId,
|
|
398
|
-
})
|
|
412
|
+
const notification = await findScopedNotificationOrThrow(em, notificationId, ctx)
|
|
399
413
|
|
|
400
414
|
if (notification.status !== 'dismissed') {
|
|
401
415
|
return notification
|
|
@@ -425,11 +439,7 @@ export function createNotificationService(deps: NotificationServiceDeps): Notifi
|
|
|
425
439
|
|
|
426
440
|
async executeAction(notificationId, input, ctx) {
|
|
427
441
|
const em = rootEm.fork()
|
|
428
|
-
const notification = await em
|
|
429
|
-
id: notificationId,
|
|
430
|
-
recipientUserId: ctx.userId,
|
|
431
|
-
tenantId: ctx.tenantId,
|
|
432
|
-
})
|
|
442
|
+
const notification = await findScopedNotificationOrThrow(em, notificationId, ctx)
|
|
433
443
|
|
|
434
444
|
const actionData = notification.actionData
|
|
435
445
|
const action = actionData?.actions?.find((a) => a.id === input.actionId)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { resolveRequestContext } from '@open-mercato/shared/lib/api/context'
|
|
3
|
+
import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
4
|
+
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
3
5
|
import { resolveNotificationService, type NotificationService } from './notificationService'
|
|
4
6
|
|
|
5
7
|
/**
|
|
@@ -20,6 +22,30 @@ export interface NotificationRequestContext {
|
|
|
20
22
|
ctx: Awaited<ReturnType<typeof resolveRequestContext>>['ctx']
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
function formatZodIssues(error: z.ZodError): string {
|
|
26
|
+
return error.issues
|
|
27
|
+
.map((issue) => {
|
|
28
|
+
const path = issue.path.length ? `${issue.path.join('.')}: ` : ''
|
|
29
|
+
return `${path}${issue.message}`
|
|
30
|
+
})
|
|
31
|
+
.join('; ')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function notificationValidationErrorResponse(error: z.ZodError): Promise<Response> {
|
|
35
|
+
const { t } = await resolveTranslations()
|
|
36
|
+
const prefix = t('api.errors.invalidPayload', 'Invalid request body')
|
|
37
|
+
const details = formatZodIssues(error)
|
|
38
|
+
return Response.json(
|
|
39
|
+
{ error: details ? `${prefix}: ${details}` : prefix },
|
|
40
|
+
{ status: 400 },
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function notificationCrudErrorResponse(error: unknown): Response | null {
|
|
45
|
+
if (!isCrudHttpError(error)) return null
|
|
46
|
+
return Response.json(error.body ?? { error: 'Notification request failed' }, { status: error.status })
|
|
47
|
+
}
|
|
48
|
+
|
|
23
49
|
/**
|
|
24
50
|
* Resolve notification service and scope from a request.
|
|
25
51
|
* Centralizes the common pattern used across all notification API routes.
|
|
@@ -49,15 +75,24 @@ export function createBulkNotificationRoute<TSchema extends z.ZodTypeAny>(
|
|
|
49
75
|
const { service, scope } = await resolveNotificationContext(req)
|
|
50
76
|
|
|
51
77
|
const body = await req.json().catch(() => ({}))
|
|
52
|
-
const
|
|
78
|
+
const parsed = schema.safeParse(body)
|
|
79
|
+
if (!parsed.success) {
|
|
80
|
+
return notificationValidationErrorResponse(parsed.error)
|
|
81
|
+
}
|
|
53
82
|
|
|
54
|
-
|
|
83
|
+
try {
|
|
84
|
+
const notifications = await service[serviceMethod](parsed.data as never, scope)
|
|
55
85
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
86
|
+
return Response.json({
|
|
87
|
+
ok: true,
|
|
88
|
+
count: notifications.length,
|
|
89
|
+
ids: notifications.map((n) => n.id),
|
|
90
|
+
}, { status: 201 })
|
|
91
|
+
} catch (error) {
|
|
92
|
+
const errorResponse = notificationCrudErrorResponse(error)
|
|
93
|
+
if (errorResponse) return errorResponse
|
|
94
|
+
throw error
|
|
95
|
+
}
|
|
61
96
|
}
|
|
62
97
|
}
|
|
63
98
|
|
|
@@ -111,7 +146,13 @@ export function createSingleNotificationActionRoute(
|
|
|
111
146
|
const { id } = await params
|
|
112
147
|
const { service, scope } = await resolveNotificationContext(req)
|
|
113
148
|
|
|
114
|
-
|
|
149
|
+
try {
|
|
150
|
+
await service[serviceMethod](id, scope)
|
|
151
|
+
} catch (error) {
|
|
152
|
+
const errorResponse = notificationCrudErrorResponse(error)
|
|
153
|
+
if (errorResponse) return errorResponse
|
|
154
|
+
throw error
|
|
155
|
+
}
|
|
115
156
|
|
|
116
157
|
return Response.json({ ok: true })
|
|
117
158
|
}
|