vintasend-prisma 0.2.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PrismaNotificationBackendFactory = exports.PrismaNotificationBackend = exports.NotificationTypeEnum = exports.NotificationStatusEnum = void 0;
4
+ const attachment_1 = require("vintasend/dist/types/attachment");
4
5
  exports.NotificationStatusEnum = {
5
6
  PENDING_SEND: 'PENDING_SEND',
6
7
  SENT: 'SENT',
@@ -14,6 +15,14 @@ exports.NotificationTypeEnum = {
14
15
  SMS: 'SMS',
15
16
  IN_APP: 'IN_APP',
16
17
  };
18
+ // Centralized attachment include shape for DRY
19
+ const notificationWithAttachmentsInclude = {
20
+ attachments: {
21
+ include: {
22
+ attachmentFile: true,
23
+ },
24
+ },
25
+ };
17
26
  function convertJsonValueToRecord(jsonValue) {
18
27
  if (typeof jsonValue === 'object' && !Array.isArray(jsonValue)) {
19
28
  return jsonValue;
@@ -21,13 +30,35 @@ function convertJsonValueToRecord(jsonValue) {
21
30
  throw new Error('Invalid JSON value. It should be an object.');
22
31
  }
23
32
  class PrismaNotificationBackend {
24
- constructor(prismaClient) {
33
+ constructor(prismaClient, attachmentManager) {
25
34
  this.prismaClient = prismaClient;
35
+ this.attachmentManager = attachmentManager;
26
36
  }
27
- serializeNotification(notification) {
28
- return {
37
+ /**
38
+ * Inject attachment manager (called by VintaSend when both service and backend exist)
39
+ */
40
+ injectAttachmentManager(manager) {
41
+ this.attachmentManager = manager;
42
+ }
43
+ /**
44
+ * Build a where clause for status-based updates
45
+ */
46
+ buildStatusWhere(id, opts = {}) {
47
+ const where = {
48
+ id: id,
49
+ };
50
+ if (opts.checkStatus) {
51
+ where.status = opts.checkStatus;
52
+ }
53
+ return where;
54
+ }
55
+ /**
56
+ * Serialize a Prisma notification model to either DatabaseNotification or DatabaseOneOffNotification
57
+ * based on whether it has a userId or not (internal implementation)
58
+ */
59
+ serializeAnyNotification(notification) {
60
+ const baseData = {
29
61
  id: notification.id,
30
- userId: notification.userId,
31
62
  notificationType: notification.notificationType,
32
63
  title: notification.title,
33
64
  bodyTemplate: notification.bodyTemplate,
@@ -47,15 +78,65 @@ class PrismaNotificationBackend {
47
78
  readAt: notification.readAt,
48
79
  createdAt: notification.createdAt,
49
80
  updatedAt: notification.updatedAt,
81
+ // Serialize attachments if present and attachmentManager is available
82
+ attachments: notification.attachments && this.attachmentManager
83
+ ? notification.attachments.map((att) => this.serializeStoredAttachment(att))
84
+ : undefined,
50
85
  };
51
- }
52
- deserializeNotification(notification) {
86
+ // Check if this is a one-off notification (has emailOrPhone but no userId)
87
+ // Use explicit null checks to avoid misclassification with empty strings or other falsy values
88
+ if (notification.userId == null && notification.emailOrPhone != null) {
89
+ return {
90
+ ...baseData,
91
+ emailOrPhone: notification.emailOrPhone,
92
+ firstName: notification.firstName || '',
93
+ lastName: notification.lastName || '',
94
+ };
95
+ }
96
+ // Regular notification with userId
97
+ if (notification.userId == null) {
98
+ throw new Error('Invalid notification: missing both userId and emailOrPhone');
99
+ }
53
100
  return {
54
- user: {
55
- connect: {
56
- id: notification.userId,
57
- },
58
- },
101
+ ...baseData,
102
+ userId: notification.userId,
103
+ };
104
+ }
105
+ /**
106
+ * Serialize a Prisma notification model to DatabaseNotification
107
+ */
108
+ serializeRegularNotification(notification) {
109
+ return this.serializeAnyNotification(notification);
110
+ }
111
+ /**
112
+ * Serialize a Prisma notification model to DatabaseOneOffNotification
113
+ */
114
+ serializeOneOffNotification(notification) {
115
+ return this.serializeAnyNotification(notification);
116
+ }
117
+ /**
118
+ * Public accessor for serialization - primarily for testing
119
+ * @internal
120
+ */
121
+ serializeNotification(notification) {
122
+ return this.serializeAnyNotification(notification);
123
+ }
124
+ /**
125
+ * Core internal builder for creating notification data
126
+ * Validates that notification has either userId or emailOrPhone (but not neither)
127
+ */
128
+ buildCreateData(notification) {
129
+ var _a, _b;
130
+ const hasUserId = 'userId' in notification && notification.userId != null;
131
+ const hasEmailOrPhone = 'emailOrPhone' in notification && notification.emailOrPhone != null;
132
+ // Validate: must have either userId or emailOrPhone
133
+ if (!hasUserId && !hasEmailOrPhone) {
134
+ throw new Error('Invalid notification: missing both userId and emailOrPhone');
135
+ }
136
+ // Determine if this is a one-off notification
137
+ // When both are provided, userId takes precedence (regular notification)
138
+ const isOneOff = !hasUserId && hasEmailOrPhone;
139
+ const base = {
59
140
  notificationType: notification.notificationType,
60
141
  title: notification.title,
61
142
  bodyTemplate: notification.bodyTemplate,
@@ -64,7 +145,245 @@ class PrismaNotificationBackend {
64
145
  sendAfter: notification.sendAfter,
65
146
  subjectTemplate: notification.subjectTemplate,
66
147
  extraParams: notification.extraParams,
148
+ // Only include one-off fields if this is actually a one-off notification
149
+ ...(isOneOff && {
150
+ emailOrPhone: 'emailOrPhone' in notification ? notification.emailOrPhone : null,
151
+ firstName: 'firstName' in notification ? (_a = notification.firstName) !== null && _a !== void 0 ? _a : null : null,
152
+ lastName: 'lastName' in notification ? (_b = notification.lastName) !== null && _b !== void 0 ? _b : null : null,
153
+ }),
67
154
  };
155
+ if (isOneOff) {
156
+ return {
157
+ ...base,
158
+ userId: null,
159
+ status: exports.NotificationStatusEnum.PENDING_SEND,
160
+ user: undefined,
161
+ };
162
+ }
163
+ // At this point we know hasUserId is true, so userId exists and is not null
164
+ const userId = ('userId' in notification ? notification.userId : null);
165
+ return {
166
+ ...base,
167
+ userId,
168
+ user: {
169
+ connect: { id: userId },
170
+ },
171
+ };
172
+ }
173
+ /**
174
+ * Deserialize a regular notification input for creation
175
+ */
176
+ deserializeRegularNotification(notification) {
177
+ return this.buildCreateData(notification);
178
+ }
179
+ /**
180
+ * Build one-off notification data for creation
181
+ */
182
+ buildOneOffNotificationData(notification) {
183
+ return this.buildCreateData(notification);
184
+ }
185
+ /**
186
+ * Core internal builder for update data (supports both regular and one-off)
187
+ */
188
+ buildUpdateData(notification) {
189
+ const data = {};
190
+ // Determine if this is transitioning between regular and one-off notification types
191
+ const hasUserId = 'userId' in notification && notification.userId !== undefined;
192
+ const hasOneOffFields = 'emailOrPhone' in notification && notification.emailOrPhone !== undefined;
193
+ // Handle user / one-off fields with mutual exclusion
194
+ if (hasUserId) {
195
+ // Converting to regular notification: set user and clear one-off fields
196
+ data.user = { connect: { id: notification.userId } };
197
+ // Clear one-off specific fields when transitioning to regular notification
198
+ data.emailOrPhone = null;
199
+ data.firstName = null;
200
+ data.lastName = null;
201
+ }
202
+ else if (hasOneOffFields) {
203
+ // Converting to one-off notification: set one-off fields
204
+ // Note: We cannot explicitly clear the user relationship via update,
205
+ // but setting emailOrPhone indicates this is now a one-off notification
206
+ data.emailOrPhone = notification.emailOrPhone;
207
+ if ('firstName' in notification && notification.firstName !== undefined) {
208
+ data.firstName = notification.firstName;
209
+ }
210
+ if ('lastName' in notification && notification.lastName !== undefined) {
211
+ data.lastName = notification.lastName;
212
+ }
213
+ }
214
+ else {
215
+ // No type transition, just update individual fields if provided
216
+ if ('emailOrPhone' in notification && notification.emailOrPhone !== undefined) {
217
+ data.emailOrPhone = notification.emailOrPhone;
218
+ }
219
+ if ('firstName' in notification && notification.firstName !== undefined) {
220
+ data.firstName = notification.firstName;
221
+ }
222
+ if ('lastName' in notification && notification.lastName !== undefined) {
223
+ data.lastName = notification.lastName;
224
+ }
225
+ }
226
+ // Handle common fields
227
+ if (notification.notificationType !== undefined) {
228
+ data.notificationType = notification.notificationType;
229
+ }
230
+ if (notification.title !== undefined) {
231
+ data.title = notification.title;
232
+ }
233
+ if (notification.bodyTemplate !== undefined) {
234
+ data.bodyTemplate = notification.bodyTemplate;
235
+ }
236
+ if (notification.contextName !== undefined) {
237
+ data.contextName = notification.contextName;
238
+ }
239
+ if (notification.contextParameters !== undefined) {
240
+ data.contextParameters = notification.contextParameters;
241
+ }
242
+ if (notification.sendAfter !== undefined) {
243
+ data.sendAfter = notification.sendAfter;
244
+ }
245
+ if (notification.subjectTemplate !== undefined) {
246
+ data.subjectTemplate = notification.subjectTemplate;
247
+ }
248
+ if (notification.status !== undefined) {
249
+ data.status = notification.status;
250
+ }
251
+ if (notification.contextUsed !== undefined) {
252
+ data.contextUsed = notification.contextUsed;
253
+ }
254
+ if (notification.extraParams !== undefined) {
255
+ data.extraParams = notification.extraParams;
256
+ }
257
+ if (notification.adapterUsed !== undefined) {
258
+ data.adapterUsed = notification.adapterUsed;
259
+ }
260
+ if (notification.sentAt !== undefined) {
261
+ data.sentAt = notification.sentAt;
262
+ }
263
+ if (notification.readAt !== undefined) {
264
+ data.readAt = notification.readAt;
265
+ }
266
+ return data;
267
+ }
268
+ /**
269
+ * Get or create file record for attachment upload with deduplication (transaction-aware)
270
+ * @private
271
+ */
272
+ async getOrCreateFileRecordForUploadInTransaction(tx, att) {
273
+ const manager = this.getAttachmentManager();
274
+ const buffer = await manager.fileToBuffer(att.file);
275
+ const checksum = manager.calculateChecksum(buffer);
276
+ let fileRecord = await this.findAttachmentFileByChecksumInTransaction(tx, checksum);
277
+ if (!fileRecord) {
278
+ fileRecord = await manager.uploadFile(att.file, att.filename, att.contentType);
279
+ await tx.attachmentFile.create({
280
+ data: {
281
+ id: fileRecord.id,
282
+ filename: fileRecord.filename,
283
+ contentType: fileRecord.contentType,
284
+ size: fileRecord.size,
285
+ checksum: fileRecord.checksum,
286
+ storageMetadata: fileRecord.storageMetadata,
287
+ },
288
+ });
289
+ }
290
+ return fileRecord;
291
+ }
292
+ /**
293
+ * Get or create file record for attachment upload with deduplication (non-transactional)
294
+ * @private
295
+ */
296
+ async getOrCreateFileRecordForUpload(att) {
297
+ return this.getOrCreateFileRecordForUploadInTransaction(this.prismaClient, att);
298
+ }
299
+ /**
300
+ * Create notification attachment link (transaction-aware)
301
+ * @private
302
+ */
303
+ async createNotificationAttachmentLinkInTransaction(tx, notificationId, fileId, description) {
304
+ await tx.notificationAttachment.create({
305
+ data: {
306
+ notificationId,
307
+ fileId,
308
+ description: description !== null && description !== void 0 ? description : null,
309
+ },
310
+ });
311
+ }
312
+ /**
313
+ * Create notification attachment link (non-transactional)
314
+ * @private
315
+ */
316
+ async createNotificationAttachmentLink(notificationId, fileId, description) {
317
+ return this.createNotificationAttachmentLinkInTransaction(this.prismaClient, notificationId, fileId, description);
318
+ }
319
+ /**
320
+ * Get attachment file by ID (transaction-aware)
321
+ * @private
322
+ */
323
+ async getAttachmentFileInTransaction(tx, fileId) {
324
+ const file = await tx.attachmentFile.findUnique({
325
+ where: { id: fileId },
326
+ });
327
+ if (!file)
328
+ return null;
329
+ return this.serializeAttachmentFileRecord(file);
330
+ }
331
+ /**
332
+ * Find attachment file by checksum (transaction-aware)
333
+ * @private
334
+ */
335
+ async findAttachmentFileByChecksumInTransaction(tx, checksum) {
336
+ const file = await tx.attachmentFile.findUnique({
337
+ where: { checksum },
338
+ });
339
+ if (!file)
340
+ return null;
341
+ return this.serializeAttachmentFileRecord(file);
342
+ }
343
+ /**
344
+ * Core helper for creating notifications with attachments (both regular and one-off)
345
+ * Uses transactions to ensure atomicity - if attachment processing fails, the notification won't be created
346
+ * @private
347
+ */
348
+ async createNotificationWithAttachments(input, buildData, serialize) {
349
+ const { attachments, ...notificationData } = input;
350
+ // If no attachments, skip transaction overhead
351
+ if (!attachments || attachments.length === 0) {
352
+ const created = await this.prismaClient.notification.create({
353
+ data: buildData(notificationData),
354
+ include: notificationWithAttachmentsInclude,
355
+ });
356
+ return serialize(created);
357
+ }
358
+ // Use transaction to ensure atomicity of notification + attachments
359
+ return await this.prismaClient.$transaction(async (tx) => {
360
+ const created = await tx.notification.create({
361
+ data: buildData(notificationData),
362
+ include: notificationWithAttachmentsInclude,
363
+ });
364
+ await this.processAndStoreAttachmentsInTransaction(tx, created.id, attachments);
365
+ const withAttachments = await tx.notification.findUnique({
366
+ where: { id: created.id },
367
+ include: notificationWithAttachmentsInclude,
368
+ });
369
+ if (!withAttachments) {
370
+ throw new Error('Failed to retrieve notification after creating attachments');
371
+ }
372
+ return serialize(withAttachments);
373
+ });
374
+ }
375
+ /**
376
+ * Get attachment manager with null check
377
+ * @private
378
+ */
379
+ getAttachmentManager() {
380
+ if (!this.attachmentManager) {
381
+ throw new Error('AttachmentManager is required but not provided');
382
+ }
383
+ return this.attachmentManager;
384
+ }
385
+ deserializeNotification(notification) {
386
+ return this.buildCreateData(notification);
68
387
  }
69
388
  deserializeNotificationForUpdate(notification) {
70
389
  return {
@@ -88,46 +407,40 @@ class PrismaNotificationBackend {
88
407
  }
89
408
  async getAllPendingNotifications() {
90
409
  const notifications = await this.prismaClient.notification.findMany({
91
- where: {
92
- status: exports.NotificationStatusEnum.PENDING_SEND,
93
- },
410
+ where: { status: exports.NotificationStatusEnum.PENDING_SEND },
94
411
  });
95
- return notifications.map(this.serializeNotification);
412
+ return notifications.map((n) => this.serializeAnyNotification(n));
96
413
  }
97
- async getPendingNotifications() {
414
+ async getPendingNotifications(page = 0, pageSize = 100) {
98
415
  const notifications = await this.prismaClient.notification.findMany({
99
416
  where: {
100
417
  status: exports.NotificationStatusEnum.PENDING_SEND,
101
418
  sendAfter: null,
102
419
  },
420
+ skip: page * pageSize,
421
+ take: pageSize,
103
422
  });
104
- return notifications.map(this.serializeNotification);
423
+ return notifications.map((n) => this.serializeAnyNotification(n));
105
424
  }
106
425
  async getAllFutureNotifications() {
107
426
  const notifications = await this.prismaClient.notification.findMany({
108
427
  where: {
109
- status: {
110
- not: exports.NotificationStatusEnum.PENDING_SEND,
111
- },
112
- sendAfter: {
113
- lte: new Date(),
114
- },
428
+ status: { not: exports.NotificationStatusEnum.PENDING_SEND },
429
+ sendAfter: { lte: new Date() },
115
430
  },
116
431
  });
117
- return notifications.map(this.serializeNotification);
432
+ return notifications.map((n) => this.serializeAnyNotification(n));
118
433
  }
119
- async getFutureNotifications() {
434
+ async getFutureNotifications(page = 0, pageSize = 100) {
120
435
  const notifications = await this.prismaClient.notification.findMany({
121
436
  where: {
122
- status: {
123
- not: exports.NotificationStatusEnum.PENDING_SEND,
124
- },
125
- sendAfter: {
126
- lte: new Date(),
127
- },
437
+ status: { not: exports.NotificationStatusEnum.PENDING_SEND },
438
+ sendAfter: { lte: new Date() },
128
439
  },
440
+ skip: page * pageSize,
441
+ take: pageSize,
129
442
  });
130
- return notifications.map(this.serializeNotification);
443
+ return notifications.map((n) => this.serializeAnyNotification(n));
131
444
  }
132
445
  async getAllFutureNotificationsFromUser(userId) {
133
446
  const notifications = await this.prismaClient.notification.findMany({
@@ -141,7 +454,7 @@ class PrismaNotificationBackend {
141
454
  },
142
455
  },
143
456
  });
144
- return notifications.map(this.serializeNotification);
457
+ return notifications.map((n) => this.serializeRegularNotification(n));
145
458
  }
146
459
  async getFutureNotificationsFromUser(userId, page, pageSize) {
147
460
  const notifications = await this.prismaClient.notification.findMany({
@@ -157,67 +470,118 @@ class PrismaNotificationBackend {
157
470
  skip: page * pageSize,
158
471
  take: pageSize,
159
472
  });
160
- return notifications.map(this.serializeNotification);
473
+ return notifications.map((n) => this.serializeRegularNotification(n));
161
474
  }
162
475
  async getAllNotifications() {
163
476
  const notifications = await this.prismaClient.notification.findMany({});
164
- return notifications.map(this.serializeNotification);
477
+ return notifications.map((n) => this.serializeAnyNotification(n));
165
478
  }
166
479
  async getNotifications(page, pageSize) {
167
480
  const notifications = await this.prismaClient.notification.findMany({
168
481
  skip: page * pageSize,
169
482
  take: pageSize,
170
483
  });
171
- return notifications.map(this.serializeNotification);
484
+ return notifications.map((n) => this.serializeAnyNotification(n));
172
485
  }
173
486
  async persistNotification(notification) {
174
- return this.serializeNotification(await this.prismaClient.notification.create({
175
- data: this.deserializeNotification(notification),
176
- }));
487
+ return this.createNotificationWithAttachments(notification, (n) => this.deserializeRegularNotification(n), (db) => this.serializeRegularNotification(db));
177
488
  }
178
489
  async persistNotificationUpdate(notificationId, notification) {
179
- return this.serializeNotification(await this.prismaClient.notification.update({
490
+ const updated = await this.prismaClient.notification.update({
180
491
  where: {
181
492
  id: notificationId,
182
493
  },
183
- data: this.deserializeNotificationForUpdate(notification),
184
- }));
494
+ data: this.buildUpdateData(notification),
495
+ });
496
+ return this.serializeRegularNotification(updated);
185
497
  }
186
- async markAsSent(notificationId, checkIsPending = true) {
187
- return this.serializeNotification(await this.prismaClient.notification.update({
498
+ /* One-off notification persistence and query methods */
499
+ async persistOneOffNotification(notification) {
500
+ return this.createNotificationWithAttachments(notification, (n) => this.buildOneOffNotificationData(n), (db) => this.serializeOneOffNotification(db));
501
+ }
502
+ async persistOneOffNotificationUpdate(notificationId, notification) {
503
+ const updated = await this.prismaClient.notification.update({
504
+ where: { id: notificationId },
505
+ data: this.buildUpdateData(notification),
506
+ });
507
+ return this.serializeOneOffNotification(updated);
508
+ }
509
+ async getOneOffNotification(notificationId, _forUpdate) {
510
+ const notification = await this.prismaClient.notification.findUnique({
188
511
  where: {
189
512
  id: notificationId,
190
- ...(checkIsPending ? { status: exports.NotificationStatusEnum.PENDING_SEND } : {}),
191
513
  },
514
+ });
515
+ if (!notification || notification.emailOrPhone == null || notification.userId !== null) {
516
+ return null;
517
+ }
518
+ return this.serializeOneOffNotification(notification);
519
+ }
520
+ async getAllOneOffNotifications() {
521
+ const notifications = await this.prismaClient.notification.findMany({
522
+ where: {
523
+ userId: null,
524
+ emailOrPhone: { not: null },
525
+ },
526
+ });
527
+ return notifications.map((n) => this.serializeOneOffNotification(n));
528
+ }
529
+ async getOneOffNotifications(page, pageSize) {
530
+ const notifications = await this.prismaClient.notification.findMany({
531
+ where: {
532
+ userId: null,
533
+ emailOrPhone: { not: null },
534
+ },
535
+ skip: page * pageSize,
536
+ take: pageSize,
537
+ });
538
+ return notifications.map((n) => this.serializeOneOffNotification(n));
539
+ }
540
+ async markAsSent(notificationId, checkIsPending = true) {
541
+ const updated = await this.prismaClient.notification.update({
542
+ where: this.buildStatusWhere(notificationId, {
543
+ checkStatus: checkIsPending ? exports.NotificationStatusEnum.PENDING_SEND : undefined,
544
+ }),
192
545
  data: {
193
546
  status: exports.NotificationStatusEnum.SENT,
194
547
  sentAt: new Date(),
195
548
  },
196
- }));
549
+ });
550
+ return this.serializeAnyNotification(updated);
197
551
  }
198
552
  async markAsFailed(notificationId, checkIsPending = true) {
199
- return this.serializeNotification(await this.prismaClient.notification.update({
200
- where: {
201
- id: notificationId,
202
- ...(checkIsPending ? { status: exports.NotificationStatusEnum.PENDING_SEND } : {}),
203
- },
553
+ const updated = await this.prismaClient.notification.update({
554
+ where: this.buildStatusWhere(notificationId, {
555
+ checkStatus: checkIsPending ? exports.NotificationStatusEnum.PENDING_SEND : undefined,
556
+ }),
204
557
  data: {
205
558
  status: exports.NotificationStatusEnum.FAILED,
206
559
  sentAt: new Date(),
207
560
  },
208
- }));
561
+ });
562
+ return this.serializeAnyNotification(updated);
209
563
  }
210
564
  async markAsRead(notificationId, checkIsSent = true) {
211
- return this.serializeNotification(await this.prismaClient.notification.update({
212
- where: {
213
- id: notificationId,
214
- ...(checkIsSent ? { status: exports.NotificationStatusEnum.SENT } : {}),
215
- },
565
+ // First fetch to validate it's a regular notification
566
+ const notification = await this.prismaClient.notification.findUnique({
567
+ where: { id: notificationId },
568
+ });
569
+ if (!notification) {
570
+ throw new Error('Notification not found');
571
+ }
572
+ if (notification.userId == null) {
573
+ throw new Error('Cannot mark one-off notification as read');
574
+ }
575
+ const updated = await this.prismaClient.notification.update({
576
+ where: this.buildStatusWhere(notificationId, {
577
+ checkStatus: checkIsSent ? exports.NotificationStatusEnum.SENT : undefined,
578
+ }),
216
579
  data: {
217
- status: 'READ',
580
+ status: exports.NotificationStatusEnum.READ,
218
581
  readAt: new Date(),
219
582
  },
220
- }));
583
+ });
584
+ return this.serializeRegularNotification(updated);
221
585
  }
222
586
  async cancelNotification(notificationId) {
223
587
  await this.prismaClient.notification.update({
@@ -231,14 +595,12 @@ class PrismaNotificationBackend {
231
595
  }
232
596
  async getNotification(notificationId, _forUpdate) {
233
597
  const notification = await this.prismaClient.notification.findUnique({
234
- where: {
235
- id: notificationId,
236
- },
598
+ where: { id: notificationId },
599
+ include: notificationWithAttachmentsInclude,
237
600
  });
238
- if (!notification) {
601
+ if (!notification)
239
602
  return null;
240
- }
241
- return this.serializeNotification(notification);
603
+ return this.serializeAnyNotification(notification);
242
604
  }
243
605
  async filterAllInAppUnreadNotifications(userId) {
244
606
  const notifications = await this.prismaClient.notification.findMany({
@@ -248,7 +610,7 @@ class PrismaNotificationBackend {
248
610
  readAt: null,
249
611
  },
250
612
  });
251
- return notifications.map(this.serializeNotification);
613
+ return notifications.map((n) => this.serializeRegularNotification(n));
252
614
  }
253
615
  async filterInAppUnreadNotifications(userId, page, pageSize) {
254
616
  const notifications = await this.prismaClient.notification.findMany({
@@ -260,7 +622,7 @@ class PrismaNotificationBackend {
260
622
  skip: page * pageSize,
261
623
  take: pageSize,
262
624
  });
263
- return notifications.map(this.serializeNotification);
625
+ return notifications.map((n) => this.serializeRegularNotification(n));
264
626
  }
265
627
  async getUserEmailFromNotification(notificationId) {
266
628
  var _a;
@@ -276,24 +638,159 @@ class PrismaNotificationBackend {
276
638
  }
277
639
  async storeContextUsed(notificationId, context) {
278
640
  await this.prismaClient.notification.update({
279
- where: {
280
- id: notificationId,
281
- },
282
- data: {
283
- contextUsed: context,
284
- },
641
+ where: { id: notificationId },
642
+ data: { contextUsed: context },
285
643
  });
286
644
  }
287
645
  async bulkPersistNotifications(notifications) {
288
- return this.prismaClient.notification.createMany({
646
+ const created = await this.prismaClient.notification.createManyAndReturn({
289
647
  data: notifications.map((notification) => this.deserializeNotification(notification)),
290
648
  });
649
+ return created.map((n) => n.id);
650
+ }
651
+ /* Attachment management methods */
652
+ async getAttachmentFile(fileId) {
653
+ const file = await this.prismaClient.attachmentFile.findUnique({
654
+ where: { id: fileId },
655
+ });
656
+ if (!file)
657
+ return null;
658
+ return this.serializeAttachmentFileRecord(file);
659
+ }
660
+ /**
661
+ * Find an attachment file by checksum for deduplication.
662
+ * This allows the backend to check if a file already exists before uploading.
663
+ */
664
+ async findAttachmentFileByChecksum(checksum) {
665
+ const file = await this.prismaClient.attachmentFile.findUnique({
666
+ where: { checksum },
667
+ });
668
+ if (!file)
669
+ return null;
670
+ return this.serializeAttachmentFileRecord(file);
671
+ }
672
+ async deleteAttachmentFile(fileId) {
673
+ const file = await this.prismaClient.attachmentFile.findUnique({
674
+ where: { id: fileId },
675
+ });
676
+ // If there's no DB record, there's nothing to delete
677
+ if (!file)
678
+ return;
679
+ // First delete the underlying stored file so DB and storage stay in sync
680
+ const manager = this.getAttachmentManager();
681
+ await manager.deleteFile(fileId);
682
+ // Only after successful storage deletion, remove the DB record
683
+ await this.prismaClient.attachmentFile.delete({
684
+ where: { id: fileId },
685
+ });
686
+ }
687
+ async getOrphanedAttachmentFiles() {
688
+ const orphanedFiles = await this.prismaClient.attachmentFile.findMany({
689
+ where: {
690
+ notificationAttachments: { none: {} },
691
+ },
692
+ });
693
+ return orphanedFiles.map((file) => this.serializeAttachmentFileRecord(file));
694
+ }
695
+ async getAttachments(notificationId) {
696
+ const attachments = await this.prismaClient.notificationAttachment.findMany({
697
+ where: { notificationId },
698
+ include: { attachmentFile: true },
699
+ });
700
+ this.getAttachmentManager(); // Validate attachment manager exists
701
+ return attachments.map((att) => this.serializeStoredAttachment(att));
702
+ }
703
+ async deleteNotificationAttachment(notificationId, attachmentId) {
704
+ const result = await this.prismaClient.notificationAttachment.deleteMany({
705
+ where: {
706
+ id: attachmentId,
707
+ notificationId,
708
+ },
709
+ });
710
+ if (result.count === 0) {
711
+ throw new Error(`Attachment ${attachmentId} not found for notification ${notificationId}`);
712
+ }
713
+ }
714
+ /**
715
+ * Process and store attachments for a notification within a transaction.
716
+ * Handles both new file uploads and references to existing files.
717
+ * Uses attachmentManager for checksum calculation and storage operations.
718
+ * @private
719
+ */
720
+ async processAndStoreAttachmentsInTransaction(tx, notificationId, attachments) {
721
+ this.getAttachmentManager(); // Validate attachment manager exists
722
+ // Process each attachment
723
+ for (const att of attachments) {
724
+ if ((0, attachment_1.isAttachmentReference)(att)) {
725
+ // Reference existing file - just create the notification link
726
+ const fileRecord = await this.getAttachmentFileInTransaction(tx, att.fileId);
727
+ if (!fileRecord) {
728
+ throw new Error(`Referenced file ${att.fileId} not found`);
729
+ }
730
+ await this.createNotificationAttachmentLinkInTransaction(tx, notificationId, att.fileId, att.description);
731
+ }
732
+ else {
733
+ // Upload new file with deduplication
734
+ const fileRecord = await this.getOrCreateFileRecordForUploadInTransaction(tx, att);
735
+ await this.createNotificationAttachmentLinkInTransaction(tx, notificationId, fileRecord.id, att.description);
736
+ }
737
+ }
738
+ }
739
+ /**
740
+ * Process and store attachments for a notification (non-transactional version).
741
+ * Handles both new file uploads and references to existing files.
742
+ * Uses attachmentManager for checksum calculation and storage operations.
743
+ * @private
744
+ */
745
+ async processAndStoreAttachments(notificationId, attachments) {
746
+ return this.processAndStoreAttachmentsInTransaction(this.prismaClient, notificationId, attachments);
747
+ }
748
+ /**
749
+ * Serialize a Prisma attachment file model to AttachmentFileRecord
750
+ * @private
751
+ */
752
+ serializeAttachmentFileRecord(file) {
753
+ return {
754
+ id: file.id,
755
+ filename: file.filename,
756
+ contentType: file.contentType,
757
+ size: file.size,
758
+ checksum: file.checksum,
759
+ storageMetadata: file.storageMetadata,
760
+ createdAt: file.createdAt,
761
+ updatedAt: file.updatedAt,
762
+ };
763
+ }
764
+ /**
765
+ * Serialize a Prisma notification attachment model to StoredAttachment
766
+ * @private
767
+ */
768
+ serializeStoredAttachment(attachment) {
769
+ var _a;
770
+ const manager = this.getAttachmentManager();
771
+ if (!attachment.attachmentFile) {
772
+ throw new Error('AttachmentFile is required to reconstruct stored attachment');
773
+ }
774
+ const fileRecord = this.serializeAttachmentFileRecord(attachment.attachmentFile);
775
+ const attachmentFile = manager.reconstructAttachmentFile(fileRecord.storageMetadata);
776
+ return {
777
+ id: attachment.id,
778
+ fileId: attachment.fileId,
779
+ filename: fileRecord.filename,
780
+ contentType: fileRecord.contentType,
781
+ size: fileRecord.size,
782
+ checksum: fileRecord.checksum,
783
+ createdAt: attachment.createdAt,
784
+ file: attachmentFile,
785
+ description: (_a = attachment.description) !== null && _a !== void 0 ? _a : undefined,
786
+ storageMetadata: fileRecord.storageMetadata,
787
+ };
291
788
  }
292
789
  }
293
790
  exports.PrismaNotificationBackend = PrismaNotificationBackend;
294
791
  class PrismaNotificationBackendFactory {
295
- create(prismaClient) {
296
- return new PrismaNotificationBackend(prismaClient);
792
+ create(prismaClient, attachmentManager) {
793
+ return new PrismaNotificationBackend(prismaClient, attachmentManager);
297
794
  }
298
795
  }
299
796
  exports.PrismaNotificationBackendFactory = PrismaNotificationBackendFactory;