@open-mercato/core 0.4.2-canary-f6b7824b47 → 0.4.2-canary-70c8402224
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/generated/entities/notification/index.js +57 -0
- package/dist/generated/entities/notification/index.js.map +7 -0
- package/dist/generated/entities.ids.generated.js +5 -1
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +2 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/api_docs/frontend/docs/api/page.js +3 -2
- package/dist/modules/api_docs/frontend/docs/api/page.js.map +2 -2
- package/dist/modules/auth/api/admin/nav.js +4 -3
- package/dist/modules/auth/api/admin/nav.js.map +2 -2
- package/dist/modules/auth/api/profile/route.js +155 -0
- package/dist/modules/auth/api/profile/route.js.map +7 -0
- package/dist/modules/auth/api/reset/confirm.js +25 -2
- package/dist/modules/auth/api/reset/confirm.js.map +2 -2
- package/dist/modules/auth/api/reset.js +23 -0
- package/dist/modules/auth/api/reset.js.map +2 -2
- package/dist/modules/auth/api/sidebar/preferences/route.js +14 -9
- package/dist/modules/auth/api/sidebar/preferences/route.js.map +2 -2
- package/dist/modules/auth/backend/auth/profile/page.js +99 -0
- package/dist/modules/auth/backend/auth/profile/page.js.map +7 -0
- package/dist/modules/auth/backend/auth/profile/page.meta.js +12 -0
- package/dist/modules/auth/backend/auth/profile/page.meta.js.map +7 -0
- package/dist/modules/auth/commands/users.js +55 -0
- package/dist/modules/auth/commands/users.js.map +2 -2
- package/dist/modules/auth/lib/setup-app.js +1 -0
- package/dist/modules/auth/lib/setup-app.js.map +2 -2
- package/dist/modules/auth/notifications.js +112 -0
- package/dist/modules/auth/notifications.js.map +7 -0
- package/dist/modules/auth/services/authService.js +3 -3
- package/dist/modules/auth/services/authService.js.map +2 -2
- package/dist/modules/business_rules/notifications.js +28 -0
- package/dist/modules/business_rules/notifications.js.map +7 -0
- package/dist/modules/business_rules/subscribers/rule-execution-failed-notification.js +37 -0
- package/dist/modules/business_rules/subscribers/rule-execution-failed-notification.js.map +7 -0
- package/dist/modules/catalog/notifications.js +28 -0
- package/dist/modules/catalog/notifications.js.map +7 -0
- package/dist/modules/catalog/subscribers/low-stock-notification.js +38 -0
- package/dist/modules/catalog/subscribers/low-stock-notification.js.map +7 -0
- package/dist/modules/configs/cli.js +6 -0
- package/dist/modules/configs/cli.js.map +2 -2
- package/dist/modules/customers/commands/deals.js +31 -0
- package/dist/modules/customers/commands/deals.js.map +2 -2
- package/dist/modules/customers/notifications.js +48 -0
- package/dist/modules/customers/notifications.js.map +7 -0
- package/dist/modules/notifications/acl.js +11 -0
- package/dist/modules/notifications/acl.js.map +7 -0
- package/dist/modules/notifications/api/[id]/action/route.js +69 -0
- package/dist/modules/notifications/api/[id]/action/route.js.map +7 -0
- package/dist/modules/notifications/api/[id]/dismiss/route.js +15 -0
- package/dist/modules/notifications/api/[id]/dismiss/route.js.map +7 -0
- package/dist/modules/notifications/api/[id]/read/route.js +15 -0
- package/dist/modules/notifications/api/[id]/read/route.js.map +7 -0
- package/dist/modules/notifications/api/[id]/restore/route.js +53 -0
- package/dist/modules/notifications/api/[id]/restore/route.js.map +7 -0
- package/dist/modules/notifications/api/batch/route.js +17 -0
- package/dist/modules/notifications/api/batch/route.js.map +7 -0
- package/dist/modules/notifications/api/feature/route.js +17 -0
- package/dist/modules/notifications/api/feature/route.js.map +7 -0
- package/dist/modules/notifications/api/mark-all-read/route.js +35 -0
- package/dist/modules/notifications/api/mark-all-read/route.js.map +7 -0
- package/dist/modules/notifications/api/openapi.js +57 -0
- package/dist/modules/notifications/api/openapi.js.map +7 -0
- package/dist/modules/notifications/api/role/route.js +17 -0
- package/dist/modules/notifications/api/role/route.js.map +7 -0
- package/dist/modules/notifications/api/route.js +85 -0
- package/dist/modules/notifications/api/route.js.map +7 -0
- package/dist/modules/notifications/api/settings/route.js +96 -0
- package/dist/modules/notifications/api/settings/route.js.map +7 -0
- package/dist/modules/notifications/api/unread-count/route.js +38 -0
- package/dist/modules/notifications/api/unread-count/route.js.map +7 -0
- package/dist/modules/notifications/backend/config/notifications/page.js +10 -0
- package/dist/modules/notifications/backend/config/notifications/page.js.map +7 -0
- package/dist/modules/notifications/backend/config/notifications/page.meta.js +24 -0
- package/dist/modules/notifications/backend/config/notifications/page.meta.js.map +7 -0
- package/dist/modules/notifications/cli.js +16 -0
- package/dist/modules/notifications/cli.js.map +7 -0
- package/dist/modules/notifications/data/entities.js +112 -0
- package/dist/modules/notifications/data/entities.js.map +7 -0
- package/dist/modules/notifications/data/validators.js +94 -0
- package/dist/modules/notifications/data/validators.js.map +7 -0
- package/dist/modules/notifications/di.js +13 -0
- package/dist/modules/notifications/di.js.map +7 -0
- package/dist/modules/notifications/emails/NotificationEmail.js +58 -0
- package/dist/modules/notifications/emails/NotificationEmail.js.map +7 -0
- package/dist/modules/notifications/frontend/NotificationInboxPageClient.js +44 -0
- package/dist/modules/notifications/frontend/NotificationInboxPageClient.js.map +7 -0
- package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js +219 -0
- package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js.map +7 -0
- package/dist/modules/notifications/index.js +14 -0
- package/dist/modules/notifications/index.js.map +7 -0
- package/dist/modules/notifications/lib/deliveryConfig.js +105 -0
- package/dist/modules/notifications/lib/deliveryConfig.js.map +7 -0
- package/dist/modules/notifications/lib/events.js +12 -0
- package/dist/modules/notifications/lib/events.js.map +7 -0
- package/dist/modules/notifications/lib/notificationBuilder.js +66 -0
- package/dist/modules/notifications/lib/notificationBuilder.js.map +7 -0
- package/dist/modules/notifications/lib/notificationFactory.js +54 -0
- package/dist/modules/notifications/lib/notificationFactory.js.map +7 -0
- package/dist/modules/notifications/lib/notificationMapper.js +34 -0
- package/dist/modules/notifications/lib/notificationMapper.js.map +7 -0
- package/dist/modules/notifications/lib/notificationRecipients.js +35 -0
- package/dist/modules/notifications/lib/notificationRecipients.js.map +7 -0
- package/dist/modules/notifications/lib/notificationService.js +279 -0
- package/dist/modules/notifications/lib/notificationService.js.map +7 -0
- package/dist/modules/notifications/lib/routeHelpers.js +101 -0
- package/dist/modules/notifications/lib/routeHelpers.js.map +7 -0
- package/dist/modules/notifications/lib/safeHref.js +24 -0
- package/dist/modules/notifications/lib/safeHref.js.map +7 -0
- package/dist/modules/notifications/migrations/Migration20260123000001.js +70 -0
- package/dist/modules/notifications/migrations/Migration20260123000001.js.map +7 -0
- package/dist/modules/notifications/migrations/Migration20260126150000.js +37 -0
- package/dist/modules/notifications/migrations/Migration20260126150000.js.map +7 -0
- package/dist/modules/notifications/subscribers/deliver-notification.js +139 -0
- package/dist/modules/notifications/subscribers/deliver-notification.js.map +7 -0
- package/dist/modules/notifications/workers/create-notification.worker.js +70 -0
- package/dist/modules/notifications/workers/create-notification.worker.js.map +7 -0
- package/dist/modules/sales/commands/documents.js +53 -0
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/commands/payments.js +26 -0
- package/dist/modules/sales/commands/payments.js.map +2 -2
- package/dist/modules/sales/notifications.client.js +51 -0
- package/dist/modules/sales/notifications.client.js.map +7 -0
- package/dist/modules/sales/notifications.js +88 -0
- package/dist/modules/sales/notifications.js.map +7 -0
- package/dist/modules/sales/subscribers/quote-expiring-notification.js +38 -0
- package/dist/modules/sales/subscribers/quote-expiring-notification.js.map +7 -0
- package/dist/modules/sales/widgets/notifications/SalesOrderCreatedRenderer.js +137 -0
- package/dist/modules/sales/widgets/notifications/SalesOrderCreatedRenderer.js.map +7 -0
- package/dist/modules/sales/widgets/notifications/SalesQuoteCreatedRenderer.js +137 -0
- package/dist/modules/sales/widgets/notifications/SalesQuoteCreatedRenderer.js.map +7 -0
- package/dist/modules/sales/widgets/notifications/index.js +7 -0
- package/dist/modules/sales/widgets/notifications/index.js.map +7 -0
- package/dist/modules/sales/widgets/notifications/useSalesDocumentTotals.js +60 -0
- package/dist/modules/sales/widgets/notifications/useSalesDocumentTotals.js.map +7 -0
- package/dist/modules/staff/commands/leave-requests.js +79 -0
- package/dist/modules/staff/commands/leave-requests.js.map +2 -2
- package/dist/modules/staff/notifications.js +75 -0
- package/dist/modules/staff/notifications.js.map +7 -0
- package/dist/modules/workflows/notifications.js +28 -0
- package/dist/modules/workflows/notifications.js.map +7 -0
- package/dist/modules/workflows/subscribers/task-assigned-notification.js +38 -0
- package/dist/modules/workflows/subscribers/task-assigned-notification.js.map +7 -0
- package/generated/entities/notification/index.ts +27 -0
- package/generated/entities.ids.generated.ts +5 -1
- package/generated/entity-fields-registry.ts +2 -0
- package/package.json +2 -2
- package/src/modules/api_docs/frontend/docs/api/page.tsx +3 -2
- package/src/modules/auth/api/admin/nav.ts +10 -6
- package/src/modules/auth/api/profile/route.ts +160 -0
- package/src/modules/auth/api/reset/confirm.ts +25 -2
- package/src/modules/auth/api/reset.ts +23 -0
- package/src/modules/auth/api/sidebar/preferences/route.ts +21 -12
- package/src/modules/auth/backend/auth/profile/page.meta.ts +8 -0
- package/src/modules/auth/backend/auth/profile/page.tsx +127 -0
- package/src/modules/auth/commands/users.ts +68 -0
- package/src/modules/auth/i18n/de.json +29 -1
- package/src/modules/auth/i18n/en.json +29 -1
- package/src/modules/auth/i18n/es.json +29 -1
- package/src/modules/auth/i18n/pl.json +29 -1
- package/src/modules/auth/lib/setup-app.ts +1 -0
- package/src/modules/auth/notifications.ts +109 -0
- package/src/modules/auth/services/authService.ts +4 -4
- package/src/modules/business_rules/i18n/en.json +3 -1
- package/src/modules/business_rules/notifications.ts +25 -0
- package/src/modules/business_rules/subscribers/rule-execution-failed-notification.ts +50 -0
- package/src/modules/catalog/i18n/en.json +3 -1
- package/src/modules/catalog/notifications.ts +25 -0
- package/src/modules/catalog/subscribers/low-stock-notification.ts +52 -0
- package/src/modules/configs/cli.ts +6 -0
- package/src/modules/customers/commands/deals.ts +39 -0
- package/src/modules/customers/i18n/en.json +5 -1
- package/src/modules/customers/notifications.ts +44 -0
- package/src/modules/notifications/acl.ts +7 -0
- package/src/modules/notifications/api/[id]/action/route.ts +70 -0
- package/src/modules/notifications/api/[id]/dismiss/route.ts +12 -0
- package/src/modules/notifications/api/[id]/read/route.ts +12 -0
- package/src/modules/notifications/api/[id]/restore/route.ts +53 -0
- package/src/modules/notifications/api/batch/route.ts +14 -0
- package/src/modules/notifications/api/feature/route.ts +14 -0
- package/src/modules/notifications/api/mark-all-read/route.ts +34 -0
- package/src/modules/notifications/api/openapi.ts +52 -0
- package/src/modules/notifications/api/role/route.ts +14 -0
- package/src/modules/notifications/api/route.ts +92 -0
- package/src/modules/notifications/api/settings/route.ts +98 -0
- package/src/modules/notifications/api/unread-count/route.ts +38 -0
- package/src/modules/notifications/backend/config/notifications/page.meta.ts +22 -0
- package/src/modules/notifications/backend/config/notifications/page.tsx +12 -0
- package/src/modules/notifications/cli.ts +18 -0
- package/src/modules/notifications/data/entities.ts +99 -0
- package/src/modules/notifications/data/validators.ts +110 -0
- package/src/modules/notifications/di.ts +11 -0
- package/src/modules/notifications/emails/NotificationEmail.tsx +98 -0
- package/src/modules/notifications/frontend/NotificationInboxPageClient.tsx +42 -0
- package/src/modules/notifications/frontend/NotificationSettingsPageClient.tsx +231 -0
- package/src/modules/notifications/i18n/de.json +50 -0
- package/src/modules/notifications/i18n/en.json +50 -0
- package/src/modules/notifications/i18n/es.json +50 -0
- package/src/modules/notifications/i18n/pl.json +50 -0
- package/src/modules/notifications/index.ts +12 -0
- package/src/modules/notifications/lib/deliveryConfig.ts +145 -0
- package/src/modules/notifications/lib/events.ts +48 -0
- package/src/modules/notifications/lib/notificationBuilder.ts +121 -0
- package/src/modules/notifications/lib/notificationFactory.ts +76 -0
- package/src/modules/notifications/lib/notificationMapper.ts +33 -0
- package/src/modules/notifications/lib/notificationRecipients.ts +83 -0
- package/src/modules/notifications/lib/notificationService.ts +414 -0
- package/src/modules/notifications/lib/routeHelpers.ts +151 -0
- package/src/modules/notifications/lib/safeHref.ts +29 -0
- package/src/modules/notifications/migrations/.snapshot-open-mercato.json +300 -0
- package/src/modules/notifications/migrations/Migration20260123000001.ts +73 -0
- package/src/modules/notifications/migrations/Migration20260126150000.ts +39 -0
- package/src/modules/notifications/subscribers/deliver-notification.ts +175 -0
- package/src/modules/notifications/workers/create-notification.worker.ts +122 -0
- package/src/modules/sales/commands/documents.ts +65 -0
- package/src/modules/sales/commands/payments.ts +33 -0
- package/src/modules/sales/i18n/de.json +20 -0
- package/src/modules/sales/i18n/en.json +25 -1
- package/src/modules/sales/i18n/es.json +20 -0
- package/src/modules/sales/i18n/pl.json +20 -0
- package/src/modules/sales/notifications.client.ts +65 -0
- package/src/modules/sales/notifications.ts +82 -0
- package/src/modules/sales/subscribers/quote-expiring-notification.ts +53 -0
- package/src/modules/sales/widgets/notifications/SalesOrderCreatedRenderer.tsx +156 -0
- package/src/modules/sales/widgets/notifications/SalesQuoteCreatedRenderer.tsx +156 -0
- package/src/modules/sales/widgets/notifications/index.ts +2 -0
- package/src/modules/sales/widgets/notifications/useSalesDocumentTotals.ts +81 -0
- package/src/modules/staff/commands/leave-requests.ts +94 -0
- package/src/modules/staff/i18n/de.json +4 -0
- package/src/modules/staff/i18n/en.json +9 -1
- package/src/modules/staff/i18n/es.json +4 -0
- package/src/modules/staff/i18n/pl.json +4 -0
- package/src/modules/staff/notifications.ts +71 -0
- package/src/modules/workflows/i18n/en.json +3 -1
- package/src/modules/workflows/notifications.ts +25 -0
- package/src/modules/workflows/subscribers/task-assigned-notification.ts +53 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { restoreNotificationSchema } from '../../../data/validators'
|
|
3
|
+
import { resolveNotificationContext } from '../../../lib/routeHelpers'
|
|
4
|
+
|
|
5
|
+
export const metadata = {
|
|
6
|
+
PUT: { requireAuth: true },
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
10
|
+
const { id } = await params
|
|
11
|
+
const { service, scope } = await resolveNotificationContext(req)
|
|
12
|
+
|
|
13
|
+
const body = await req.json().catch(() => ({}))
|
|
14
|
+
const input = restoreNotificationSchema.parse(body)
|
|
15
|
+
|
|
16
|
+
await service.restoreDismissed(id, input.status, scope)
|
|
17
|
+
|
|
18
|
+
return Response.json({ ok: true })
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const openApi = {
|
|
22
|
+
PUT: {
|
|
23
|
+
summary: 'Restore dismissed notification',
|
|
24
|
+
description: 'Undo a dismissal and restore a notification to read or unread.',
|
|
25
|
+
tags: ['Notifications'],
|
|
26
|
+
parameters: [
|
|
27
|
+
{
|
|
28
|
+
name: 'id',
|
|
29
|
+
in: 'path',
|
|
30
|
+
required: true,
|
|
31
|
+
schema: { type: 'string', format: 'uuid' },
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
requestBody: {
|
|
35
|
+
required: false,
|
|
36
|
+
content: {
|
|
37
|
+
'application/json': {
|
|
38
|
+
schema: restoreNotificationSchema,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
responses: {
|
|
43
|
+
200: {
|
|
44
|
+
description: 'Notification restored',
|
|
45
|
+
content: {
|
|
46
|
+
'application/json': {
|
|
47
|
+
schema: z.object({ ok: z.boolean() }),
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createBatchNotificationSchema } from '../../data/validators'
|
|
2
|
+
import { createBulkNotificationRoute, createBulkNotificationOpenApi } from '../../lib/routeHelpers'
|
|
3
|
+
|
|
4
|
+
export const metadata = {
|
|
5
|
+
POST: { requireAuth: true, requireFeatures: ['notifications.create'] },
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const POST = createBulkNotificationRoute(createBatchNotificationSchema, 'createBatch')
|
|
9
|
+
|
|
10
|
+
export const openApi = createBulkNotificationOpenApi(
|
|
11
|
+
createBatchNotificationSchema,
|
|
12
|
+
'Create batch notifications',
|
|
13
|
+
'Send the same notification to multiple users'
|
|
14
|
+
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createFeatureNotificationSchema } from '../../data/validators'
|
|
2
|
+
import { createBulkNotificationRoute, createBulkNotificationOpenApi } from '../../lib/routeHelpers'
|
|
3
|
+
|
|
4
|
+
export const metadata = {
|
|
5
|
+
POST: { requireAuth: true, requireFeatures: ['notifications.create'] },
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const POST = createBulkNotificationRoute(createFeatureNotificationSchema, 'createForFeature')
|
|
9
|
+
|
|
10
|
+
export const openApi = createBulkNotificationOpenApi(
|
|
11
|
+
createFeatureNotificationSchema,
|
|
12
|
+
'Create notifications for all users with a specific feature/permission',
|
|
13
|
+
'Send the same notification to all users who have the specified feature permission (via role ACL or user ACL). Supports wildcard matching.'
|
|
14
|
+
)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { resolveNotificationContext } from '../../lib/routeHelpers'
|
|
3
|
+
|
|
4
|
+
export const metadata = {
|
|
5
|
+
PUT: { requireAuth: true },
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function PUT(req: Request) {
|
|
9
|
+
const { service, scope } = await resolveNotificationContext(req)
|
|
10
|
+
|
|
11
|
+
const count = await service.markAllAsRead(scope)
|
|
12
|
+
|
|
13
|
+
return Response.json({ ok: true, count })
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const openApi = {
|
|
17
|
+
PUT: {
|
|
18
|
+
summary: 'Mark all notifications as read',
|
|
19
|
+
tags: ['Notifications'],
|
|
20
|
+
responses: {
|
|
21
|
+
200: {
|
|
22
|
+
description: 'All notifications marked as read',
|
|
23
|
+
content: {
|
|
24
|
+
'application/json': {
|
|
25
|
+
schema: z.object({
|
|
26
|
+
ok: z.boolean(),
|
|
27
|
+
count: z.number(),
|
|
28
|
+
}),
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { createCrudOpenApiFactory, createPagedListResponseSchema } from '@open-mercato/shared/lib/openapi/crud'
|
|
3
|
+
import { listNotificationsSchema, createNotificationSchema, executeActionSchema } from '../data/validators'
|
|
4
|
+
|
|
5
|
+
export const buildNotificationsCrudOpenApi = createCrudOpenApiFactory({
|
|
6
|
+
defaultTag: 'Notifications',
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
export const notificationItemSchema = z.object({
|
|
10
|
+
id: z.string().uuid(),
|
|
11
|
+
type: z.string(),
|
|
12
|
+
title: z.string(),
|
|
13
|
+
body: z.string().nullable().optional(),
|
|
14
|
+
titleKey: z.string().nullable().optional(),
|
|
15
|
+
bodyKey: z.string().nullable().optional(),
|
|
16
|
+
titleVariables: z.record(z.string(), z.string()).nullable().optional(),
|
|
17
|
+
bodyVariables: z.record(z.string(), z.string()).nullable().optional(),
|
|
18
|
+
icon: z.string().nullable().optional(),
|
|
19
|
+
severity: z.string(),
|
|
20
|
+
status: z.string(),
|
|
21
|
+
actions: z.array(z.object({
|
|
22
|
+
id: z.string(),
|
|
23
|
+
label: z.string(),
|
|
24
|
+
labelKey: z.string().optional(),
|
|
25
|
+
variant: z.string().optional(),
|
|
26
|
+
icon: z.string().optional(),
|
|
27
|
+
})),
|
|
28
|
+
primaryActionId: z.string().optional(),
|
|
29
|
+
sourceModule: z.string().nullable().optional(),
|
|
30
|
+
sourceEntityType: z.string().nullable().optional(),
|
|
31
|
+
sourceEntityId: z.string().uuid().nullable().optional(),
|
|
32
|
+
linkHref: z.string().nullable().optional(),
|
|
33
|
+
createdAt: z.string(),
|
|
34
|
+
readAt: z.string().nullable().optional(),
|
|
35
|
+
actionTaken: z.string().nullable().optional(),
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
export const okResponseSchema = z.object({
|
|
39
|
+
ok: z.boolean(),
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
export const unreadCountResponseSchema = z.object({
|
|
43
|
+
unreadCount: z.number(),
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
export const actionResultResponseSchema = z.object({
|
|
47
|
+
ok: z.boolean(),
|
|
48
|
+
result: z.unknown().optional(),
|
|
49
|
+
href: z.string().optional(),
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
export { createPagedListResponseSchema, listNotificationsSchema, createNotificationSchema, executeActionSchema }
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createRoleNotificationSchema } from '../../data/validators'
|
|
2
|
+
import { createBulkNotificationRoute, createBulkNotificationOpenApi } from '../../lib/routeHelpers'
|
|
3
|
+
|
|
4
|
+
export const metadata = {
|
|
5
|
+
POST: { requireAuth: true, requireFeatures: ['notifications.create'] },
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const POST = createBulkNotificationRoute(createRoleNotificationSchema, 'createForRole')
|
|
9
|
+
|
|
10
|
+
export const openApi = createBulkNotificationOpenApi(
|
|
11
|
+
createRoleNotificationSchema,
|
|
12
|
+
'Create notifications for all users in a role',
|
|
13
|
+
'Send the same notification to all users who have the specified role within the organization'
|
|
14
|
+
)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import type { EntityManager } from '@mikro-orm/core'
|
|
3
|
+
import { Notification } from '../data/entities'
|
|
4
|
+
import { listNotificationsSchema, createNotificationSchema } from '../data/validators'
|
|
5
|
+
import { toNotificationDto } from '../lib/notificationMapper'
|
|
6
|
+
import { resolveNotificationContext } from '../lib/routeHelpers'
|
|
7
|
+
import {
|
|
8
|
+
buildNotificationsCrudOpenApi,
|
|
9
|
+
createPagedListResponseSchema,
|
|
10
|
+
notificationItemSchema,
|
|
11
|
+
} from './openapi'
|
|
12
|
+
|
|
13
|
+
export const metadata = {
|
|
14
|
+
GET: { requireAuth: true },
|
|
15
|
+
POST: { requireAuth: true, requireFeatures: ['notifications.create'] },
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function GET(req: Request) {
|
|
19
|
+
const { ctx, scope } = await resolveNotificationContext(req)
|
|
20
|
+
const em = ctx.container.resolve('em') as EntityManager
|
|
21
|
+
|
|
22
|
+
const url = new URL(req.url)
|
|
23
|
+
const queryParams = Object.fromEntries(url.searchParams.entries())
|
|
24
|
+
const input = listNotificationsSchema.parse(queryParams)
|
|
25
|
+
|
|
26
|
+
const filters: Record<string, unknown> = {
|
|
27
|
+
recipientUserId: scope.userId,
|
|
28
|
+
tenantId: scope.tenantId,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (input.status) {
|
|
32
|
+
filters.status = Array.isArray(input.status) ? { $in: input.status } : input.status
|
|
33
|
+
} else {
|
|
34
|
+
filters.status = { $ne: 'dismissed' }
|
|
35
|
+
}
|
|
36
|
+
if (input.type) {
|
|
37
|
+
filters.type = input.type
|
|
38
|
+
}
|
|
39
|
+
if (input.severity) {
|
|
40
|
+
filters.severity = input.severity
|
|
41
|
+
}
|
|
42
|
+
if (input.sourceEntityType) {
|
|
43
|
+
filters.sourceEntityType = input.sourceEntityType
|
|
44
|
+
}
|
|
45
|
+
if (input.sourceEntityId) {
|
|
46
|
+
filters.sourceEntityId = input.sourceEntityId
|
|
47
|
+
}
|
|
48
|
+
if (input.since) {
|
|
49
|
+
filters.createdAt = { $gt: new Date(input.since) }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const [notifications, total] = await Promise.all([
|
|
53
|
+
em.find(Notification, filters, {
|
|
54
|
+
orderBy: { createdAt: 'desc' },
|
|
55
|
+
limit: input.pageSize,
|
|
56
|
+
offset: (input.page - 1) * input.pageSize,
|
|
57
|
+
}),
|
|
58
|
+
em.count(Notification, filters),
|
|
59
|
+
])
|
|
60
|
+
|
|
61
|
+
const items = notifications.map(toNotificationDto)
|
|
62
|
+
|
|
63
|
+
return Response.json({
|
|
64
|
+
items,
|
|
65
|
+
total,
|
|
66
|
+
page: input.page,
|
|
67
|
+
pageSize: input.pageSize,
|
|
68
|
+
totalPages: Math.ceil(total / input.pageSize),
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function POST(req: Request) {
|
|
73
|
+
const { service, scope } = await resolveNotificationContext(req)
|
|
74
|
+
|
|
75
|
+
const body = await req.json().catch(() => ({}))
|
|
76
|
+
const input = createNotificationSchema.parse(body)
|
|
77
|
+
|
|
78
|
+
const notification = await service.create(input, scope)
|
|
79
|
+
|
|
80
|
+
return Response.json({ id: notification.id }, { status: 201 })
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const openApi = buildNotificationsCrudOpenApi({
|
|
84
|
+
resourceName: 'Notification',
|
|
85
|
+
querySchema: listNotificationsSchema,
|
|
86
|
+
listResponseSchema: createPagedListResponseSchema(notificationItemSchema),
|
|
87
|
+
create: {
|
|
88
|
+
schema: createNotificationSchema,
|
|
89
|
+
responseSchema: z.object({ id: z.string().uuid() }),
|
|
90
|
+
description: 'Creates a notification for a user.',
|
|
91
|
+
},
|
|
92
|
+
})
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
3
|
+
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
4
|
+
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
5
|
+
import { notificationDeliveryConfigSchema } from '../../data/validators'
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_NOTIFICATION_DELIVERY_CONFIG,
|
|
8
|
+
resolveNotificationDeliveryConfig,
|
|
9
|
+
saveNotificationDeliveryConfig,
|
|
10
|
+
} from '../../lib/deliveryConfig'
|
|
11
|
+
|
|
12
|
+
export const metadata = {
|
|
13
|
+
GET: { requireAuth: true, requireFeatures: ['notifications.manage'] },
|
|
14
|
+
POST: { requireAuth: true, requireFeatures: ['notifications.manage'] },
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const unauthorized = async () => {
|
|
18
|
+
const { t } = await resolveTranslations()
|
|
19
|
+
return NextResponse.json({ error: t('api.errors.unauthorized', 'Unauthorized') }, { status: 401 })
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function GET(req: Request) {
|
|
23
|
+
const auth = await getAuthFromRequest(req)
|
|
24
|
+
if (!auth?.sub) return await unauthorized()
|
|
25
|
+
|
|
26
|
+
const container = await createRequestContainer()
|
|
27
|
+
try {
|
|
28
|
+
const settings = await resolveNotificationDeliveryConfig(container, {
|
|
29
|
+
defaultValue: DEFAULT_NOTIFICATION_DELIVERY_CONFIG,
|
|
30
|
+
})
|
|
31
|
+
return NextResponse.json({ settings })
|
|
32
|
+
} finally {
|
|
33
|
+
const disposable = container as unknown as { dispose?: () => Promise<void> }
|
|
34
|
+
if (typeof disposable.dispose === 'function') {
|
|
35
|
+
await disposable.dispose()
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function POST(req: Request) {
|
|
41
|
+
const { t } = await resolveTranslations()
|
|
42
|
+
const auth = await getAuthFromRequest(req)
|
|
43
|
+
if (!auth?.sub) return await unauthorized()
|
|
44
|
+
|
|
45
|
+
let body: unknown
|
|
46
|
+
try {
|
|
47
|
+
body = await req.json()
|
|
48
|
+
} catch {
|
|
49
|
+
return NextResponse.json(
|
|
50
|
+
{ error: t('api.errors.invalidPayload', 'Invalid request body') },
|
|
51
|
+
{ status: 400 }
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const parsed = notificationDeliveryConfigSchema.safeParse(body)
|
|
56
|
+
if (!parsed.success) {
|
|
57
|
+
return NextResponse.json(
|
|
58
|
+
{ error: t('notifications.delivery.settings.invalid', 'Invalid delivery settings') },
|
|
59
|
+
{ status: 400 }
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const container = await createRequestContainer()
|
|
64
|
+
try {
|
|
65
|
+
await saveNotificationDeliveryConfig(container, parsed.data)
|
|
66
|
+
const settings = await resolveNotificationDeliveryConfig(container, {
|
|
67
|
+
defaultValue: DEFAULT_NOTIFICATION_DELIVERY_CONFIG,
|
|
68
|
+
})
|
|
69
|
+
return NextResponse.json({ ok: true, settings })
|
|
70
|
+
} catch (error) {
|
|
71
|
+
return NextResponse.json(
|
|
72
|
+
{ error: error instanceof Error ? error.message : t('api.errors.internal', 'Internal error') },
|
|
73
|
+
{ status: 500 }
|
|
74
|
+
)
|
|
75
|
+
} finally {
|
|
76
|
+
const disposable = container as unknown as { dispose?: () => Promise<void> }
|
|
77
|
+
if (typeof disposable.dispose === 'function') {
|
|
78
|
+
await disposable.dispose()
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const openApi = {
|
|
84
|
+
GET: {
|
|
85
|
+
summary: 'Get notification delivery settings',
|
|
86
|
+
tags: ['Notifications'],
|
|
87
|
+
responses: {
|
|
88
|
+
200: { description: 'Current delivery settings' },
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
POST: {
|
|
92
|
+
summary: 'Update notification delivery settings',
|
|
93
|
+
tags: ['Notifications'],
|
|
94
|
+
responses: {
|
|
95
|
+
200: { description: 'Delivery settings updated' },
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/core'
|
|
2
|
+
import { Notification } from '../../data/entities'
|
|
3
|
+
import { unreadCountResponseSchema } from '../openapi'
|
|
4
|
+
import { resolveNotificationContext } from '../../lib/routeHelpers'
|
|
5
|
+
|
|
6
|
+
export const metadata = {
|
|
7
|
+
GET: { requireAuth: true },
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function GET(req: Request) {
|
|
11
|
+
const { scope, ctx } = await resolveNotificationContext(req)
|
|
12
|
+
const em = ctx.container.resolve('em') as EntityManager
|
|
13
|
+
|
|
14
|
+
const count = await em.count(Notification, {
|
|
15
|
+
recipientUserId: scope.userId,
|
|
16
|
+
tenantId: scope.tenantId,
|
|
17
|
+
status: 'unread',
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
return Response.json({ unreadCount: count })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const openApi = {
|
|
24
|
+
GET: {
|
|
25
|
+
summary: 'Get unread notification count',
|
|
26
|
+
tags: ['Notifications'],
|
|
27
|
+
responses: {
|
|
28
|
+
200: {
|
|
29
|
+
description: 'Unread count',
|
|
30
|
+
content: {
|
|
31
|
+
'application/json': {
|
|
32
|
+
schema: unreadCountResponseSchema,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
const bellIcon = React.createElement(
|
|
4
|
+
'svg',
|
|
5
|
+
{ width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' },
|
|
6
|
+
React.createElement('path', { d: 'M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9' }),
|
|
7
|
+
React.createElement('path', { d: 'M13.73 21a2 2 0 0 1-3.46 0' }),
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
export const metadata = {
|
|
11
|
+
requireAuth: true,
|
|
12
|
+
requireFeatures: ['notifications.manage'],
|
|
13
|
+
pageTitle: 'Notification Delivery',
|
|
14
|
+
pageTitleKey: 'notifications.settings.pageTitle',
|
|
15
|
+
pageGroup: 'Configuration',
|
|
16
|
+
pageGroupKey: 'backend.nav.configuration',
|
|
17
|
+
pageOrder: 435,
|
|
18
|
+
icon: bellIcon,
|
|
19
|
+
breadcrumb: [
|
|
20
|
+
{ label: 'Notification Delivery', labelKey: 'notifications.settings.pageTitle' },
|
|
21
|
+
],
|
|
22
|
+
} as const
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
2
|
+
import { NotificationSettingsPageClient } from '../../../frontend/NotificationSettingsPageClient'
|
|
3
|
+
|
|
4
|
+
export default async function NotificationSettingsPage() {
|
|
5
|
+
return (
|
|
6
|
+
<Page>
|
|
7
|
+
<PageBody>
|
|
8
|
+
<NotificationSettingsPageClient />
|
|
9
|
+
</PageBody>
|
|
10
|
+
</Page>
|
|
11
|
+
)
|
|
12
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ModuleCli } from '@open-mercato/shared/modules/registry'
|
|
2
|
+
import { createQueue } from '@open-mercato/queue'
|
|
3
|
+
import type { CleanupExpiredJob } from './workers/create-notification.worker'
|
|
4
|
+
|
|
5
|
+
const cleanupExpiredCommand: ModuleCli = {
|
|
6
|
+
command: 'cleanup-expired',
|
|
7
|
+
async run() {
|
|
8
|
+
const queue = createQueue('notifications', 'async')
|
|
9
|
+
|
|
10
|
+
await queue.enqueue({
|
|
11
|
+
type: 'cleanup-expired',
|
|
12
|
+
} satisfies CleanupExpiredJob)
|
|
13
|
+
|
|
14
|
+
console.log('✓ Cleanup job enqueued')
|
|
15
|
+
},
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default [cleanupExpiredCommand]
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Entity, PrimaryKey, Property, Index, OptionalProps } from '@mikro-orm/core'
|
|
2
|
+
import type { NotificationActionData } from '@open-mercato/shared/modules/notifications/types'
|
|
3
|
+
|
|
4
|
+
export type NotificationStatus = 'unread' | 'read' | 'actioned' | 'dismissed'
|
|
5
|
+
export type NotificationSeverity = 'info' | 'warning' | 'success' | 'error'
|
|
6
|
+
|
|
7
|
+
@Entity({ tableName: 'notifications' })
|
|
8
|
+
@Index({ name: 'notifications_recipient_status_idx', properties: ['recipientUserId', 'status', 'createdAt'] })
|
|
9
|
+
@Index({ name: 'notifications_source_idx', properties: ['sourceEntityType', 'sourceEntityId'] })
|
|
10
|
+
@Index({ name: 'notifications_tenant_idx', properties: ['tenantId', 'organizationId'] })
|
|
11
|
+
@Index({ name: 'notifications_expires_idx', properties: ['expiresAt'] })
|
|
12
|
+
@Index({ name: 'notifications_group_idx', properties: ['groupKey', 'recipientUserId'] })
|
|
13
|
+
export class Notification {
|
|
14
|
+
[OptionalProps]?: 'status' | 'severity' | 'createdAt'
|
|
15
|
+
|
|
16
|
+
@PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
|
|
17
|
+
id!: string
|
|
18
|
+
|
|
19
|
+
@Property({ name: 'recipient_user_id', type: 'uuid' })
|
|
20
|
+
recipientUserId!: string
|
|
21
|
+
|
|
22
|
+
@Property({ name: 'type', type: 'text' })
|
|
23
|
+
type!: string
|
|
24
|
+
|
|
25
|
+
// i18n keys (preferred for i18n-first approach)
|
|
26
|
+
@Property({ name: 'title_key', type: 'text', nullable: true })
|
|
27
|
+
titleKey?: string | null
|
|
28
|
+
|
|
29
|
+
@Property({ name: 'body_key', type: 'text', nullable: true })
|
|
30
|
+
bodyKey?: string | null
|
|
31
|
+
|
|
32
|
+
// Template variables for i18n interpolation (stored as JSONB)
|
|
33
|
+
@Property({ name: 'title_variables', type: 'json', nullable: true })
|
|
34
|
+
titleVariables?: Record<string, string> | null
|
|
35
|
+
|
|
36
|
+
@Property({ name: 'body_variables', type: 'json', nullable: true })
|
|
37
|
+
bodyVariables?: Record<string, string> | null
|
|
38
|
+
|
|
39
|
+
// Fallback text (for backward compatibility or when keys are not available)
|
|
40
|
+
@Property({ name: 'title', type: 'text' })
|
|
41
|
+
title!: string
|
|
42
|
+
|
|
43
|
+
@Property({ name: 'body', type: 'text', nullable: true })
|
|
44
|
+
body?: string | null
|
|
45
|
+
|
|
46
|
+
@Property({ name: 'icon', type: 'text', nullable: true })
|
|
47
|
+
icon?: string | null
|
|
48
|
+
|
|
49
|
+
@Property({ name: 'severity', type: 'text', default: 'info' })
|
|
50
|
+
severity: NotificationSeverity = 'info'
|
|
51
|
+
|
|
52
|
+
@Property({ name: 'status', type: 'text', default: 'unread' })
|
|
53
|
+
status: NotificationStatus = 'unread'
|
|
54
|
+
|
|
55
|
+
@Property({ name: 'action_data', type: 'json', nullable: true })
|
|
56
|
+
actionData?: NotificationActionData | null
|
|
57
|
+
|
|
58
|
+
@Property({ name: 'action_result', type: 'json', nullable: true })
|
|
59
|
+
actionResult?: Record<string, unknown> | null
|
|
60
|
+
|
|
61
|
+
@Property({ name: 'action_taken', type: 'text', nullable: true })
|
|
62
|
+
actionTaken?: string | null
|
|
63
|
+
|
|
64
|
+
@Property({ name: 'source_module', type: 'text', nullable: true })
|
|
65
|
+
sourceModule?: string | null
|
|
66
|
+
|
|
67
|
+
@Property({ name: 'source_entity_type', type: 'text', nullable: true })
|
|
68
|
+
sourceEntityType?: string | null
|
|
69
|
+
|
|
70
|
+
@Property({ name: 'source_entity_id', type: 'uuid', nullable: true })
|
|
71
|
+
sourceEntityId?: string | null
|
|
72
|
+
|
|
73
|
+
@Property({ name: 'link_href', type: 'text', nullable: true })
|
|
74
|
+
linkHref?: string | null
|
|
75
|
+
|
|
76
|
+
@Property({ name: 'group_key', type: 'text', nullable: true })
|
|
77
|
+
groupKey?: string | null
|
|
78
|
+
|
|
79
|
+
@Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
|
|
80
|
+
createdAt: Date = new Date()
|
|
81
|
+
|
|
82
|
+
@Property({ name: 'read_at', type: Date, nullable: true })
|
|
83
|
+
readAt?: Date | null
|
|
84
|
+
|
|
85
|
+
@Property({ name: 'actioned_at', type: Date, nullable: true })
|
|
86
|
+
actionedAt?: Date | null
|
|
87
|
+
|
|
88
|
+
@Property({ name: 'dismissed_at', type: Date, nullable: true })
|
|
89
|
+
dismissedAt?: Date | null
|
|
90
|
+
|
|
91
|
+
@Property({ name: 'expires_at', type: Date, nullable: true })
|
|
92
|
+
expiresAt?: Date | null
|
|
93
|
+
|
|
94
|
+
@Property({ name: 'tenant_id', type: 'uuid' })
|
|
95
|
+
tenantId!: string
|
|
96
|
+
|
|
97
|
+
@Property({ name: 'organization_id', type: 'uuid', nullable: true })
|
|
98
|
+
organizationId?: string | null
|
|
99
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { isSafeNotificationHref } from '../lib/safeHref'
|
|
3
|
+
|
|
4
|
+
export const notificationStatusSchema = z.enum(['unread', 'read', 'actioned', 'dismissed'])
|
|
5
|
+
export const notificationSeveritySchema = z.enum(['info', 'warning', 'success', 'error'])
|
|
6
|
+
|
|
7
|
+
export const safeRelativeHrefSchema = z.string().min(1).refine(
|
|
8
|
+
(href) => isSafeNotificationHref(href),
|
|
9
|
+
{ message: 'Href must be a same-origin relative path starting with /' }
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
export const notificationActionSchema = z.object({
|
|
13
|
+
id: z.string().min(1),
|
|
14
|
+
label: z.string().min(1),
|
|
15
|
+
labelKey: z.string().optional(),
|
|
16
|
+
variant: z.enum(['default', 'secondary', 'destructive', 'outline', 'ghost']).optional(),
|
|
17
|
+
icon: z.string().optional(),
|
|
18
|
+
commandId: z.string().optional(),
|
|
19
|
+
href: safeRelativeHrefSchema.optional(),
|
|
20
|
+
confirmRequired: z.boolean().optional(),
|
|
21
|
+
confirmMessage: z.string().optional(),
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const baseNotificationFieldsSchema = z.object({
|
|
25
|
+
type: z.string().min(1).max(100),
|
|
26
|
+
titleKey: z.string().min(1).max(200).optional(),
|
|
27
|
+
bodyKey: z.string().min(1).max(200).optional(),
|
|
28
|
+
titleVariables: z.record(z.string(), z.string()).optional(),
|
|
29
|
+
bodyVariables: z.record(z.string(), z.string()).optional(),
|
|
30
|
+
title: z.string().min(1).max(500).optional(),
|
|
31
|
+
body: z.string().max(2000).optional(),
|
|
32
|
+
icon: z.string().max(100).optional(),
|
|
33
|
+
severity: notificationSeveritySchema.optional().default('info'),
|
|
34
|
+
actions: z.array(notificationActionSchema).optional(),
|
|
35
|
+
primaryActionId: z.string().optional(),
|
|
36
|
+
sourceModule: z.string().optional(),
|
|
37
|
+
sourceEntityType: z.string().optional(),
|
|
38
|
+
sourceEntityId: z.string().uuid().optional(),
|
|
39
|
+
linkHref: safeRelativeHrefSchema.optional(),
|
|
40
|
+
groupKey: z.string().optional(),
|
|
41
|
+
expiresAt: z.string().datetime().optional(),
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const titleRequiredRefinement = {
|
|
45
|
+
refine: (data: { titleKey?: string; title?: string }) => data.titleKey || data.title,
|
|
46
|
+
message: 'Either titleKey or title must be provided',
|
|
47
|
+
} as const
|
|
48
|
+
|
|
49
|
+
export const createNotificationSchema = baseNotificationFieldsSchema
|
|
50
|
+
.extend({ recipientUserId: z.string().uuid() })
|
|
51
|
+
.refine(titleRequiredRefinement.refine, { message: titleRequiredRefinement.message })
|
|
52
|
+
|
|
53
|
+
export const createBatchNotificationSchema = baseNotificationFieldsSchema
|
|
54
|
+
.extend({ recipientUserIds: z.array(z.string().uuid()).min(1).max(1000) })
|
|
55
|
+
.refine(titleRequiredRefinement.refine, { message: titleRequiredRefinement.message })
|
|
56
|
+
|
|
57
|
+
export const createRoleNotificationSchema = baseNotificationFieldsSchema
|
|
58
|
+
.extend({ roleId: z.string().uuid() })
|
|
59
|
+
.refine(titleRequiredRefinement.refine, { message: titleRequiredRefinement.message })
|
|
60
|
+
|
|
61
|
+
export const createFeatureNotificationSchema = baseNotificationFieldsSchema
|
|
62
|
+
.extend({ requiredFeature: z.string().min(1).max(100) })
|
|
63
|
+
.refine(titleRequiredRefinement.refine, { message: titleRequiredRefinement.message })
|
|
64
|
+
|
|
65
|
+
export const listNotificationsSchema = z.object({
|
|
66
|
+
status: z.union([notificationStatusSchema, z.array(notificationStatusSchema)]).optional(),
|
|
67
|
+
type: z.string().optional(),
|
|
68
|
+
severity: notificationSeveritySchema.optional(),
|
|
69
|
+
sourceEntityType: z.string().optional(),
|
|
70
|
+
sourceEntityId: z.string().uuid().optional(),
|
|
71
|
+
since: z.string().datetime().optional(),
|
|
72
|
+
page: z.coerce.number().int().min(1).optional().default(1),
|
|
73
|
+
pageSize: z.coerce.number().int().min(1).max(100).optional().default(20),
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
export const executeActionSchema = z.object({
|
|
77
|
+
actionId: z.string().min(1),
|
|
78
|
+
payload: z.record(z.string(), z.unknown()).optional(),
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
export const restoreNotificationSchema = z.object({
|
|
82
|
+
status: z.enum(['read', 'unread']).optional(),
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const notificationDeliveryStrategySchema = z.object({
|
|
86
|
+
enabled: z.boolean().optional(),
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const notificationDeliveryEmailSchema = notificationDeliveryStrategySchema.extend({
|
|
90
|
+
from: z.string().trim().min(1).optional(),
|
|
91
|
+
replyTo: z.string().trim().min(1).optional(),
|
|
92
|
+
subjectPrefix: z.string().trim().min(1).optional(),
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
export const notificationDeliveryConfigSchema = z.object({
|
|
96
|
+
appUrl: z.string().url().optional(),
|
|
97
|
+
panelPath: safeRelativeHrefSchema.optional(),
|
|
98
|
+
strategies: z.object({
|
|
99
|
+
database: notificationDeliveryStrategySchema.optional(),
|
|
100
|
+
email: notificationDeliveryEmailSchema.optional(),
|
|
101
|
+
}).optional(),
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
export type CreateNotificationInput = z.infer<typeof createNotificationSchema>
|
|
105
|
+
export type CreateBatchNotificationInput = z.infer<typeof createBatchNotificationSchema>
|
|
106
|
+
export type CreateRoleNotificationInput = z.infer<typeof createRoleNotificationSchema>
|
|
107
|
+
export type CreateFeatureNotificationInput = z.infer<typeof createFeatureNotificationSchema>
|
|
108
|
+
export type ListNotificationsInput = z.infer<typeof listNotificationsSchema>
|
|
109
|
+
export type ExecuteActionInput = z.infer<typeof executeActionSchema>
|
|
110
|
+
export type NotificationDeliveryConfigInput = z.infer<typeof notificationDeliveryConfigSchema>
|