@studious-lms/server 1.1.8 → 1.1.10

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.
@@ -0,0 +1,531 @@
1
+ import { z } from 'zod';
2
+ import { createTRPCRouter, protectedProcedure } from '../trpc.js';
3
+ import { prisma } from '../lib/prisma.js';
4
+ import { pusher } from '../lib/pusher.js';
5
+ import { TRPCError } from '@trpc/server';
6
+ export const messageRouter = createTRPCRouter({
7
+ list: protectedProcedure
8
+ .input(z.object({
9
+ conversationId: z.string(),
10
+ cursor: z.string().optional(),
11
+ limit: z.number().min(1).max(100).default(50),
12
+ }))
13
+ .query(async ({ input, ctx }) => {
14
+ const userId = ctx.user.id;
15
+ const { conversationId, cursor, limit } = input;
16
+ // Verify user is a member of the conversation
17
+ const membership = await prisma.conversationMember.findFirst({
18
+ where: {
19
+ conversationId,
20
+ userId,
21
+ },
22
+ });
23
+ if (!membership) {
24
+ throw new TRPCError({
25
+ code: 'FORBIDDEN',
26
+ message: 'Not a member of this conversation',
27
+ });
28
+ }
29
+ const messages = await prisma.message.findMany({
30
+ where: {
31
+ conversationId,
32
+ ...(cursor && {
33
+ createdAt: {
34
+ lt: new Date(cursor),
35
+ },
36
+ }),
37
+ },
38
+ include: {
39
+ sender: {
40
+ select: {
41
+ id: true,
42
+ username: true,
43
+ profile: {
44
+ select: {
45
+ displayName: true,
46
+ profilePicture: true,
47
+ },
48
+ },
49
+ },
50
+ },
51
+ mentions: {
52
+ include: {
53
+ user: {
54
+ select: {
55
+ id: true,
56
+ username: true,
57
+ profile: {
58
+ select: {
59
+ displayName: true,
60
+ },
61
+ },
62
+ },
63
+ },
64
+ },
65
+ },
66
+ },
67
+ orderBy: {
68
+ createdAt: 'desc',
69
+ },
70
+ take: limit + 1,
71
+ });
72
+ let nextCursor = undefined;
73
+ if (messages.length > limit) {
74
+ const nextItem = messages.pop();
75
+ nextCursor = nextItem.createdAt.toISOString();
76
+ }
77
+ return {
78
+ messages: messages.reverse().map((message) => ({
79
+ id: message.id,
80
+ content: message.content,
81
+ senderId: message.senderId,
82
+ conversationId: message.conversationId,
83
+ createdAt: message.createdAt,
84
+ sender: message.sender,
85
+ mentions: message.mentions.map((mention) => ({
86
+ user: mention.user,
87
+ })),
88
+ mentionsMe: message.mentions.some((mention) => mention.userId === userId),
89
+ })),
90
+ nextCursor,
91
+ };
92
+ }),
93
+ send: protectedProcedure
94
+ .input(z.object({
95
+ conversationId: z.string(),
96
+ content: z.string().min(1).max(4000),
97
+ mentionedUserIds: z.array(z.string()).optional(),
98
+ }))
99
+ .mutation(async ({ input, ctx }) => {
100
+ const userId = ctx.user.id;
101
+ const { conversationId, content, mentionedUserIds = [] } = input;
102
+ // Verify user is a member of the conversation
103
+ const membership = await prisma.conversationMember.findFirst({
104
+ where: {
105
+ conversationId,
106
+ userId,
107
+ },
108
+ });
109
+ if (!membership) {
110
+ throw new TRPCError({
111
+ code: 'FORBIDDEN',
112
+ message: 'Not a member of this conversation',
113
+ });
114
+ }
115
+ // Verify mentioned users are members of the conversation
116
+ if (mentionedUserIds.length > 0) {
117
+ const mentionedMemberships = await prisma.conversationMember.findMany({
118
+ where: {
119
+ conversationId,
120
+ userId: { in: mentionedUserIds },
121
+ },
122
+ });
123
+ if (mentionedMemberships.length !== mentionedUserIds.length) {
124
+ throw new TRPCError({
125
+ code: 'BAD_REQUEST',
126
+ message: 'Some mentioned users are not members of this conversation',
127
+ });
128
+ }
129
+ }
130
+ // Create message, mentions, and update conversation timestamp
131
+ const result = await prisma.$transaction(async (tx) => {
132
+ const message = await tx.message.create({
133
+ data: {
134
+ content,
135
+ senderId: userId,
136
+ conversationId,
137
+ },
138
+ include: {
139
+ sender: {
140
+ select: {
141
+ id: true,
142
+ username: true,
143
+ profile: {
144
+ select: {
145
+ displayName: true,
146
+ profilePicture: true,
147
+ },
148
+ },
149
+ },
150
+ },
151
+ },
152
+ });
153
+ // Create mentions
154
+ if (mentionedUserIds.length > 0) {
155
+ await tx.mention.createMany({
156
+ data: mentionedUserIds.map((mentionedUserId) => ({
157
+ messageId: message.id,
158
+ userId: mentionedUserId,
159
+ })),
160
+ });
161
+ }
162
+ // Update conversation timestamp
163
+ await tx.conversation.update({
164
+ where: { id: conversationId },
165
+ data: { updatedAt: new Date() },
166
+ });
167
+ return message;
168
+ });
169
+ // Broadcast to Pusher channel
170
+ try {
171
+ await pusher.trigger(`conversation-${conversationId}`, 'new-message', {
172
+ id: result.id,
173
+ content: result.content,
174
+ senderId: result.senderId,
175
+ conversationId: result.conversationId,
176
+ createdAt: result.createdAt,
177
+ sender: result.sender,
178
+ mentionedUserIds,
179
+ });
180
+ }
181
+ catch (error) {
182
+ console.error('Failed to broadcast message:', error);
183
+ // Don't fail the request if Pusher fails
184
+ }
185
+ return {
186
+ id: result.id,
187
+ content: result.content,
188
+ senderId: result.senderId,
189
+ conversationId: result.conversationId,
190
+ createdAt: result.createdAt,
191
+ sender: result.sender,
192
+ mentionedUserIds,
193
+ };
194
+ }),
195
+ update: protectedProcedure
196
+ .input(z.object({
197
+ messageId: z.string(),
198
+ content: z.string().min(1).max(4000),
199
+ mentionedUserIds: z.array(z.string()).optional(),
200
+ }))
201
+ .mutation(async ({ input, ctx }) => {
202
+ const userId = ctx.user.id;
203
+ const { messageId, content, mentionedUserIds = [] } = input;
204
+ // Get the existing message and verify user is the sender
205
+ const existingMessage = await prisma.message.findUnique({
206
+ where: { id: messageId },
207
+ include: {
208
+ sender: {
209
+ select: {
210
+ id: true,
211
+ username: true,
212
+ profile: {
213
+ select: {
214
+ displayName: true,
215
+ profilePicture: true,
216
+ },
217
+ },
218
+ },
219
+ },
220
+ },
221
+ });
222
+ if (!existingMessage) {
223
+ throw new TRPCError({
224
+ code: 'NOT_FOUND',
225
+ message: 'Message not found',
226
+ });
227
+ }
228
+ if (existingMessage.senderId !== userId) {
229
+ throw new TRPCError({
230
+ code: 'FORBIDDEN',
231
+ message: 'Not the sender of this message',
232
+ });
233
+ }
234
+ // Verify user is still a member of the conversation
235
+ const membership = await prisma.conversationMember.findFirst({
236
+ where: {
237
+ conversationId: existingMessage.conversationId,
238
+ userId,
239
+ },
240
+ });
241
+ if (!membership) {
242
+ throw new TRPCError({
243
+ code: 'FORBIDDEN',
244
+ message: 'Not a member of this conversation',
245
+ });
246
+ }
247
+ // Verify mentioned users are members of the conversation
248
+ if (mentionedUserIds.length > 0) {
249
+ const mentionedMemberships = await prisma.conversationMember.findMany({
250
+ where: {
251
+ conversationId: existingMessage.conversationId,
252
+ userId: { in: mentionedUserIds },
253
+ },
254
+ });
255
+ if (mentionedMemberships.length !== mentionedUserIds.length) {
256
+ throw new TRPCError({
257
+ code: 'BAD_REQUEST',
258
+ message: 'Some mentioned users are not members of this conversation',
259
+ });
260
+ }
261
+ }
262
+ // Update message and mentions in transaction
263
+ const updatedMessage = await prisma.$transaction(async (tx) => {
264
+ // Update the message content
265
+ const message = await tx.message.update({
266
+ where: { id: messageId },
267
+ data: { content },
268
+ include: {
269
+ sender: {
270
+ select: {
271
+ id: true,
272
+ username: true,
273
+ profile: {
274
+ select: {
275
+ displayName: true,
276
+ profilePicture: true,
277
+ },
278
+ },
279
+ },
280
+ },
281
+ },
282
+ });
283
+ // Delete existing mentions
284
+ await tx.mention.deleteMany({
285
+ where: { messageId },
286
+ });
287
+ // Create new mentions if any
288
+ if (mentionedUserIds.length > 0) {
289
+ await tx.mention.createMany({
290
+ data: mentionedUserIds.map((mentionedUserId) => ({
291
+ messageId,
292
+ userId: mentionedUserId,
293
+ })),
294
+ });
295
+ }
296
+ return message;
297
+ });
298
+ // Broadcast message update to Pusher
299
+ try {
300
+ await pusher.trigger(`conversation-${existingMessage.conversationId}`, 'message-updated', {
301
+ id: updatedMessage.id,
302
+ content: updatedMessage.content,
303
+ senderId: updatedMessage.senderId,
304
+ conversationId: updatedMessage.conversationId,
305
+ createdAt: updatedMessage.createdAt,
306
+ sender: updatedMessage.sender,
307
+ mentionedUserIds,
308
+ });
309
+ }
310
+ catch (error) {
311
+ console.error('Failed to broadcast message update:', error);
312
+ // Don't fail the request if Pusher fails
313
+ }
314
+ return {
315
+ id: updatedMessage.id,
316
+ content: updatedMessage.content,
317
+ senderId: updatedMessage.senderId,
318
+ conversationId: updatedMessage.conversationId,
319
+ createdAt: updatedMessage.createdAt,
320
+ sender: updatedMessage.sender,
321
+ mentionedUserIds,
322
+ };
323
+ }),
324
+ delete: protectedProcedure
325
+ .input(z.object({
326
+ messageId: z.string(),
327
+ }))
328
+ .mutation(async ({ input, ctx }) => {
329
+ const userId = ctx.user.id;
330
+ const { messageId } = input;
331
+ // Get the message and verify user is the sender
332
+ const existingMessage = await prisma.message.findUnique({
333
+ where: { id: messageId },
334
+ include: {
335
+ sender: {
336
+ select: {
337
+ id: true,
338
+ username: true,
339
+ },
340
+ },
341
+ },
342
+ });
343
+ if (!existingMessage) {
344
+ throw new TRPCError({
345
+ code: 'NOT_FOUND',
346
+ message: 'Message not found',
347
+ });
348
+ }
349
+ if (existingMessage.senderId !== userId) {
350
+ throw new TRPCError({
351
+ code: 'FORBIDDEN',
352
+ message: 'Not the sender of this message',
353
+ });
354
+ }
355
+ // Verify user is still a member of the conversation
356
+ const membership = await prisma.conversationMember.findFirst({
357
+ where: {
358
+ conversationId: existingMessage.conversationId,
359
+ userId,
360
+ },
361
+ });
362
+ if (!membership) {
363
+ throw new TRPCError({
364
+ code: 'FORBIDDEN',
365
+ message: 'Not a member of this conversation',
366
+ });
367
+ }
368
+ // Delete message and all related mentions in transaction
369
+ await prisma.$transaction(async (tx) => {
370
+ // Delete mentions first (due to foreign key constraint)
371
+ await tx.mention.deleteMany({
372
+ where: { messageId },
373
+ });
374
+ // Delete the message
375
+ await tx.message.delete({
376
+ where: { id: messageId },
377
+ });
378
+ });
379
+ // Broadcast message deletion to Pusher
380
+ try {
381
+ await pusher.trigger(`conversation-${existingMessage.conversationId}`, 'message-deleted', {
382
+ messageId,
383
+ conversationId: existingMessage.conversationId,
384
+ senderId: existingMessage.senderId,
385
+ });
386
+ }
387
+ catch (error) {
388
+ console.error('Failed to broadcast message deletion:', error);
389
+ // Don't fail the request if Pusher fails
390
+ }
391
+ return {
392
+ success: true,
393
+ messageId,
394
+ };
395
+ }),
396
+ markAsRead: protectedProcedure
397
+ .input(z.object({
398
+ conversationId: z.string(),
399
+ }))
400
+ .mutation(async ({ input, ctx }) => {
401
+ const userId = ctx.user.id;
402
+ const { conversationId } = input;
403
+ // Verify user is a member of the conversation and update lastViewedAt
404
+ const membership = await prisma.conversationMember.findFirst({
405
+ where: {
406
+ conversationId,
407
+ userId,
408
+ },
409
+ });
410
+ if (!membership) {
411
+ throw new TRPCError({
412
+ code: 'FORBIDDEN',
413
+ message: 'Not a member of this conversation',
414
+ });
415
+ }
416
+ // Update the user's lastViewedAt timestamp for this conversation
417
+ await prisma.conversationMember.update({
418
+ where: {
419
+ id: membership.id,
420
+ },
421
+ data: {
422
+ lastViewedAt: new Date(),
423
+ },
424
+ });
425
+ // Broadcast that user has viewed the conversation
426
+ try {
427
+ await pusher.trigger(`conversation-${conversationId}`, 'conversation-viewed', {
428
+ userId,
429
+ viewedAt: new Date(),
430
+ });
431
+ }
432
+ catch (error) {
433
+ console.error('Failed to broadcast conversation view:', error);
434
+ // Don't fail the request if Pusher fails
435
+ }
436
+ return { success: true };
437
+ }),
438
+ markMentionsAsRead: protectedProcedure
439
+ .input(z.object({
440
+ conversationId: z.string(),
441
+ }))
442
+ .mutation(async ({ input, ctx }) => {
443
+ const userId = ctx.user.id;
444
+ const { conversationId } = input;
445
+ // Verify user is a member of the conversation and update lastViewedMentionAt
446
+ const membership = await prisma.conversationMember.findFirst({
447
+ where: {
448
+ conversationId,
449
+ userId,
450
+ },
451
+ });
452
+ if (!membership) {
453
+ throw new TRPCError({
454
+ code: 'FORBIDDEN',
455
+ message: 'Not a member of this conversation',
456
+ });
457
+ }
458
+ // Update the user's lastViewedMentionAt timestamp for this conversation
459
+ await prisma.conversationMember.update({
460
+ where: {
461
+ id: membership.id,
462
+ },
463
+ data: {
464
+ lastViewedMentionAt: new Date(),
465
+ },
466
+ });
467
+ // Broadcast that user has viewed mentions
468
+ try {
469
+ await pusher.trigger(`conversation-${conversationId}`, 'mentions-viewed', {
470
+ userId,
471
+ viewedAt: new Date(),
472
+ });
473
+ }
474
+ catch (error) {
475
+ console.error('Failed to broadcast mentions view:', error);
476
+ // Don't fail the request if Pusher fails
477
+ }
478
+ return { success: true };
479
+ }),
480
+ getUnreadCount: protectedProcedure
481
+ .input(z.object({ conversationId: z.string() }))
482
+ .query(async ({ input, ctx }) => {
483
+ const userId = ctx.user.id;
484
+ const { conversationId } = input;
485
+ // Get user's membership with lastViewedAt and lastViewedMentionAt
486
+ const membership = await prisma.conversationMember.findFirst({
487
+ where: {
488
+ conversationId,
489
+ userId,
490
+ },
491
+ });
492
+ if (!membership) {
493
+ throw new TRPCError({
494
+ code: 'FORBIDDEN',
495
+ message: 'Not a member of this conversation',
496
+ });
497
+ }
498
+ // Count regular unread messages
499
+ const unreadCount = await prisma.message.count({
500
+ where: {
501
+ conversationId,
502
+ senderId: { not: userId },
503
+ ...(membership.lastViewedAt && {
504
+ createdAt: { gt: membership.lastViewedAt }
505
+ }),
506
+ },
507
+ });
508
+ // Count unread mentions
509
+ // Use the later of lastViewedAt or lastViewedMentionAt
510
+ // This means if user viewed conversation after mention, mention is considered read
511
+ const mentionCutoffTime = membership.lastViewedMentionAt && membership.lastViewedAt
512
+ ? (membership.lastViewedMentionAt > membership.lastViewedAt ? membership.lastViewedMentionAt : membership.lastViewedAt)
513
+ : (membership.lastViewedMentionAt || membership.lastViewedAt);
514
+ const unreadMentionCount = await prisma.mention.count({
515
+ where: {
516
+ userId,
517
+ message: {
518
+ conversationId,
519
+ senderId: { not: userId },
520
+ ...(mentionCutoffTime && {
521
+ createdAt: { gt: mentionCutoffTime }
522
+ }),
523
+ },
524
+ },
525
+ });
526
+ return {
527
+ unreadCount,
528
+ unreadMentionCount
529
+ };
530
+ }),
531
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"user.d.ts","sourceRoot":"","sources":["../../src/routers/user.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AA2DxB,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqMrB,CAAC"}
1
+ {"version":3,"file":"user.d.ts","sourceRoot":"","sources":["../../src/routers/user.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AA2DxB,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuMrB,CAAC"}
@@ -2,7 +2,6 @@ import { z } from "zod";
2
2
  import { createTRPCRouter, protectedProcedure } from "../trpc.js";
3
3
  import { TRPCError } from "@trpc/server";
4
4
  import { prisma } from "../lib/prisma.js";
5
- import { getSignedUrl } from "../lib/googleCloudStorage.js";
6
5
  import { logger } from "../utils/logger.js";
7
6
  // Helper function to convert file path to backend proxy URL
8
7
  function getFileUrl(filePath) {
@@ -203,13 +202,15 @@ export const userRouter = createTRPCRouter({
203
202
  const fileExtension = input.fileName.split('.').pop();
204
203
  const uniqueFilename = `${ctx.user.id}-${Date.now()}.${fileExtension}`;
205
204
  const filePath = `users/${ctx.user.id}/profile/${uniqueFilename}`;
206
- // Generate signed URL for direct upload (write permission)
207
- const uploadUrl = await getSignedUrl(filePath, 'write', input.fileType);
205
+ // Generate backend proxy upload URL instead of direct GCS signed URL
206
+ const backendUrl = process.env.BACKEND_URL || 'http://localhost:3001';
207
+ const uploadUrl = `${backendUrl}/api/upload/${encodeURIComponent(filePath)}`;
208
208
  logger.info('Generated upload URL', {
209
209
  userId: ctx.user.id,
210
210
  filePath,
211
211
  fileName: uniqueFilename,
212
- fileType: input.fileType
212
+ fileType: input.fileType,
213
+ uploadUrl
213
214
  });
214
215
  return {
215
216
  uploadUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@studious-lms/server",
3
- "version": "1.1.8",
3
+ "version": "1.1.10",
4
4
  "description": "Backend server for Studious application",
5
5
  "main": "dist/exportType.js",
6
6
  "types": "dist/exportType.d.ts",
@@ -31,6 +31,7 @@
31
31
  "express": "^4.18.3",
32
32
  "nodemailer": "^7.0.4",
33
33
  "prisma": "^6.7.0",
34
+ "pusher": "^5.2.0",
34
35
  "sharp": "^0.34.2",
35
36
  "socket.io": "^4.8.1",
36
37
  "superjson": "^2.2.2",
@@ -0,0 +1,68 @@
1
+ -- CreateEnum
2
+ CREATE TYPE "ConversationType" AS ENUM ('DM', 'GROUP');
3
+
4
+ -- CreateTable
5
+ CREATE TABLE "Conversation" (
6
+ "id" TEXT NOT NULL,
7
+ "type" "ConversationType" NOT NULL,
8
+ "name" TEXT,
9
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
10
+ "updatedAt" TIMESTAMP(3) NOT NULL,
11
+
12
+ CONSTRAINT "Conversation_pkey" PRIMARY KEY ("id")
13
+ );
14
+
15
+ -- CreateTable
16
+ CREATE TABLE "ConversationMember" (
17
+ "id" TEXT NOT NULL,
18
+ "userId" TEXT NOT NULL,
19
+ "conversationId" TEXT NOT NULL,
20
+ "joinedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
21
+
22
+ CONSTRAINT "ConversationMember_pkey" PRIMARY KEY ("id")
23
+ );
24
+
25
+ -- CreateTable
26
+ CREATE TABLE "Message" (
27
+ "id" TEXT NOT NULL,
28
+ "content" TEXT NOT NULL,
29
+ "senderId" TEXT NOT NULL,
30
+ "conversationId" TEXT NOT NULL,
31
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
32
+
33
+ CONSTRAINT "Message_pkey" PRIMARY KEY ("id")
34
+ );
35
+
36
+ -- CreateTable
37
+ CREATE TABLE "MessageRead" (
38
+ "id" TEXT NOT NULL,
39
+ "messageId" TEXT NOT NULL,
40
+ "userId" TEXT NOT NULL,
41
+ "readAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
42
+
43
+ CONSTRAINT "MessageRead_pkey" PRIMARY KEY ("id")
44
+ );
45
+
46
+ -- CreateIndex
47
+ CREATE UNIQUE INDEX "ConversationMember_userId_conversationId_key" ON "ConversationMember"("userId", "conversationId");
48
+
49
+ -- CreateIndex
50
+ CREATE UNIQUE INDEX "MessageRead_messageId_userId_key" ON "MessageRead"("messageId", "userId");
51
+
52
+ -- AddForeignKey
53
+ ALTER TABLE "ConversationMember" ADD CONSTRAINT "ConversationMember_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
54
+
55
+ -- AddForeignKey
56
+ ALTER TABLE "ConversationMember" ADD CONSTRAINT "ConversationMember_conversationId_fkey" FOREIGN KEY ("conversationId") REFERENCES "Conversation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
57
+
58
+ -- AddForeignKey
59
+ ALTER TABLE "Message" ADD CONSTRAINT "Message_senderId_fkey" FOREIGN KEY ("senderId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
60
+
61
+ -- AddForeignKey
62
+ ALTER TABLE "Message" ADD CONSTRAINT "Message_conversationId_fkey" FOREIGN KEY ("conversationId") REFERENCES "Conversation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
63
+
64
+ -- AddForeignKey
65
+ ALTER TABLE "MessageRead" ADD CONSTRAINT "MessageRead_messageId_fkey" FOREIGN KEY ("messageId") REFERENCES "Message"("id") ON DELETE CASCADE ON UPDATE CASCADE;
66
+
67
+ -- AddForeignKey
68
+ ALTER TABLE "MessageRead" ADD CONSTRAINT "MessageRead_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,5 @@
1
+ -- CreateEnum
2
+ CREATE TYPE "ConversationRole" AS ENUM ('ADMIN', 'MEMBER');
3
+
4
+ -- AlterTable
5
+ ALTER TABLE "ConversationMember" ADD COLUMN "role" "ConversationRole" NOT NULL DEFAULT 'MEMBER';
@@ -0,0 +1,20 @@
1
+ /*
2
+ Warnings:
3
+
4
+ - You are about to drop the `MessageRead` table. If the table is not empty, all the data it contains will be lost.
5
+
6
+ */
7
+ -- DropForeignKey
8
+ ALTER TABLE "MessageRead" DROP CONSTRAINT "MessageRead_messageId_fkey";
9
+
10
+ -- DropForeignKey
11
+ ALTER TABLE "MessageRead" DROP CONSTRAINT "MessageRead_userId_fkey";
12
+
13
+ -- AlterTable
14
+ ALTER TABLE "Conversation" ADD COLUMN "displayInChat" BOOLEAN NOT NULL DEFAULT true;
15
+
16
+ -- AlterTable
17
+ ALTER TABLE "ConversationMember" ADD COLUMN "lastViewedAt" TIMESTAMP(3);
18
+
19
+ -- DropTable
20
+ DROP TABLE "MessageRead";