vintasend-prisma 0.1.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/.editorconfig ADDED
@@ -0,0 +1,9 @@
1
+ root = true
2
+
3
+ [{src,scripts}/**.{ts,json,js}]
4
+ end_of_line = crlf
5
+ charset = utf-8
6
+ trim_trailing_whitespace = true
7
+ insert_final_newline = true
8
+ indent_style = space
9
+ indent_size = 2
package/biome.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
3
+ "vcs": {
4
+ "enabled": false,
5
+ "clientKind": "git",
6
+ "useIgnoreFile": false
7
+ },
8
+ "files": {
9
+ "ignoreUnknown": false,
10
+ "ignore": []
11
+ },
12
+ "formatter": {
13
+ "enabled": true,
14
+ "indentStyle": "space",
15
+ "indentWidth": 2,
16
+ "lineWidth": 100
17
+ },
18
+ "organizeImports": {
19
+ "enabled": true
20
+ },
21
+ "linter": {
22
+ "enabled": true,
23
+ "rules": {
24
+ "recommended": true
25
+ }
26
+ },
27
+ "javascript": {
28
+ "formatter": {
29
+ "quoteStyle": "single",
30
+ "semicolons": "always",
31
+ "trailingCommas": "all"
32
+ }
33
+ }
34
+ }
package/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { PrismaNotificationBackend } from './prisma-notification-backend';
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "vintasend-prisma",
3
+ "version": "0.1.0",
4
+ "description": "VintaSend Backend implementation for Prisma",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1",
8
+ "generate-prisma-client": "prisma generate"
9
+ },
10
+ "author": "",
11
+ "license": "ISC",
12
+ "dependencies": {
13
+ "@prisma/client": "^6.3.1",
14
+ "prisma": "^6.3.1",
15
+ "vintasend": "^0.1.0"
16
+ }
17
+ }
@@ -0,0 +1,516 @@
1
+ import type { BaseNotificationBackend } from 'vintasend/src/services/notification-backends/base-notification-backend';
2
+ import type { ContextGenerator } from 'vintasend/src/services/notification-context-registry';
3
+ import type { InputJsonValue, JsonValue } from 'vintasend/src/types/json-values';
4
+ import type { Notification, NotificationInput } from 'vintasend/src/types/notification';
5
+ import type { NotificationStatus } from 'vintasend/src/types/notification-status';
6
+ import type { NotificationType } from 'vintasend/src/types/notification-type';
7
+
8
+ export const NotificationStatusEnum = {
9
+ PENDING_SEND: 'PENDING_SEND',
10
+ SENT: 'SENT',
11
+ FAILED: 'FAILED',
12
+ READ: 'READ',
13
+ CANCELLED: 'CANCELLED',
14
+ } as const;
15
+
16
+ export const NotificationTypeEnum = {
17
+ EMAIL: 'EMAIL',
18
+ PUSH: 'PUSH',
19
+ SMS: 'SMS',
20
+ IN_APP: 'IN_APP',
21
+ } as const;
22
+
23
+ export interface PrismaNotificationModel<IdType, UserId> {
24
+ id: IdType;
25
+ userId: UserId;
26
+ notificationType: NotificationType;
27
+ title: string | null;
28
+ bodyTemplate: string;
29
+ contextName: string;
30
+ contextParameters: JsonValue;
31
+ sendAfter: Date | null;
32
+ subjectTemplate: string | null;
33
+ status: NotificationStatus;
34
+ contextUsed: JsonValue | null;
35
+ extraParams: JsonValue | null;
36
+ adapterUsed: string | null;
37
+ sentAt: Date | null;
38
+ readAt: Date | null;
39
+ createdAt: Date;
40
+ updatedAt: Date;
41
+ user?: {
42
+ email: string;
43
+ };
44
+ }
45
+
46
+ export interface NotificationPrismaClientInterface<NotificationIdType, UserIdType> {
47
+ notification: {
48
+ findMany(args: {
49
+ where?: {
50
+ status?: NotificationStatus | { not: NotificationStatus };
51
+ sendAfter?: { lte: Date } | null;
52
+ userId?: UserIdType;
53
+ readAt?: null;
54
+ };
55
+ skip?: number;
56
+ take?: number;
57
+ include?: { user?: boolean };
58
+ }): Promise<PrismaNotificationModel<NotificationIdType, UserIdType>[]>;
59
+ create(args: {
60
+ data: BaseNotificationCreateInput<UserIdType>;
61
+ include?: { user?: boolean };
62
+ }): Promise<PrismaNotificationModel<NotificationIdType, UserIdType>>;
63
+ update(args: {
64
+ where: { id: NotificationIdType };
65
+ data: Partial<BaseNotificationUpdateInput<UserIdType>>;
66
+ include?: { user?: boolean };
67
+ }): Promise<PrismaNotificationModel<NotificationIdType, UserIdType>>;
68
+ findUnique(args: {
69
+ where: { id: NotificationIdType };
70
+ include?: { user?: boolean };
71
+ }): Promise<PrismaNotificationModel<NotificationIdType, UserIdType> | null>;
72
+ };
73
+ }
74
+
75
+ // cause typescript not to expand types and preserve names
76
+ type NoExpand<T> = T extends unknown ? T : never;
77
+
78
+ // this type assumes the passed object is entirely optional
79
+ type AtLeast<O extends object, K extends string> = NoExpand<
80
+ O extends unknown
81
+ ?
82
+ | (K extends keyof O ? { [P in K]: O[P] } & O : O)
83
+ | ({ [P in keyof O as P extends K ? K : never]-?: O[P] } & O)
84
+ : never
85
+ >;
86
+
87
+ export interface BaseNotificationCreateInput<UserIdType> {
88
+ user: {
89
+ connect?: AtLeast<
90
+ {
91
+ id?: UserIdType;
92
+ email?: string;
93
+ },
94
+ 'id' | 'email'
95
+ >;
96
+ };
97
+ notificationType: NotificationType;
98
+ title?: string | null;
99
+ bodyTemplate: string;
100
+ contextName: string;
101
+ contextParameters: InputJsonValue;
102
+ sendAfter?: Date | null;
103
+ subjectTemplate?: string | null;
104
+ status?: NotificationStatus;
105
+ contextUsed?: InputJsonValue;
106
+ extraParams?: InputJsonValue;
107
+ adapterUsed?: string | null;
108
+ sentAt?: Date | null;
109
+ readAt?: Date | null;
110
+ }
111
+
112
+ export interface BaseNotificationUpdateInput<UserIdType> {
113
+ user: {
114
+ connect?: AtLeast<
115
+ {
116
+ id?: UserIdType;
117
+ email?: string;
118
+ },
119
+ 'id' | 'email'
120
+ >;
121
+ };
122
+ notificationType?: NotificationType;
123
+ title?: string | null;
124
+ bodyTemplate?: string;
125
+ contextName?: string;
126
+ contextParameters?: InputJsonValue;
127
+ sendAfter?: Date | null;
128
+ subjectTemplate?: string | null;
129
+ status?: NotificationStatus;
130
+ contextUsed?: InputJsonValue;
131
+ extraParams?: InputJsonValue;
132
+ adapterUsed?: string | null;
133
+ sentAt?: Date | null;
134
+ readAt?: Date | null;
135
+ }
136
+
137
+ function convertJsonValueToRecord(jsonValue: JsonValue): Record<string, string | number | boolean> {
138
+ if (typeof jsonValue === 'object' && !Array.isArray(jsonValue) && jsonValue !== null) {
139
+ return jsonValue as Record<string, string | number | boolean>;
140
+ }
141
+
142
+ throw new Error('Invalid JSON value. It should be an object.');
143
+ }
144
+
145
+ export class PrismaNotificationBackend<
146
+ Client extends NotificationPrismaClientInterface<NotificationIdType, UserIdType>,
147
+ AvailableContexts extends Record<string, ContextGenerator>,
148
+ NotificationIdType extends string | number,
149
+ UserIdType extends string | number,
150
+ > implements BaseNotificationBackend<AvailableContexts>
151
+ {
152
+ constructor(private prismaClient: Client) {}
153
+
154
+ serializeNotification(
155
+ notification: NonNullable<
156
+ Awaited<ReturnType<typeof this.prismaClient.notification.findUnique>>
157
+ >,
158
+ ): Notification<AvailableContexts, NotificationIdType, UserIdType> {
159
+ return {
160
+ id: notification.id,
161
+ userId: notification.userId,
162
+ notificationType: notification.notificationType,
163
+ title: notification.title,
164
+ bodyTemplate: notification.bodyTemplate,
165
+ contextName: notification.contextName as keyof AvailableContexts,
166
+ contextParameters: notification.contextParameters
167
+ ? (notification.contextParameters as Parameters<
168
+ AvailableContexts[keyof AvailableContexts]['generate']
169
+ >[0])
170
+ : {},
171
+ sendAfter: notification.sendAfter,
172
+ subjectTemplate: notification.subjectTemplate,
173
+ status: notification.status,
174
+ contextUsed: notification.contextUsed as ReturnType<
175
+ AvailableContexts[keyof AvailableContexts]['generate']
176
+ > | null,
177
+ extraParams: notification.extraParams
178
+ ? convertJsonValueToRecord(notification.extraParams)
179
+ : null,
180
+ adapterUsed: notification.adapterUsed,
181
+ sentAt: notification.sentAt,
182
+ readAt: notification.readAt,
183
+ createdAt: notification.createdAt,
184
+ updatedAt: notification.updatedAt,
185
+ };
186
+ }
187
+
188
+ deserializeNotification(
189
+ notification: NotificationInput<AvailableContexts, UserIdType>,
190
+ ): BaseNotificationCreateInput<UserIdType> {
191
+ return {
192
+ user: {
193
+ connect: {
194
+ id: notification.userId,
195
+ },
196
+ },
197
+ notificationType: notification.notificationType,
198
+ title: notification.title,
199
+ bodyTemplate: notification.bodyTemplate,
200
+ contextName: notification.contextName as string,
201
+ contextParameters: notification.contextParameters as InputJsonValue,
202
+ sendAfter: notification.sendAfter,
203
+ subjectTemplate: notification.subjectTemplate,
204
+ extraParams: notification.extraParams as InputJsonValue,
205
+ };
206
+ }
207
+
208
+ deserializeNotificationForUpdate(
209
+ notification: Partial<Notification<AvailableContexts, NotificationIdType, UserIdType>>,
210
+ ): Partial<Parameters<typeof this.prismaClient.notification.update>[0]['data']> {
211
+ return {
212
+ ...(notification.userId ? { user: { connect: { id: notification.userId } } } : {}),
213
+ ...(notification.notificationType
214
+ ? {
215
+ notificationType: NotificationTypeEnum[
216
+ notification.notificationType as keyof typeof NotificationTypeEnum
217
+ ] as NotificationType,
218
+ }
219
+ : {}),
220
+ ...(notification.title ? { title: notification.title } : {}),
221
+ ...(notification.bodyTemplate ? { bodyTemplate: notification.bodyTemplate } : {}),
222
+ ...(notification.contextName ? { contextName: notification.contextName } : {}),
223
+ ...(notification.contextParameters
224
+ ? {
225
+ contextParameters: notification.contextParameters ? notification.contextParameters : {},
226
+ }
227
+ : {}),
228
+ ...(notification.sendAfter ? { sendAfter: notification.sendAfter } : {}),
229
+ ...(notification.subjectTemplate ? { subjectTemplate: notification.subjectTemplate } : {}),
230
+ } as Partial<Parameters<typeof this.prismaClient.notification.update>[0]['data']>;
231
+ }
232
+
233
+ async getAllPendingNotifications(): Promise<
234
+ Notification<AvailableContexts, NotificationIdType, UserIdType>[]
235
+ > {
236
+ const notifications = await this.prismaClient.notification.findMany({
237
+ where: {
238
+ status: NotificationStatusEnum.PENDING_SEND,
239
+ },
240
+ });
241
+
242
+ return notifications.map(this.serializeNotification);
243
+ }
244
+
245
+ async getPendingNotifications(): Promise<
246
+ Notification<AvailableContexts, NotificationIdType, UserIdType>[]
247
+ > {
248
+ const notifications = await this.prismaClient.notification.findMany({
249
+ where: {
250
+ status: NotificationStatusEnum.PENDING_SEND,
251
+ sendAfter: null,
252
+ },
253
+ });
254
+
255
+ return notifications.map(this.serializeNotification);
256
+ }
257
+
258
+ async getAllFutureNotifications(): Promise<
259
+ Notification<AvailableContexts, NotificationIdType, UserIdType>[]
260
+ > {
261
+ const notifications = await this.prismaClient.notification.findMany({
262
+ where: {
263
+ status: {
264
+ not: NotificationStatusEnum.PENDING_SEND,
265
+ },
266
+ sendAfter: {
267
+ lte: new Date(),
268
+ },
269
+ },
270
+ });
271
+
272
+ return notifications.map(this.serializeNotification);
273
+ }
274
+
275
+ async getFutureNotifications(): Promise<
276
+ Notification<AvailableContexts, NotificationIdType, UserIdType>[]
277
+ > {
278
+ const notifications = await this.prismaClient.notification.findMany({
279
+ where: {
280
+ status: {
281
+ not: NotificationStatusEnum.PENDING_SEND,
282
+ },
283
+ sendAfter: {
284
+ lte: new Date(),
285
+ },
286
+ },
287
+ });
288
+
289
+ return notifications.map(this.serializeNotification);
290
+ }
291
+
292
+ async getAllFutureNotificationsFromUser(
293
+ userId: NonNullable<
294
+ Awaited<ReturnType<typeof this.prismaClient.notification.findUnique>>
295
+ >['userId'],
296
+ ): Promise<Notification<AvailableContexts, NotificationIdType, UserIdType>[]> {
297
+ const notifications = await this.prismaClient.notification.findMany({
298
+ where: {
299
+ userId,
300
+ status: {
301
+ not: NotificationStatusEnum.PENDING_SEND,
302
+ },
303
+ sendAfter: {
304
+ lte: new Date(),
305
+ },
306
+ },
307
+ });
308
+
309
+ return notifications.map(this.serializeNotification);
310
+ }
311
+
312
+ async getFutureNotificationsFromUser(
313
+ userId: NonNullable<
314
+ Awaited<ReturnType<typeof this.prismaClient.notification.findUnique>>
315
+ >['userId'],
316
+ ): Promise<Notification<AvailableContexts, NotificationIdType, UserIdType>[]> {
317
+ const notifications = await this.prismaClient.notification.findMany({
318
+ where: {
319
+ userId,
320
+ status: {
321
+ not: NotificationStatusEnum.PENDING_SEND,
322
+ },
323
+ sendAfter: {
324
+ lte: new Date(),
325
+ },
326
+ },
327
+ });
328
+
329
+ return notifications.map(this.serializeNotification);
330
+ }
331
+
332
+ async persistNotification(
333
+ notification: NotificationInput<AvailableContexts, UserIdType>,
334
+ ): Promise<Notification<AvailableContexts, NotificationIdType, UserIdType>> {
335
+ return this.serializeNotification(
336
+ await this.prismaClient.notification.create({
337
+ data: this.deserializeNotification(notification),
338
+ }),
339
+ );
340
+ }
341
+
342
+ async persistNotificationUpdate(
343
+ notificationId: NonNullable<
344
+ Awaited<ReturnType<typeof this.prismaClient.notification.findUnique>>
345
+ >['id'],
346
+ notification: Partial<
347
+ Omit<Notification<AvailableContexts, NotificationIdType, UserIdType>, 'id'>
348
+ >,
349
+ ): Promise<Notification<AvailableContexts, NotificationIdType, UserIdType>> {
350
+ return this.serializeNotification(
351
+ await this.prismaClient.notification.update({
352
+ where: {
353
+ id: notificationId,
354
+ },
355
+ data: this.deserializeNotificationForUpdate(notification),
356
+ }),
357
+ );
358
+ }
359
+
360
+ async markPendingAsSent(
361
+ notificationId: NonNullable<
362
+ Awaited<ReturnType<typeof this.prismaClient.notification.findUnique>>
363
+ >['id'],
364
+ ): Promise<Notification<AvailableContexts, NotificationIdType, UserIdType>> {
365
+ return this.serializeNotification(
366
+ await this.prismaClient.notification.update({
367
+ where: {
368
+ id: notificationId,
369
+ },
370
+ data: {
371
+ status: NotificationStatusEnum.SENT,
372
+ sentAt: new Date(),
373
+ },
374
+ }),
375
+ );
376
+ }
377
+
378
+ async markPendingAsFailed(
379
+ notificationId: NonNullable<
380
+ Awaited<ReturnType<typeof this.prismaClient.notification.findUnique>>
381
+ >['id'],
382
+ ): Promise<Notification<AvailableContexts, NotificationIdType, UserIdType>> {
383
+ return this.serializeNotification(
384
+ await this.prismaClient.notification.update({
385
+ where: {
386
+ id: notificationId,
387
+ },
388
+ data: {
389
+ status: NotificationStatusEnum.FAILED,
390
+ sentAt: new Date(),
391
+ },
392
+ }),
393
+ );
394
+ }
395
+
396
+ async markSentAsRead(
397
+ notificationId: NonNullable<
398
+ Awaited<ReturnType<typeof this.prismaClient.notification.findUnique>>
399
+ >['id'],
400
+ ): Promise<Notification<AvailableContexts, NotificationIdType, UserIdType>> {
401
+ return this.serializeNotification(
402
+ await this.prismaClient.notification.update({
403
+ where: {
404
+ id: notificationId,
405
+ },
406
+ data: {
407
+ status: 'READ',
408
+ readAt: new Date(),
409
+ },
410
+ }),
411
+ );
412
+ }
413
+
414
+ async cancelNotification(
415
+ notificationId: NonNullable<
416
+ Awaited<ReturnType<typeof this.prismaClient.notification.findUnique>>
417
+ >['id'],
418
+ ): Promise<void> {
419
+ await this.prismaClient.notification.update({
420
+ where: {
421
+ id: notificationId,
422
+ },
423
+ data: {
424
+ status: NotificationStatusEnum.CANCELLED,
425
+ },
426
+ });
427
+ }
428
+
429
+ async getNotification(
430
+ notificationId: NonNullable<
431
+ Awaited<ReturnType<typeof this.prismaClient.notification.findUnique>>
432
+ >['id'],
433
+ // biome-ignore lint/correctness/noUnusedVariables: <explanation>
434
+ forUpdate: boolean,
435
+ ): Promise<Notification<AvailableContexts, NotificationIdType, UserIdType> | null> {
436
+ const notification = await this.prismaClient.notification.findUnique({
437
+ where: {
438
+ id: notificationId,
439
+ },
440
+ });
441
+ if (!notification) {
442
+ return null;
443
+ }
444
+
445
+ return this.serializeNotification(notification);
446
+ }
447
+
448
+ async filterAllInAppUnreadNotifications(
449
+ userId: NonNullable<
450
+ Awaited<ReturnType<typeof this.prismaClient.notification.findUnique>>
451
+ >['userId'],
452
+ ): Promise<Notification<AvailableContexts, NotificationIdType, UserIdType>[]> {
453
+ const notifications = await this.prismaClient.notification.findMany({
454
+ where: {
455
+ userId,
456
+ status: 'SENT',
457
+ readAt: null,
458
+ },
459
+ });
460
+
461
+ return notifications.map(this.serializeNotification);
462
+ }
463
+
464
+ async filterInAppUnreadNotifications(
465
+ userId: NonNullable<
466
+ Awaited<ReturnType<typeof this.prismaClient.notification.findUnique>>
467
+ >['userId'],
468
+ page: number,
469
+ pageSize: number,
470
+ ): Promise<Notification<AvailableContexts, NotificationIdType, UserIdType>[]> {
471
+ const notifications = await this.prismaClient.notification.findMany({
472
+ where: {
473
+ userId,
474
+ status: 'SENT',
475
+ readAt: null,
476
+ },
477
+ skip: page * pageSize,
478
+ take: pageSize,
479
+ });
480
+
481
+ return notifications.map(this.serializeNotification);
482
+ }
483
+
484
+ async getUserEmailFromNotification(
485
+ notificationId: NonNullable<
486
+ Awaited<ReturnType<typeof this.prismaClient.notification.findUnique>>
487
+ >['id'],
488
+ ): Promise<string | undefined> {
489
+ const notification = await this.prismaClient.notification.findUnique({
490
+ where: {
491
+ id: notificationId,
492
+ },
493
+ include: {
494
+ user: true,
495
+ },
496
+ });
497
+
498
+ return notification?.user?.email;
499
+ }
500
+
501
+ async storeContextUsed(
502
+ notificationId: NonNullable<
503
+ Awaited<ReturnType<typeof this.prismaClient.notification.findUnique>>
504
+ >['id'],
505
+ context: InputJsonValue,
506
+ ): Promise<void> {
507
+ await this.prismaClient.notification.update({
508
+ where: {
509
+ id: notificationId,
510
+ },
511
+ data: {
512
+ contextUsed: context,
513
+ },
514
+ });
515
+ }
516
+ }
@@ -0,0 +1,49 @@
1
+ datasource db {
2
+ provider = "postgresql"
3
+ url = env("DATABASE_URL")
4
+ }
5
+
6
+ generator client {
7
+ provider = "prisma-client-js"
8
+ }
9
+
10
+ model User {
11
+ id Int @id @default(autoincrement())
12
+ email String @unique
13
+ notifications Notification[]
14
+ }
15
+
16
+ model Notification {
17
+ id Int @id @default(autoincrement())
18
+ user User @relation(fields: [userId], references: [id])
19
+ userId Int
20
+ notificationType NotificationType
21
+ title String?
22
+ bodyTemplate String
23
+ contextName String
24
+ contextParameters Json @default("{}")
25
+ sendAfter DateTime?
26
+ subjectTemplate String?
27
+ status NotificationStatus @default(PENDING_SEND)
28
+ contextUsed Json?
29
+ adapterUsed String?
30
+ sentAt DateTime?
31
+ readAt DateTime?
32
+ createdAt DateTime @default(now())
33
+ updatedAt DateTime @updatedAt
34
+ }
35
+
36
+ enum NotificationType {
37
+ EMAIL
38
+ PUSH
39
+ SMS
40
+ IN_APP
41
+ }
42
+
43
+ enum NotificationStatus {
44
+ PENDING_SEND
45
+ SENT
46
+ FAILED
47
+ READ
48
+ CANCELLED
49
+ }