@open-mercato/core 0.4.2-canary-7c76659938 → 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,35 @@
|
|
|
1
|
+
import { hasFeature } from "@open-mercato/shared/security/features";
|
|
2
|
+
function normalizeFeatures(features) {
|
|
3
|
+
if (!Array.isArray(features)) return void 0;
|
|
4
|
+
const normalized = features.filter((feature) => typeof feature === "string");
|
|
5
|
+
return normalized.length ? normalized : void 0;
|
|
6
|
+
}
|
|
7
|
+
function collectUsersWithFeature(userIdsSet, rows, requiredFeature) {
|
|
8
|
+
for (const row of rows) {
|
|
9
|
+
if (row.is_super_admin) {
|
|
10
|
+
userIdsSet.add(row.user_id);
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
const features = normalizeFeatures(row.features_json);
|
|
14
|
+
if (features && hasFeature(features, requiredFeature)) {
|
|
15
|
+
userIdsSet.add(row.user_id);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
async function getRecipientUserIdsForRole(knex, tenantId, roleId) {
|
|
20
|
+
const userRoles = await knex("user_roles").join("users", "user_roles.user_id", "users.id").where("user_roles.role_id", roleId).whereNull("user_roles.deleted_at").whereNull("users.deleted_at").where("users.tenant_id", tenantId).select("users.id as user_id");
|
|
21
|
+
return userRoles.map((row) => row.user_id);
|
|
22
|
+
}
|
|
23
|
+
async function getRecipientUserIdsForFeature(knex, tenantId, requiredFeature) {
|
|
24
|
+
const userIdsSet = /* @__PURE__ */ new Set();
|
|
25
|
+
const userAcls = await knex("user_acls").join("users", "user_acls.user_id", "users.id").where("user_acls.tenant_id", tenantId).whereNull("user_acls.deleted_at").whereNull("users.deleted_at").where("users.tenant_id", tenantId).select("users.id as user_id", "user_acls.features_json", "user_acls.is_super_admin");
|
|
26
|
+
collectUsersWithFeature(userIdsSet, userAcls, requiredFeature);
|
|
27
|
+
const roleAcls = await knex("role_acls").join("user_roles", "role_acls.role_id", "user_roles.role_id").join("users", "user_roles.user_id", "users.id").where("role_acls.tenant_id", tenantId).whereNull("role_acls.deleted_at").whereNull("user_roles.deleted_at").whereNull("users.deleted_at").where("users.tenant_id", tenantId).select("users.id as user_id", "role_acls.features_json", "role_acls.is_super_admin");
|
|
28
|
+
collectUsersWithFeature(userIdsSet, roleAcls, requiredFeature);
|
|
29
|
+
return Array.from(userIdsSet);
|
|
30
|
+
}
|
|
31
|
+
export {
|
|
32
|
+
getRecipientUserIdsForFeature,
|
|
33
|
+
getRecipientUserIdsForRole
|
|
34
|
+
};
|
|
35
|
+
//# sourceMappingURL=notificationRecipients.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/notifications/lib/notificationRecipients.ts"],
|
|
4
|
+
"sourcesContent": ["import type { Knex } from 'knex'\nimport { hasFeature } from '@open-mercato/shared/security/features'\n\ninterface AclRow {\n user_id: string\n features_json: unknown\n is_super_admin: boolean\n}\n\nfunction normalizeFeatures(features: unknown): string[] | undefined {\n if (!Array.isArray(features)) return undefined\n const normalized = features.filter((feature): feature is string => typeof feature === 'string')\n return normalized.length ? normalized : undefined\n}\n\n/**\n * Extract user IDs from ACL rows that have the required feature or are super admins.\n */\nfunction collectUsersWithFeature(\n userIdsSet: Set<string>,\n rows: AclRow[],\n requiredFeature: string\n): void {\n for (const row of rows) {\n if (row.is_super_admin) {\n userIdsSet.add(row.user_id)\n continue\n }\n\n const features = normalizeFeatures(row.features_json)\n if (features && hasFeature(features, requiredFeature)) {\n userIdsSet.add(row.user_id)\n }\n }\n}\n\nexport async function getRecipientUserIdsForRole(\n knex: Knex,\n tenantId: string,\n roleId: string\n): Promise<string[]> {\n const userRoles = await knex('user_roles')\n .join('users', 'user_roles.user_id', 'users.id')\n .where('user_roles.role_id', roleId)\n .whereNull('user_roles.deleted_at')\n .whereNull('users.deleted_at')\n .where('users.tenant_id', tenantId)\n .select('users.id as user_id')\n\n return userRoles.map((row: { user_id: string }) => row.user_id)\n}\n\nexport async function getRecipientUserIdsForFeature(\n knex: Knex,\n tenantId: string,\n requiredFeature: string\n): Promise<string[]> {\n const userIdsSet = new Set<string>()\n\n const userAcls = await knex('user_acls')\n .join('users', 'user_acls.user_id', 'users.id')\n .where('user_acls.tenant_id', tenantId)\n .whereNull('user_acls.deleted_at')\n .whereNull('users.deleted_at')\n .where('users.tenant_id', tenantId)\n .select('users.id as user_id', 'user_acls.features_json', 'user_acls.is_super_admin')\n\n collectUsersWithFeature(userIdsSet, userAcls, requiredFeature)\n\n const roleAcls = await knex('role_acls')\n .join('user_roles', 'role_acls.role_id', 'user_roles.role_id')\n .join('users', 'user_roles.user_id', 'users.id')\n .where('role_acls.tenant_id', tenantId)\n .whereNull('role_acls.deleted_at')\n .whereNull('user_roles.deleted_at')\n .whereNull('users.deleted_at')\n .where('users.tenant_id', tenantId)\n .select('users.id as user_id', 'role_acls.features_json', 'role_acls.is_super_admin')\n\n collectUsersWithFeature(userIdsSet, roleAcls, requiredFeature)\n\n return Array.from(userIdsSet)\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,kBAAkB;AAQ3B,SAAS,kBAAkB,UAAyC;AAClE,MAAI,CAAC,MAAM,QAAQ,QAAQ,EAAG,QAAO;AACrC,QAAM,aAAa,SAAS,OAAO,CAAC,YAA+B,OAAO,YAAY,QAAQ;AAC9F,SAAO,WAAW,SAAS,aAAa;AAC1C;AAKA,SAAS,wBACP,YACA,MACA,iBACM;AACN,aAAW,OAAO,MAAM;AACtB,QAAI,IAAI,gBAAgB;AACtB,iBAAW,IAAI,IAAI,OAAO;AAC1B;AAAA,IACF;AAEA,UAAM,WAAW,kBAAkB,IAAI,aAAa;AACpD,QAAI,YAAY,WAAW,UAAU,eAAe,GAAG;AACrD,iBAAW,IAAI,IAAI,OAAO;AAAA,IAC5B;AAAA,EACF;AACF;AAEA,eAAsB,2BACpB,MACA,UACA,QACmB;AACnB,QAAM,YAAY,MAAM,KAAK,YAAY,EACtC,KAAK,SAAS,sBAAsB,UAAU,EAC9C,MAAM,sBAAsB,MAAM,EAClC,UAAU,uBAAuB,EACjC,UAAU,kBAAkB,EAC5B,MAAM,mBAAmB,QAAQ,EACjC,OAAO,qBAAqB;AAE/B,SAAO,UAAU,IAAI,CAAC,QAA6B,IAAI,OAAO;AAChE;AAEA,eAAsB,8BACpB,MACA,UACA,iBACmB;AACnB,QAAM,aAAa,oBAAI,IAAY;AAEnC,QAAM,WAAW,MAAM,KAAK,WAAW,EACpC,KAAK,SAAS,qBAAqB,UAAU,EAC7C,MAAM,uBAAuB,QAAQ,EACrC,UAAU,sBAAsB,EAChC,UAAU,kBAAkB,EAC5B,MAAM,mBAAmB,QAAQ,EACjC,OAAO,uBAAuB,2BAA2B,0BAA0B;AAEtF,0BAAwB,YAAY,UAAU,eAAe;AAE7D,QAAM,WAAW,MAAM,KAAK,WAAW,EACpC,KAAK,cAAc,qBAAqB,oBAAoB,EAC5D,KAAK,SAAS,sBAAsB,UAAU,EAC9C,MAAM,uBAAuB,QAAQ,EACrC,UAAU,sBAAsB,EAChC,UAAU,uBAAuB,EACjC,UAAU,kBAAkB,EAC5B,MAAM,mBAAmB,QAAQ,EACjC,OAAO,uBAAuB,2BAA2B,0BAA0B;AAEtF,0BAAwB,YAAY,UAAU,eAAe;AAE7D,SAAO,MAAM,KAAK,UAAU;AAC9B;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { Notification } from "../data/entities.js";
|
|
2
|
+
import { NOTIFICATION_EVENTS } from "./events.js";
|
|
3
|
+
import { buildNotificationEntity, emitNotificationCreated, emitNotificationCreatedBatch } from "./notificationFactory.js";
|
|
4
|
+
import { toNotificationDto } from "./notificationMapper.js";
|
|
5
|
+
import { getRecipientUserIdsForFeature, getRecipientUserIdsForRole } from "./notificationRecipients.js";
|
|
6
|
+
const DEBUG = process.env.NOTIFICATIONS_DEBUG === "true";
|
|
7
|
+
function debug(...args) {
|
|
8
|
+
if (DEBUG) {
|
|
9
|
+
console.log("[notifications]", ...args);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function getKnex(em) {
|
|
13
|
+
return em.getConnection().getKnex();
|
|
14
|
+
}
|
|
15
|
+
function createNotificationService(deps) {
|
|
16
|
+
const { em: rootEm, eventBus, commandBus, container } = deps;
|
|
17
|
+
return {
|
|
18
|
+
async create(input, ctx) {
|
|
19
|
+
const em = rootEm.fork();
|
|
20
|
+
const { recipientUserId, ...content } = input;
|
|
21
|
+
const notification = buildNotificationEntity(em, content, recipientUserId, ctx);
|
|
22
|
+
await em.persistAndFlush(notification);
|
|
23
|
+
await emitNotificationCreated(eventBus, notification, ctx);
|
|
24
|
+
return notification;
|
|
25
|
+
},
|
|
26
|
+
async createBatch(input, ctx) {
|
|
27
|
+
const em = rootEm.fork();
|
|
28
|
+
const { recipientUserIds, ...content } = input;
|
|
29
|
+
const notifications = [];
|
|
30
|
+
for (const recipientUserId of recipientUserIds) {
|
|
31
|
+
const notification = buildNotificationEntity(em, content, recipientUserId, ctx);
|
|
32
|
+
notifications.push(notification);
|
|
33
|
+
}
|
|
34
|
+
await em.persistAndFlush(notifications);
|
|
35
|
+
await emitNotificationCreatedBatch(eventBus, notifications, ctx);
|
|
36
|
+
return notifications;
|
|
37
|
+
},
|
|
38
|
+
async createForRole(input, ctx) {
|
|
39
|
+
const em = rootEm.fork();
|
|
40
|
+
const knex = getKnex(em);
|
|
41
|
+
const recipientUserIds = await getRecipientUserIdsForRole(knex, ctx.tenantId, input.roleId);
|
|
42
|
+
if (recipientUserIds.length === 0) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
const { roleId: _roleId, ...content } = input;
|
|
46
|
+
const notifications = [];
|
|
47
|
+
for (const recipientUserId of recipientUserIds) {
|
|
48
|
+
const notification = buildNotificationEntity(em, content, recipientUserId, ctx);
|
|
49
|
+
notifications.push(notification);
|
|
50
|
+
}
|
|
51
|
+
await em.persistAndFlush(notifications);
|
|
52
|
+
await emitNotificationCreatedBatch(eventBus, notifications, ctx);
|
|
53
|
+
return notifications;
|
|
54
|
+
},
|
|
55
|
+
async createForFeature(input, ctx) {
|
|
56
|
+
const em = rootEm.fork();
|
|
57
|
+
const knex = getKnex(em);
|
|
58
|
+
const recipientUserIds = await getRecipientUserIdsForFeature(knex, ctx.tenantId, input.requiredFeature);
|
|
59
|
+
if (recipientUserIds.length === 0) {
|
|
60
|
+
debug("No users found with feature:", input.requiredFeature, "in tenant:", ctx.tenantId);
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
debug("Creating notifications for", recipientUserIds.length, "user(s) with feature:", input.requiredFeature);
|
|
64
|
+
const { requiredFeature: _requiredFeature, ...content } = input;
|
|
65
|
+
const notifications = [];
|
|
66
|
+
for (const recipientUserId of recipientUserIds) {
|
|
67
|
+
const notification = buildNotificationEntity(em, content, recipientUserId, ctx);
|
|
68
|
+
notifications.push(notification);
|
|
69
|
+
}
|
|
70
|
+
await em.persistAndFlush(notifications);
|
|
71
|
+
await emitNotificationCreatedBatch(eventBus, notifications, ctx);
|
|
72
|
+
return notifications;
|
|
73
|
+
},
|
|
74
|
+
async markAsRead(notificationId, ctx) {
|
|
75
|
+
const em = rootEm.fork();
|
|
76
|
+
const notification = await em.findOneOrFail(Notification, {
|
|
77
|
+
id: notificationId,
|
|
78
|
+
recipientUserId: ctx.userId,
|
|
79
|
+
tenantId: ctx.tenantId
|
|
80
|
+
});
|
|
81
|
+
if (notification.status === "unread") {
|
|
82
|
+
notification.status = "read";
|
|
83
|
+
notification.readAt = /* @__PURE__ */ new Date();
|
|
84
|
+
await em.flush();
|
|
85
|
+
await eventBus.emit(NOTIFICATION_EVENTS.READ, {
|
|
86
|
+
notificationId: notification.id,
|
|
87
|
+
userId: ctx.userId,
|
|
88
|
+
tenantId: ctx.tenantId
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return notification;
|
|
92
|
+
},
|
|
93
|
+
async markAllAsRead(ctx) {
|
|
94
|
+
const em = rootEm.fork();
|
|
95
|
+
const knex = getKnex(em);
|
|
96
|
+
const result = await knex("notifications").where({
|
|
97
|
+
recipient_user_id: ctx.userId,
|
|
98
|
+
tenant_id: ctx.tenantId,
|
|
99
|
+
status: "unread"
|
|
100
|
+
}).update({
|
|
101
|
+
status: "read",
|
|
102
|
+
read_at: knex.fn.now()
|
|
103
|
+
});
|
|
104
|
+
return result;
|
|
105
|
+
},
|
|
106
|
+
async dismiss(notificationId, ctx) {
|
|
107
|
+
const em = rootEm.fork();
|
|
108
|
+
const notification = await em.findOneOrFail(Notification, {
|
|
109
|
+
id: notificationId,
|
|
110
|
+
recipientUserId: ctx.userId,
|
|
111
|
+
tenantId: ctx.tenantId
|
|
112
|
+
});
|
|
113
|
+
notification.status = "dismissed";
|
|
114
|
+
notification.dismissedAt = /* @__PURE__ */ new Date();
|
|
115
|
+
await em.flush();
|
|
116
|
+
await eventBus.emit(NOTIFICATION_EVENTS.DISMISSED, {
|
|
117
|
+
notificationId: notification.id,
|
|
118
|
+
userId: ctx.userId,
|
|
119
|
+
tenantId: ctx.tenantId
|
|
120
|
+
});
|
|
121
|
+
return notification;
|
|
122
|
+
},
|
|
123
|
+
async restoreDismissed(notificationId, status, ctx) {
|
|
124
|
+
const em = rootEm.fork();
|
|
125
|
+
const notification = await em.findOneOrFail(Notification, {
|
|
126
|
+
id: notificationId,
|
|
127
|
+
recipientUserId: ctx.userId,
|
|
128
|
+
tenantId: ctx.tenantId
|
|
129
|
+
});
|
|
130
|
+
if (notification.status !== "dismissed") {
|
|
131
|
+
return notification;
|
|
132
|
+
}
|
|
133
|
+
const targetStatus = status ?? "read";
|
|
134
|
+
notification.status = targetStatus;
|
|
135
|
+
notification.dismissedAt = null;
|
|
136
|
+
if (targetStatus === "unread") {
|
|
137
|
+
notification.readAt = null;
|
|
138
|
+
} else if (!notification.readAt) {
|
|
139
|
+
notification.readAt = /* @__PURE__ */ new Date();
|
|
140
|
+
}
|
|
141
|
+
await em.flush();
|
|
142
|
+
await eventBus.emit(NOTIFICATION_EVENTS.RESTORED, {
|
|
143
|
+
notificationId: notification.id,
|
|
144
|
+
userId: ctx.userId,
|
|
145
|
+
tenantId: ctx.tenantId,
|
|
146
|
+
status: targetStatus
|
|
147
|
+
});
|
|
148
|
+
return notification;
|
|
149
|
+
},
|
|
150
|
+
async executeAction(notificationId, input, ctx) {
|
|
151
|
+
const em = rootEm.fork();
|
|
152
|
+
const notification = await em.findOneOrFail(Notification, {
|
|
153
|
+
id: notificationId,
|
|
154
|
+
recipientUserId: ctx.userId,
|
|
155
|
+
tenantId: ctx.tenantId
|
|
156
|
+
});
|
|
157
|
+
const actionData = notification.actionData;
|
|
158
|
+
const action = actionData?.actions?.find((a) => a.id === input.actionId);
|
|
159
|
+
if (!action) {
|
|
160
|
+
throw new Error("Action not found");
|
|
161
|
+
}
|
|
162
|
+
let result = null;
|
|
163
|
+
if (action.commandId && commandBus && container) {
|
|
164
|
+
const commandInput = {
|
|
165
|
+
id: notification.sourceEntityId,
|
|
166
|
+
...input.payload
|
|
167
|
+
};
|
|
168
|
+
const commandCtx = {
|
|
169
|
+
container,
|
|
170
|
+
auth: {
|
|
171
|
+
sub: ctx.userId,
|
|
172
|
+
tenantId: ctx.tenantId,
|
|
173
|
+
orgId: ctx.organizationId
|
|
174
|
+
},
|
|
175
|
+
organizationScope: null,
|
|
176
|
+
selectedOrganizationId: ctx.organizationId ?? null,
|
|
177
|
+
organizationIds: ctx.organizationId ? [ctx.organizationId] : null
|
|
178
|
+
};
|
|
179
|
+
const commandResult = await commandBus.execute(action.commandId, {
|
|
180
|
+
input: commandInput,
|
|
181
|
+
ctx: commandCtx,
|
|
182
|
+
metadata: {
|
|
183
|
+
tenantId: ctx.tenantId,
|
|
184
|
+
organizationId: ctx.organizationId,
|
|
185
|
+
resourceKind: "notifications"
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
result = commandResult.result;
|
|
189
|
+
}
|
|
190
|
+
notification.status = "actioned";
|
|
191
|
+
notification.actionedAt = /* @__PURE__ */ new Date();
|
|
192
|
+
notification.actionTaken = input.actionId;
|
|
193
|
+
notification.actionResult = result;
|
|
194
|
+
if (!notification.readAt) {
|
|
195
|
+
notification.readAt = /* @__PURE__ */ new Date();
|
|
196
|
+
}
|
|
197
|
+
await em.flush();
|
|
198
|
+
await eventBus.emit(NOTIFICATION_EVENTS.ACTIONED, {
|
|
199
|
+
notificationId: notification.id,
|
|
200
|
+
actionId: input.actionId,
|
|
201
|
+
userId: ctx.userId,
|
|
202
|
+
tenantId: ctx.tenantId
|
|
203
|
+
});
|
|
204
|
+
return { notification, result };
|
|
205
|
+
},
|
|
206
|
+
async getUnreadCount(ctx) {
|
|
207
|
+
const em = rootEm.fork();
|
|
208
|
+
return em.count(Notification, {
|
|
209
|
+
recipientUserId: ctx.userId,
|
|
210
|
+
tenantId: ctx.tenantId,
|
|
211
|
+
status: "unread"
|
|
212
|
+
});
|
|
213
|
+
},
|
|
214
|
+
async getPollData(ctx, since) {
|
|
215
|
+
const em = rootEm.fork();
|
|
216
|
+
const filters = {
|
|
217
|
+
recipientUserId: ctx.userId,
|
|
218
|
+
tenantId: ctx.tenantId
|
|
219
|
+
};
|
|
220
|
+
if (since) {
|
|
221
|
+
filters.createdAt = { $gt: new Date(since) };
|
|
222
|
+
}
|
|
223
|
+
const [notifications, unreadCount] = await Promise.all([
|
|
224
|
+
em.find(Notification, filters, {
|
|
225
|
+
orderBy: { createdAt: "desc" },
|
|
226
|
+
limit: 50
|
|
227
|
+
}),
|
|
228
|
+
em.count(Notification, {
|
|
229
|
+
recipientUserId: ctx.userId,
|
|
230
|
+
tenantId: ctx.tenantId,
|
|
231
|
+
status: "unread"
|
|
232
|
+
})
|
|
233
|
+
]);
|
|
234
|
+
const recent = notifications.map(toNotificationDto);
|
|
235
|
+
const hasNew = since ? recent.length > 0 : false;
|
|
236
|
+
return {
|
|
237
|
+
unreadCount,
|
|
238
|
+
recent,
|
|
239
|
+
hasNew,
|
|
240
|
+
lastId: recent[0]?.id
|
|
241
|
+
};
|
|
242
|
+
},
|
|
243
|
+
async cleanupExpired() {
|
|
244
|
+
const em = rootEm.fork();
|
|
245
|
+
const knex = getKnex(em);
|
|
246
|
+
const result = await knex("notifications").where("expires_at", "<", knex.fn.now()).whereNotIn("status", ["actioned", "dismissed"]).update({
|
|
247
|
+
status: "dismissed",
|
|
248
|
+
dismissed_at: knex.fn.now()
|
|
249
|
+
});
|
|
250
|
+
return result;
|
|
251
|
+
},
|
|
252
|
+
async deleteBySource(sourceEntityType, sourceEntityId, ctx) {
|
|
253
|
+
const em = rootEm.fork();
|
|
254
|
+
const knex = getKnex(em);
|
|
255
|
+
const result = await knex("notifications").where({
|
|
256
|
+
source_entity_type: sourceEntityType,
|
|
257
|
+
source_entity_id: sourceEntityId,
|
|
258
|
+
tenant_id: ctx.tenantId
|
|
259
|
+
}).delete();
|
|
260
|
+
return result;
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function resolveNotificationService(container) {
|
|
265
|
+
const em = container.resolve("em");
|
|
266
|
+
const eventBus = container.resolve("eventBus");
|
|
267
|
+
let commandBus;
|
|
268
|
+
try {
|
|
269
|
+
commandBus = container.resolve("commandBus");
|
|
270
|
+
} catch {
|
|
271
|
+
commandBus = void 0;
|
|
272
|
+
}
|
|
273
|
+
return createNotificationService({ em, eventBus, commandBus, container });
|
|
274
|
+
}
|
|
275
|
+
export {
|
|
276
|
+
createNotificationService,
|
|
277
|
+
resolveNotificationService
|
|
278
|
+
};
|
|
279
|
+
//# sourceMappingURL=notificationService.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/notifications/lib/notificationService.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/core'\nimport type { Knex } from 'knex'\nimport { Notification } from '../data/entities'\nimport type { CreateNotificationInput, CreateBatchNotificationInput, CreateRoleNotificationInput, CreateFeatureNotificationInput, ExecuteActionInput } from '../data/validators'\nimport type { NotificationPollData } from '@open-mercato/shared/modules/notifications/types'\nimport { NOTIFICATION_EVENTS } from './events'\nimport { buildNotificationEntity, emitNotificationCreated, emitNotificationCreatedBatch } from './notificationFactory'\nimport { toNotificationDto } from './notificationMapper'\nimport { getRecipientUserIdsForFeature, getRecipientUserIdsForRole } from './notificationRecipients'\n\nconst DEBUG = process.env.NOTIFICATIONS_DEBUG === 'true'\n\nfunction debug(...args: unknown[]): void {\n if (DEBUG) {\n console.log('[notifications]', ...args)\n }\n}\n\nfunction getKnex(em: EntityManager): Knex {\n return (em.getConnection() as unknown as { getKnex: () => Knex }).getKnex()\n}\n\nexport interface NotificationServiceContext {\n tenantId: string\n organizationId?: string | null\n userId?: string | null\n}\n\nexport interface NotificationService {\n create(input: CreateNotificationInput, ctx: NotificationServiceContext): Promise<Notification>\n createBatch(input: CreateBatchNotificationInput, ctx: NotificationServiceContext): Promise<Notification[]>\n createForRole(input: CreateRoleNotificationInput, ctx: NotificationServiceContext): Promise<Notification[]>\n createForFeature(input: CreateFeatureNotificationInput, ctx: NotificationServiceContext): Promise<Notification[]>\n markAsRead(notificationId: string, ctx: NotificationServiceContext): Promise<Notification>\n markAllAsRead(ctx: NotificationServiceContext): Promise<number>\n dismiss(notificationId: string, ctx: NotificationServiceContext): Promise<Notification>\n restoreDismissed(\n notificationId: string,\n status: 'read' | 'unread' | undefined,\n ctx: NotificationServiceContext\n ): Promise<Notification>\n executeAction(\n notificationId: string,\n input: ExecuteActionInput,\n ctx: NotificationServiceContext\n ): Promise<{ notification: Notification; result: unknown }>\n getUnreadCount(ctx: NotificationServiceContext): Promise<number>\n getPollData(ctx: NotificationServiceContext, since?: string): Promise<NotificationPollData>\n cleanupExpired(): Promise<number>\n deleteBySource(\n sourceEntityType: string,\n sourceEntityId: string,\n ctx: NotificationServiceContext\n ): Promise<number>\n}\n\nexport interface NotificationServiceDeps {\n em: EntityManager\n eventBus: { emit: (event: string, payload: unknown) => Promise<void> }\n commandBus?: {\n execute: (\n commandId: string,\n options: { input: unknown; ctx: unknown; metadata?: unknown }\n ) => Promise<{ result: unknown }>\n }\n container?: { resolve: (name: string) => unknown }\n}\n\nexport function createNotificationService(deps: NotificationServiceDeps): NotificationService {\n const { em: rootEm, eventBus, commandBus, container } = deps\n\n return {\n async create(input, ctx) {\n const em = rootEm.fork()\n const { recipientUserId, ...content } = input\n const notification = buildNotificationEntity(em, content, recipientUserId, ctx)\n\n await em.persistAndFlush(notification)\n\n await emitNotificationCreated(eventBus, notification, ctx)\n\n return notification\n },\n\n async createBatch(input, ctx) {\n const em = rootEm.fork()\n const { recipientUserIds, ...content } = input\n const notifications: Notification[] = []\n\n for (const recipientUserId of recipientUserIds) {\n const notification = buildNotificationEntity(em, content, recipientUserId, ctx)\n notifications.push(notification)\n }\n\n await em.persistAndFlush(notifications)\n\n await emitNotificationCreatedBatch(eventBus, notifications, ctx)\n\n return notifications\n },\n\n async createForRole(input, ctx) {\n const em = rootEm.fork()\n\n const knex = getKnex(em)\n const recipientUserIds = await getRecipientUserIdsForRole(knex, ctx.tenantId, input.roleId)\n if (recipientUserIds.length === 0) {\n return []\n }\n\n const { roleId: _roleId, ...content } = input\n const notifications: Notification[] = []\n\n for (const recipientUserId of recipientUserIds) {\n const notification = buildNotificationEntity(em, content, recipientUserId, ctx)\n notifications.push(notification)\n }\n\n await em.persistAndFlush(notifications)\n\n await emitNotificationCreatedBatch(eventBus, notifications, ctx)\n\n return notifications\n },\n\n async createForFeature(input, ctx) {\n const em = rootEm.fork()\n const knex = getKnex(em)\n const recipientUserIds = await getRecipientUserIdsForFeature(knex, ctx.tenantId, input.requiredFeature)\n\n if (recipientUserIds.length === 0) {\n debug('No users found with feature:', input.requiredFeature, 'in tenant:', ctx.tenantId)\n return []\n }\n\n debug('Creating notifications for', recipientUserIds.length, 'user(s) with feature:', input.requiredFeature)\n\n const { requiredFeature: _requiredFeature, ...content } = input\n const notifications: Notification[] = []\n\n for (const recipientUserId of recipientUserIds) {\n const notification = buildNotificationEntity(em, content, recipientUserId, ctx)\n notifications.push(notification)\n }\n\n await em.persistAndFlush(notifications)\n\n await emitNotificationCreatedBatch(eventBus, notifications, ctx)\n\n return notifications\n },\n\n async markAsRead(notificationId, ctx) {\n const em = rootEm.fork()\n const notification = await em.findOneOrFail(Notification, {\n id: notificationId,\n recipientUserId: ctx.userId,\n tenantId: ctx.tenantId,\n })\n\n if (notification.status === 'unread') {\n notification.status = 'read'\n notification.readAt = new Date()\n await em.flush()\n\n await eventBus.emit(NOTIFICATION_EVENTS.READ, {\n notificationId: notification.id,\n userId: ctx.userId,\n tenantId: ctx.tenantId,\n })\n }\n\n return notification\n },\n\n async markAllAsRead(ctx) {\n const em = rootEm.fork()\n const knex = getKnex(em)\n\n const result = await knex('notifications')\n .where({\n recipient_user_id: ctx.userId,\n tenant_id: ctx.tenantId,\n status: 'unread',\n })\n .update({\n status: 'read',\n read_at: knex.fn.now(),\n })\n\n return result\n },\n\n async dismiss(notificationId, ctx) {\n const em = rootEm.fork()\n const notification = await em.findOneOrFail(Notification, {\n id: notificationId,\n recipientUserId: ctx.userId,\n tenantId: ctx.tenantId,\n })\n\n notification.status = 'dismissed'\n notification.dismissedAt = new Date()\n await em.flush()\n\n await eventBus.emit(NOTIFICATION_EVENTS.DISMISSED, {\n notificationId: notification.id,\n userId: ctx.userId,\n tenantId: ctx.tenantId,\n })\n\n return notification\n },\n\n async restoreDismissed(notificationId, status, ctx) {\n const em = rootEm.fork()\n const notification = await em.findOneOrFail(Notification, {\n id: notificationId,\n recipientUserId: ctx.userId,\n tenantId: ctx.tenantId,\n })\n\n if (notification.status !== 'dismissed') {\n return notification\n }\n\n const targetStatus = status ?? 'read'\n notification.status = targetStatus\n notification.dismissedAt = null\n\n if (targetStatus === 'unread') {\n notification.readAt = null\n } else if (!notification.readAt) {\n notification.readAt = new Date()\n }\n\n await em.flush()\n\n await eventBus.emit(NOTIFICATION_EVENTS.RESTORED, {\n notificationId: notification.id,\n userId: ctx.userId,\n tenantId: ctx.tenantId,\n status: targetStatus,\n })\n\n return notification\n },\n\n async executeAction(notificationId, input, ctx) {\n const em = rootEm.fork()\n const notification = await em.findOneOrFail(Notification, {\n id: notificationId,\n recipientUserId: ctx.userId,\n tenantId: ctx.tenantId,\n })\n\n const actionData = notification.actionData\n const action = actionData?.actions?.find((a) => a.id === input.actionId)\n\n if (!action) {\n throw new Error('Action not found')\n }\n\n let result: unknown = null\n\n if (action.commandId && commandBus && container) {\n const commandInput = {\n id: notification.sourceEntityId,\n ...input.payload,\n }\n\n // Build a CommandRuntimeContext from the notification service context\n const commandCtx = {\n container,\n auth: {\n sub: ctx.userId,\n tenantId: ctx.tenantId,\n orgId: ctx.organizationId,\n },\n organizationScope: null,\n selectedOrganizationId: ctx.organizationId ?? null,\n organizationIds: ctx.organizationId ? [ctx.organizationId] : null,\n }\n\n const commandResult = await commandBus.execute(action.commandId, {\n input: commandInput,\n ctx: commandCtx,\n metadata: {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n resourceKind: 'notifications',\n },\n })\n\n result = commandResult.result\n }\n\n notification.status = 'actioned'\n notification.actionedAt = new Date()\n notification.actionTaken = input.actionId\n notification.actionResult = result as Record<string, unknown>\n\n if (!notification.readAt) {\n notification.readAt = new Date()\n }\n\n await em.flush()\n\n await eventBus.emit(NOTIFICATION_EVENTS.ACTIONED, {\n notificationId: notification.id,\n actionId: input.actionId,\n userId: ctx.userId,\n tenantId: ctx.tenantId,\n })\n\n return { notification, result }\n },\n\n async getUnreadCount(ctx) {\n const em = rootEm.fork()\n return em.count(Notification, {\n recipientUserId: ctx.userId,\n tenantId: ctx.tenantId,\n status: 'unread',\n })\n },\n\n async getPollData(ctx, since) {\n const em = rootEm.fork()\n const filters: Record<string, unknown> = {\n recipientUserId: ctx.userId,\n tenantId: ctx.tenantId,\n }\n\n if (since) {\n filters.createdAt = { $gt: new Date(since) }\n }\n\n const [notifications, unreadCount] = await Promise.all([\n em.find(Notification, filters, {\n orderBy: { createdAt: 'desc' },\n limit: 50,\n }),\n em.count(Notification, {\n recipientUserId: ctx.userId,\n tenantId: ctx.tenantId,\n status: 'unread',\n }),\n ])\n\n const recent = notifications.map(toNotificationDto)\n const hasNew = since ? recent.length > 0 : false\n\n return {\n unreadCount,\n recent,\n hasNew,\n lastId: recent[0]?.id,\n }\n },\n\n async cleanupExpired() {\n const em = rootEm.fork()\n const knex = getKnex(em)\n\n const result = await knex('notifications')\n .where('expires_at', '<', knex.fn.now())\n .whereNotIn('status', ['actioned', 'dismissed'])\n .update({\n status: 'dismissed',\n dismissed_at: knex.fn.now(),\n })\n\n return result\n },\n\n async deleteBySource(sourceEntityType, sourceEntityId, ctx) {\n const em = rootEm.fork()\n const knex = getKnex(em)\n\n const result = await knex('notifications')\n .where({\n source_entity_type: sourceEntityType,\n source_entity_id: sourceEntityId,\n tenant_id: ctx.tenantId,\n })\n .delete()\n\n return result\n },\n }\n}\n\n/**\n * Helper to create notification service from a DI container.\n * Use this in API routes and commands to avoid DI resolution issues.\n */\nexport function resolveNotificationService(container: {\n resolve: (name: string) => unknown\n}): NotificationService {\n const em = container.resolve('em') as EntityManager\n const eventBus = container.resolve('eventBus') as { emit: (event: string, payload: unknown) => Promise<void> }\n\n // commandBus may not be registered in all contexts, so resolve it safely\n let commandBus: NotificationServiceDeps['commandBus']\n try {\n commandBus = container.resolve('commandBus') as typeof commandBus\n } catch {\n // commandBus not available - actions with commandId won't work\n commandBus = undefined\n }\n\n return createNotificationService({ em, eventBus, commandBus, container })\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { resolveRequestContext } from "@open-mercato/shared/lib/api/context";
|
|
3
|
+
import { resolveNotificationService } from "./notificationService.js";
|
|
4
|
+
async function resolveNotificationContext(req) {
|
|
5
|
+
const { ctx } = await resolveRequestContext(req);
|
|
6
|
+
return {
|
|
7
|
+
service: resolveNotificationService(ctx.container),
|
|
8
|
+
scope: {
|
|
9
|
+
tenantId: ctx.auth?.tenantId ?? "",
|
|
10
|
+
organizationId: ctx.selectedOrganizationId ?? null,
|
|
11
|
+
userId: ctx.auth?.sub ?? null
|
|
12
|
+
},
|
|
13
|
+
ctx
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function createBulkNotificationRoute(schema, serviceMethod) {
|
|
17
|
+
return async function POST(req) {
|
|
18
|
+
const { service, scope } = await resolveNotificationContext(req);
|
|
19
|
+
const body = await req.json().catch(() => ({}));
|
|
20
|
+
const input = schema.parse(body);
|
|
21
|
+
const notifications = await service[serviceMethod](input, scope);
|
|
22
|
+
return Response.json({
|
|
23
|
+
ok: true,
|
|
24
|
+
count: notifications.length,
|
|
25
|
+
ids: notifications.map((n) => n.id)
|
|
26
|
+
}, { status: 201 });
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function createBulkNotificationOpenApi(schema, summary, description) {
|
|
30
|
+
return {
|
|
31
|
+
POST: {
|
|
32
|
+
summary,
|
|
33
|
+
description,
|
|
34
|
+
tags: ["Notifications"],
|
|
35
|
+
requestBody: {
|
|
36
|
+
required: true,
|
|
37
|
+
content: {
|
|
38
|
+
"application/json": {
|
|
39
|
+
schema
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
responses: {
|
|
44
|
+
201: {
|
|
45
|
+
description: "Notifications created",
|
|
46
|
+
content: {
|
|
47
|
+
"application/json": {
|
|
48
|
+
schema: z.object({
|
|
49
|
+
ok: z.boolean(),
|
|
50
|
+
count: z.number(),
|
|
51
|
+
ids: z.array(z.string().uuid())
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function createSingleNotificationActionRoute(serviceMethod) {
|
|
61
|
+
return async function PUT(req, { params }) {
|
|
62
|
+
const { id } = await params;
|
|
63
|
+
const { service, scope } = await resolveNotificationContext(req);
|
|
64
|
+
await service[serviceMethod](id, scope);
|
|
65
|
+
return Response.json({ ok: true });
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function createSingleNotificationActionOpenApi(summary, description) {
|
|
69
|
+
return {
|
|
70
|
+
PUT: {
|
|
71
|
+
summary,
|
|
72
|
+
tags: ["Notifications"],
|
|
73
|
+
parameters: [
|
|
74
|
+
{
|
|
75
|
+
name: "id",
|
|
76
|
+
in: "path",
|
|
77
|
+
required: true,
|
|
78
|
+
schema: { type: "string", format: "uuid" }
|
|
79
|
+
}
|
|
80
|
+
],
|
|
81
|
+
responses: {
|
|
82
|
+
200: {
|
|
83
|
+
description,
|
|
84
|
+
content: {
|
|
85
|
+
"application/json": {
|
|
86
|
+
schema: z.object({ ok: z.boolean() })
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export {
|
|
95
|
+
createBulkNotificationOpenApi,
|
|
96
|
+
createBulkNotificationRoute,
|
|
97
|
+
createSingleNotificationActionOpenApi,
|
|
98
|
+
createSingleNotificationActionRoute,
|
|
99
|
+
resolveNotificationContext
|
|
100
|
+
};
|
|
101
|
+
//# sourceMappingURL=routeHelpers.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/notifications/lib/routeHelpers.ts"],
|
|
4
|
+
"sourcesContent": ["import { z } from 'zod'\nimport { resolveRequestContext } from '@open-mercato/shared/lib/api/context'\nimport { resolveNotificationService, type NotificationService } from './notificationService'\n\n/**\n * Notification scope context for service calls\n */\nexport interface NotificationScope {\n tenantId: string\n organizationId: string | null\n userId: string | null\n}\n\n/**\n * Resolved notification context from a request\n */\nexport interface NotificationRequestContext {\n service: NotificationService\n scope: NotificationScope\n ctx: Awaited<ReturnType<typeof resolveRequestContext>>['ctx']\n}\n\n/**\n * Resolve notification service and scope from a request.\n * Centralizes the common pattern used across all notification API routes.\n */\nexport async function resolveNotificationContext(req: Request): Promise<NotificationRequestContext> {\n const { ctx } = await resolveRequestContext(req)\n return {\n service: resolveNotificationService(ctx.container),\n scope: {\n tenantId: ctx.auth?.tenantId ?? '',\n organizationId: ctx.selectedOrganizationId ?? null,\n userId: ctx.auth?.sub ?? null,\n },\n ctx,\n }\n}\n\n/**\n * Create a POST handler for bulk notification creation routes.\n * Used by batch, role, and feature notification endpoints.\n */\nexport function createBulkNotificationRoute<TSchema extends z.ZodTypeAny>(\n schema: TSchema,\n serviceMethod: 'createBatch' | 'createForRole' | 'createForFeature'\n) {\n return async function POST(req: Request) {\n const { service, scope } = await resolveNotificationContext(req)\n\n const body = await req.json().catch(() => ({}))\n const input = schema.parse(body)\n\n const notifications = await service[serviceMethod](input as never, scope)\n\n return Response.json({\n ok: true,\n count: notifications.length,\n ids: notifications.map((n) => n.id),\n }, { status: 201 })\n }\n}\n\n/**\n * Create OpenAPI spec for bulk notification creation routes.\n */\nexport function createBulkNotificationOpenApi<TSchema extends z.ZodTypeAny>(\n schema: TSchema,\n summary: string,\n description?: string\n) {\n return {\n POST: {\n summary,\n description,\n tags: ['Notifications'],\n requestBody: {\n required: true,\n content: {\n 'application/json': {\n schema,\n },\n },\n },\n responses: {\n 201: {\n description: 'Notifications created',\n content: {\n 'application/json': {\n schema: z.object({\n ok: z.boolean(),\n count: z.number(),\n ids: z.array(z.string().uuid()),\n }),\n },\n },\n },\n },\n },\n }\n}\n\n/**\n * Create a PUT handler for single notification action routes.\n * Used by read and dismiss endpoints.\n */\nexport function createSingleNotificationActionRoute(\n serviceMethod: 'markAsRead' | 'dismiss'\n) {\n return async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {\n const { id } = await params\n const { service, scope } = await resolveNotificationContext(req)\n\n await service[serviceMethod](id, scope)\n\n return Response.json({ ok: true })\n }\n}\n\n/**\n * Create OpenAPI spec for single notification action routes.\n */\nexport function createSingleNotificationActionOpenApi(\n summary: string,\n description: string\n) {\n return {\n PUT: {\n summary,\n tags: ['Notifications'],\n parameters: [\n {\n name: 'id',\n in: 'path',\n required: true,\n schema: { type: 'string', format: 'uuid' },\n },\n ],\n responses: {\n 200: {\n description,\n content: {\n 'application/json': {\n schema: z.object({ ok: z.boolean() }),\n },\n },\n },\n },\n },\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,SAAS;AAClB,SAAS,6BAA6B;AACtC,SAAS,kCAA4D;AAwBrE,eAAsB,2BAA2B,KAAmD;AAClG,QAAM,EAAE,IAAI,IAAI,MAAM,sBAAsB,GAAG;AAC/C,SAAO;AAAA,IACL,SAAS,2BAA2B,IAAI,SAAS;AAAA,IACjD,OAAO;AAAA,MACL,UAAU,IAAI,MAAM,YAAY;AAAA,MAChC,gBAAgB,IAAI,0BAA0B;AAAA,MAC9C,QAAQ,IAAI,MAAM,OAAO;AAAA,IAC3B;AAAA,IACA;AAAA,EACF;AACF;AAMO,SAAS,4BACd,QACA,eACA;AACA,SAAO,eAAe,KAAK,KAAc;AACvC,UAAM,EAAE,SAAS,MAAM,IAAI,MAAM,2BAA2B,GAAG;AAE/D,UAAM,OAAO,MAAM,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AAC9C,UAAM,QAAQ,OAAO,MAAM,IAAI;AAE/B,UAAM,gBAAgB,MAAM,QAAQ,aAAa,EAAE,OAAgB,KAAK;AAExE,WAAO,SAAS,KAAK;AAAA,MACnB,IAAI;AAAA,MACJ,OAAO,cAAc;AAAA,MACrB,KAAK,cAAc,IAAI,CAAC,MAAM,EAAE,EAAE;AAAA,IACpC,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACpB;AACF;AAKO,SAAS,8BACd,QACA,SACA,aACA;AACA,SAAO;AAAA,IACL,MAAM;AAAA,MACJ;AAAA,MACA;AAAA,MACA,MAAM,CAAC,eAAe;AAAA,MACtB,aAAa;AAAA,QACX,UAAU;AAAA,QACV,SAAS;AAAA,UACP,oBAAoB;AAAA,YAClB;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,MACA,WAAW;AAAA,QACT,KAAK;AAAA,UACH,aAAa;AAAA,UACb,SAAS;AAAA,YACP,oBAAoB;AAAA,cAClB,QAAQ,EAAE,OAAO;AAAA,gBACf,IAAI,EAAE,QAAQ;AAAA,gBACd,OAAO,EAAE,OAAO;AAAA,gBAChB,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC;AAAA,cAChC,CAAC;AAAA,YACH;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAMO,SAAS,oCACd,eACA;AACA,SAAO,eAAe,IAAI,KAAc,EAAE,OAAO,GAAwC;AACvF,UAAM,EAAE,GAAG,IAAI,MAAM;AACrB,UAAM,EAAE,SAAS,MAAM,IAAI,MAAM,2BAA2B,GAAG;AAE/D,UAAM,QAAQ,aAAa,EAAE,IAAI,KAAK;AAEtC,WAAO,SAAS,KAAK,EAAE,IAAI,KAAK,CAAC;AAAA,EACnC;AACF;AAKO,SAAS,sCACd,SACA,aACA;AACA,SAAO;AAAA,IACL,KAAK;AAAA,MACH;AAAA,MACA,MAAM,CAAC,eAAe;AAAA,MACtB,YAAY;AAAA,QACV;AAAA,UACE,MAAM;AAAA,UACN,IAAI;AAAA,UACJ,UAAU;AAAA,UACV,QAAQ,EAAE,MAAM,UAAU,QAAQ,OAAO;AAAA,QAC3C;AAAA,MACF;AAAA,MACA,WAAW;AAAA,QACT,KAAK;AAAA,UACH;AAAA,UACA,SAAS;AAAA,YACP,oBAAoB;AAAA,cAClB,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;AAAA,YACtC;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
function isSafeNotificationHref(href) {
|
|
2
|
+
return href.startsWith("/") && !href.startsWith("//");
|
|
3
|
+
}
|
|
4
|
+
function assertSafeNotificationHref(href) {
|
|
5
|
+
if (href == null) {
|
|
6
|
+
return void 0;
|
|
7
|
+
}
|
|
8
|
+
if (!isSafeNotificationHref(href)) {
|
|
9
|
+
throw new Error("Notification href must be a same-origin relative path starting with /");
|
|
10
|
+
}
|
|
11
|
+
return href;
|
|
12
|
+
}
|
|
13
|
+
function sanitizeNotificationActions(actions) {
|
|
14
|
+
if (!actions) {
|
|
15
|
+
return void 0;
|
|
16
|
+
}
|
|
17
|
+
return actions.map((action) => action.href ? { ...action, href: assertSafeNotificationHref(action.href) } : action);
|
|
18
|
+
}
|
|
19
|
+
export {
|
|
20
|
+
assertSafeNotificationHref,
|
|
21
|
+
isSafeNotificationHref,
|
|
22
|
+
sanitizeNotificationActions
|
|
23
|
+
};
|
|
24
|
+
//# sourceMappingURL=safeHref.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/notifications/lib/safeHref.ts"],
|
|
4
|
+
"sourcesContent": ["import type { NotificationAction } from '@open-mercato/shared/modules/notifications/types'\n\nexport function isSafeNotificationHref(href: string): boolean {\n return href.startsWith('/') && !href.startsWith('//')\n}\n\nexport function assertSafeNotificationHref(href: string | undefined | null): string | undefined {\n if (href == null) {\n return undefined\n }\n\n if (!isSafeNotificationHref(href)) {\n throw new Error('Notification href must be a same-origin relative path starting with /')\n }\n\n return href\n}\n\nexport function sanitizeNotificationActions(\n actions: NotificationAction[] | undefined\n): NotificationAction[] | undefined {\n if (!actions) {\n return undefined\n }\n\n return actions.map((action) => (\n action.href ? { ...action, href: assertSafeNotificationHref(action.href) } : action\n ))\n}\n"],
|
|
5
|
+
"mappings": "AAEO,SAAS,uBAAuB,MAAuB;AAC5D,SAAO,KAAK,WAAW,GAAG,KAAK,CAAC,KAAK,WAAW,IAAI;AACtD;AAEO,SAAS,2BAA2B,MAAqD;AAC9F,MAAI,QAAQ,MAAM;AAChB,WAAO;AAAA,EACT;AAEA,MAAI,CAAC,uBAAuB,IAAI,GAAG;AACjC,UAAM,IAAI,MAAM,uEAAuE;AAAA,EACzF;AAEA,SAAO;AACT;AAEO,SAAS,4BACd,SACkC;AAClC,MAAI,CAAC,SAAS;AACZ,WAAO;AAAA,EACT;AAEA,SAAO,QAAQ,IAAI,CAAC,WAClB,OAAO,OAAO,EAAE,GAAG,QAAQ,MAAM,2BAA2B,OAAO,IAAI,EAAE,IAAI,MAC9E;AACH;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Migration } from "@mikro-orm/migrations";
|
|
2
|
+
class Migration20260123000001 extends Migration {
|
|
3
|
+
async up() {
|
|
4
|
+
this.addSql(`
|
|
5
|
+
CREATE TABLE notifications (
|
|
6
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
7
|
+
|
|
8
|
+
recipient_user_id UUID NOT NULL,
|
|
9
|
+
|
|
10
|
+
type TEXT NOT NULL,
|
|
11
|
+
title TEXT NOT NULL,
|
|
12
|
+
body TEXT,
|
|
13
|
+
icon TEXT,
|
|
14
|
+
severity TEXT NOT NULL DEFAULT 'info',
|
|
15
|
+
|
|
16
|
+
status TEXT NOT NULL DEFAULT 'unread',
|
|
17
|
+
|
|
18
|
+
action_data JSONB,
|
|
19
|
+
action_result JSONB,
|
|
20
|
+
action_taken TEXT,
|
|
21
|
+
|
|
22
|
+
source_module TEXT,
|
|
23
|
+
source_entity_type TEXT,
|
|
24
|
+
source_entity_id UUID,
|
|
25
|
+
link_href TEXT,
|
|
26
|
+
|
|
27
|
+
group_key TEXT,
|
|
28
|
+
|
|
29
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
30
|
+
read_at TIMESTAMPTZ,
|
|
31
|
+
actioned_at TIMESTAMPTZ,
|
|
32
|
+
dismissed_at TIMESTAMPTZ,
|
|
33
|
+
expires_at TIMESTAMPTZ,
|
|
34
|
+
|
|
35
|
+
tenant_id UUID NOT NULL,
|
|
36
|
+
organization_id UUID
|
|
37
|
+
);
|
|
38
|
+
`);
|
|
39
|
+
this.addSql(`
|
|
40
|
+
CREATE INDEX notifications_recipient_status_idx
|
|
41
|
+
ON notifications(recipient_user_id, status, created_at DESC);
|
|
42
|
+
`);
|
|
43
|
+
this.addSql(`
|
|
44
|
+
CREATE INDEX notifications_source_idx
|
|
45
|
+
ON notifications(source_entity_type, source_entity_id)
|
|
46
|
+
WHERE source_entity_id IS NOT NULL;
|
|
47
|
+
`);
|
|
48
|
+
this.addSql(`
|
|
49
|
+
CREATE INDEX notifications_tenant_idx
|
|
50
|
+
ON notifications(tenant_id, organization_id);
|
|
51
|
+
`);
|
|
52
|
+
this.addSql(`
|
|
53
|
+
CREATE INDEX notifications_expires_idx
|
|
54
|
+
ON notifications(expires_at)
|
|
55
|
+
WHERE expires_at IS NOT NULL AND status NOT IN ('actioned', 'dismissed');
|
|
56
|
+
`);
|
|
57
|
+
this.addSql(`
|
|
58
|
+
CREATE INDEX notifications_group_idx
|
|
59
|
+
ON notifications(group_key, recipient_user_id)
|
|
60
|
+
WHERE group_key IS NOT NULL;
|
|
61
|
+
`);
|
|
62
|
+
}
|
|
63
|
+
async down() {
|
|
64
|
+
this.addSql("DROP TABLE IF EXISTS notifications CASCADE;");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export {
|
|
68
|
+
Migration20260123000001
|
|
69
|
+
};
|
|
70
|
+
//# sourceMappingURL=Migration20260123000001.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/notifications/migrations/Migration20260123000001.ts"],
|
|
4
|
+
"sourcesContent": ["import { Migration } from '@mikro-orm/migrations'\n\nexport class Migration20260123000001 extends Migration {\n async up(): Promise<void> {\n this.addSql(`\n CREATE TABLE notifications (\n id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n \n recipient_user_id UUID NOT NULL,\n \n type TEXT NOT NULL,\n title TEXT NOT NULL,\n body TEXT,\n icon TEXT,\n severity TEXT NOT NULL DEFAULT 'info',\n \n status TEXT NOT NULL DEFAULT 'unread',\n \n action_data JSONB,\n action_result JSONB,\n action_taken TEXT,\n \n source_module TEXT,\n source_entity_type TEXT,\n source_entity_id UUID,\n link_href TEXT,\n \n group_key TEXT,\n \n created_at TIMESTAMPTZ NOT NULL DEFAULT now(),\n read_at TIMESTAMPTZ,\n actioned_at TIMESTAMPTZ,\n dismissed_at TIMESTAMPTZ,\n expires_at TIMESTAMPTZ,\n \n tenant_id UUID NOT NULL,\n organization_id UUID\n );\n `)\n\n this.addSql(`\n CREATE INDEX notifications_recipient_status_idx \n ON notifications(recipient_user_id, status, created_at DESC);\n `)\n\n this.addSql(`\n CREATE INDEX notifications_source_idx \n ON notifications(source_entity_type, source_entity_id) \n WHERE source_entity_id IS NOT NULL;\n `)\n\n this.addSql(`\n CREATE INDEX notifications_tenant_idx \n ON notifications(tenant_id, organization_id);\n `)\n\n this.addSql(`\n CREATE INDEX notifications_expires_idx \n ON notifications(expires_at) \n WHERE expires_at IS NOT NULL AND status NOT IN ('actioned', 'dismissed');\n `)\n\n this.addSql(`\n CREATE INDEX notifications_group_idx \n ON notifications(group_key, recipient_user_id) \n WHERE group_key IS NOT NULL;\n `)\n }\n\n async down(): Promise<void> {\n this.addSql('DROP TABLE IF EXISTS notifications CASCADE;')\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,iBAAiB;AAEnB,MAAM,gCAAgC,UAAU;AAAA,EACrD,MAAM,KAAoB;AACxB,SAAK,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KAkCX;AAED,SAAK,OAAO;AAAA;AAAA;AAAA,KAGX;AAED,SAAK,OAAO;AAAA;AAAA;AAAA;AAAA,KAIX;AAED,SAAK,OAAO;AAAA;AAAA;AAAA,KAGX;AAED,SAAK,OAAO;AAAA;AAAA;AAAA;AAAA,KAIX;AAED,SAAK,OAAO;AAAA;AAAA;AAAA;AAAA,KAIX;AAAA,EACH;AAAA,EAEA,MAAM,OAAsB;AAC1B,SAAK,OAAO,6CAA6C;AAAA,EAC3D;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Migration } from "@mikro-orm/migrations";
|
|
2
|
+
class Migration20260126150000 extends Migration {
|
|
3
|
+
async up() {
|
|
4
|
+
this.addSql(`
|
|
5
|
+
alter table "notifications"
|
|
6
|
+
add column if not exists "title_key" text,
|
|
7
|
+
add column if not exists "body_key" text,
|
|
8
|
+
add column if not exists "title_variables" jsonb,
|
|
9
|
+
add column if not exists "body_variables" jsonb;
|
|
10
|
+
`);
|
|
11
|
+
this.addSql(`
|
|
12
|
+
comment on column "notifications"."title_key" is 'i18n key for notification title';
|
|
13
|
+
`);
|
|
14
|
+
this.addSql(`
|
|
15
|
+
comment on column "notifications"."body_key" is 'i18n key for notification body';
|
|
16
|
+
`);
|
|
17
|
+
this.addSql(`
|
|
18
|
+
comment on column "notifications"."title_variables" is 'Variables for i18n interpolation in title';
|
|
19
|
+
`);
|
|
20
|
+
this.addSql(`
|
|
21
|
+
comment on column "notifications"."body_variables" is 'Variables for i18n interpolation in body';
|
|
22
|
+
`);
|
|
23
|
+
}
|
|
24
|
+
async down() {
|
|
25
|
+
this.addSql(`
|
|
26
|
+
alter table "notifications"
|
|
27
|
+
drop column if exists "title_key",
|
|
28
|
+
drop column if exists "body_key",
|
|
29
|
+
drop column if exists "title_variables",
|
|
30
|
+
drop column if exists "body_variables";
|
|
31
|
+
`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export {
|
|
35
|
+
Migration20260126150000
|
|
36
|
+
};
|
|
37
|
+
//# sourceMappingURL=Migration20260126150000.js.map
|