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.
- package/dist/implementations/vintasend-prisma/src/index.d.ts +2 -0
- package/dist/implementations/vintasend-prisma/src/index.js +7 -0
- package/dist/implementations/vintasend-prisma/src/prisma-notification-backend.d.ts +318 -0
- package/dist/implementations/vintasend-prisma/src/prisma-notification-backend.js +406 -0
- package/dist/prisma-notification-backend.d.ts +276 -30
- package/dist/prisma-notification-backend.js +573 -76
- package/dist/services/notification-backends/base-notification-backend.d.ts +89 -0
- package/dist/services/notification-backends/base-notification-backend.js +1 -0
- package/dist/types/identifier.d.ts +2 -0
- package/dist/types/identifier.js +1 -0
- package/dist/types/json-values.d.ts +19 -0
- package/dist/types/json-values.js +1 -0
- package/dist/types/notification-context-generators.d.ts +6 -0
- package/dist/types/notification-context-generators.js +1 -0
- package/dist/types/notification-status.d.ts +1 -0
- package/dist/types/notification-status.js +1 -0
- package/dist/types/notification-type-config.d.ts +7 -0
- package/dist/types/notification-type-config.js +1 -0
- package/dist/types/notification-type.d.ts +1 -0
- package/dist/types/notification-type.js +1 -0
- package/dist/types/notification.d.ts +97 -0
- package/dist/types/notification.js +1 -0
- package/dist/types/one-off-notification.d.ts +91 -0
- package/dist/types/one-off-notification.js +1 -0
- package/dist/types/uuid.d.ts +1 -0
- package/dist/types/uuid.js +1 -0
- package/package.json +27 -29
|
@@ -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
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
484
|
+
return notifications.map((n) => this.serializeAnyNotification(n));
|
|
172
485
|
}
|
|
173
486
|
async persistNotification(notification) {
|
|
174
|
-
return this.
|
|
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
|
-
|
|
490
|
+
const updated = await this.prismaClient.notification.update({
|
|
180
491
|
where: {
|
|
181
492
|
id: notificationId,
|
|
182
493
|
},
|
|
183
|
-
data: this.
|
|
184
|
-
})
|
|
494
|
+
data: this.buildUpdateData(notification),
|
|
495
|
+
});
|
|
496
|
+
return this.serializeRegularNotification(updated);
|
|
185
497
|
}
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
200
|
-
where: {
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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;
|