@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.
@@ -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.findOneOrFail(Notification, {
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.findOneOrFail(Notification, {
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.findOneOrFail(Notification, {
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.findOneOrFail(Notification, {
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 input = schema.parse(body)
78
+ const parsed = schema.safeParse(body)
79
+ if (!parsed.success) {
80
+ return notificationValidationErrorResponse(parsed.error)
81
+ }
53
82
 
54
- const notifications = await service[serviceMethod](input as never, scope)
83
+ try {
84
+ const notifications = await service[serviceMethod](parsed.data as never, scope)
55
85
 
56
- return Response.json({
57
- ok: true,
58
- count: notifications.length,
59
- ids: notifications.map((n) => n.id),
60
- }, { status: 201 })
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
- await service[serviceMethod](id, scope)
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
  }