@studious-lms/server 1.1.7 → 1.1.9
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/CHAT_API_SPEC.md +579 -0
- package/dist/lib/pusher.d.ts +4 -0
- package/dist/lib/pusher.d.ts.map +1 -0
- package/dist/lib/pusher.js +9 -0
- package/dist/routers/_app.d.ts +496 -0
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/_app.js +4 -0
- package/dist/routers/auth.d.ts.map +1 -1
- package/dist/routers/class.d.ts +10 -0
- package/dist/routers/class.d.ts.map +1 -1
- package/dist/routers/class.js +13 -0
- package/dist/routers/conversation.d.ts +134 -0
- package/dist/routers/conversation.d.ts.map +1 -0
- package/dist/routers/conversation.js +261 -0
- package/dist/routers/message.d.ts +108 -0
- package/dist/routers/message.d.ts.map +1 -0
- package/dist/routers/message.js +325 -0
- package/package.json +2 -1
- package/prisma/migrations/20250922165147_add_chat_models/migration.sql +68 -0
- package/prisma/migrations/20250922165314_add_chat_roles/migration.sql +5 -0
- package/prisma/migrations/20250922171242_refactor_notification_system/migration.sql +20 -0
- package/prisma/migrations/20250922172208_add_mentions_system/migration.sql +21 -0
- package/prisma/schema.prisma +68 -1
- package/src/lib/pusher.ts +11 -0
- package/src/routers/_app.ts +4 -0
- package/src/routers/auth.ts +1 -0
- package/src/routers/class.ts +13 -0
- package/src/routers/conversation.ts +285 -0
- package/src/routers/message.ts +365 -0
|
@@ -0,0 +1,325 @@
|
|
|
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
|
+
markAsRead: protectedProcedure
|
|
196
|
+
.input(z.object({
|
|
197
|
+
conversationId: z.string(),
|
|
198
|
+
}))
|
|
199
|
+
.mutation(async ({ input, ctx }) => {
|
|
200
|
+
const userId = ctx.user.id;
|
|
201
|
+
const { conversationId } = input;
|
|
202
|
+
// Verify user is a member of the conversation and update lastViewedAt
|
|
203
|
+
const membership = await prisma.conversationMember.findFirst({
|
|
204
|
+
where: {
|
|
205
|
+
conversationId,
|
|
206
|
+
userId,
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
if (!membership) {
|
|
210
|
+
throw new TRPCError({
|
|
211
|
+
code: 'FORBIDDEN',
|
|
212
|
+
message: 'Not a member of this conversation',
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
// Update the user's lastViewedAt timestamp for this conversation
|
|
216
|
+
await prisma.conversationMember.update({
|
|
217
|
+
where: {
|
|
218
|
+
id: membership.id,
|
|
219
|
+
},
|
|
220
|
+
data: {
|
|
221
|
+
lastViewedAt: new Date(),
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
// Broadcast that user has viewed the conversation
|
|
225
|
+
try {
|
|
226
|
+
await pusher.trigger(`conversation-${conversationId}`, 'conversation-viewed', {
|
|
227
|
+
userId,
|
|
228
|
+
viewedAt: new Date(),
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
console.error('Failed to broadcast conversation view:', error);
|
|
233
|
+
// Don't fail the request if Pusher fails
|
|
234
|
+
}
|
|
235
|
+
return { success: true };
|
|
236
|
+
}),
|
|
237
|
+
markMentionsAsRead: protectedProcedure
|
|
238
|
+
.input(z.object({
|
|
239
|
+
conversationId: z.string(),
|
|
240
|
+
}))
|
|
241
|
+
.mutation(async ({ input, ctx }) => {
|
|
242
|
+
const userId = ctx.user.id;
|
|
243
|
+
const { conversationId } = input;
|
|
244
|
+
// Verify user is a member of the conversation and update lastViewedMentionAt
|
|
245
|
+
const membership = await prisma.conversationMember.findFirst({
|
|
246
|
+
where: {
|
|
247
|
+
conversationId,
|
|
248
|
+
userId,
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
if (!membership) {
|
|
252
|
+
throw new TRPCError({
|
|
253
|
+
code: 'FORBIDDEN',
|
|
254
|
+
message: 'Not a member of this conversation',
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
// Update the user's lastViewedMentionAt timestamp for this conversation
|
|
258
|
+
await prisma.conversationMember.update({
|
|
259
|
+
where: {
|
|
260
|
+
id: membership.id,
|
|
261
|
+
},
|
|
262
|
+
data: {
|
|
263
|
+
lastViewedMentionAt: new Date(),
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
// Broadcast that user has viewed mentions
|
|
267
|
+
try {
|
|
268
|
+
await pusher.trigger(`conversation-${conversationId}`, 'mentions-viewed', {
|
|
269
|
+
userId,
|
|
270
|
+
viewedAt: new Date(),
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
console.error('Failed to broadcast mentions view:', error);
|
|
275
|
+
// Don't fail the request if Pusher fails
|
|
276
|
+
}
|
|
277
|
+
return { success: true };
|
|
278
|
+
}),
|
|
279
|
+
getUnreadCount: protectedProcedure
|
|
280
|
+
.input(z.object({ conversationId: z.string() }))
|
|
281
|
+
.query(async ({ input, ctx }) => {
|
|
282
|
+
const userId = ctx.user.id;
|
|
283
|
+
const { conversationId } = input;
|
|
284
|
+
// Get user's membership with lastViewedAt and lastViewedMentionAt
|
|
285
|
+
const membership = await prisma.conversationMember.findFirst({
|
|
286
|
+
where: {
|
|
287
|
+
conversationId,
|
|
288
|
+
userId,
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
if (!membership) {
|
|
292
|
+
throw new TRPCError({
|
|
293
|
+
code: 'FORBIDDEN',
|
|
294
|
+
message: 'Not a member of this conversation',
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
// Count regular unread messages
|
|
298
|
+
const unreadCount = await prisma.message.count({
|
|
299
|
+
where: {
|
|
300
|
+
conversationId,
|
|
301
|
+
senderId: { not: userId },
|
|
302
|
+
...(membership.lastViewedAt && {
|
|
303
|
+
createdAt: { gt: membership.lastViewedAt }
|
|
304
|
+
}),
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
// Count unread mentions
|
|
308
|
+
const unreadMentionCount = await prisma.mention.count({
|
|
309
|
+
where: {
|
|
310
|
+
userId,
|
|
311
|
+
message: {
|
|
312
|
+
conversationId,
|
|
313
|
+
senderId: { not: userId },
|
|
314
|
+
...(membership.lastViewedMentionAt && {
|
|
315
|
+
createdAt: { gt: membership.lastViewedMentionAt }
|
|
316
|
+
}),
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
return {
|
|
321
|
+
unreadCount,
|
|
322
|
+
unreadMentionCount
|
|
323
|
+
};
|
|
324
|
+
}),
|
|
325
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@studious-lms/server",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.9",
|
|
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,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";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
-- AlterTable
|
|
2
|
+
ALTER TABLE "ConversationMember" ADD COLUMN "lastViewedMentionAt" TIMESTAMP(3);
|
|
3
|
+
|
|
4
|
+
-- CreateTable
|
|
5
|
+
CREATE TABLE "Mention" (
|
|
6
|
+
"id" TEXT NOT NULL,
|
|
7
|
+
"messageId" TEXT NOT NULL,
|
|
8
|
+
"userId" TEXT NOT NULL,
|
|
9
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
10
|
+
|
|
11
|
+
CONSTRAINT "Mention_pkey" PRIMARY KEY ("id")
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
-- CreateIndex
|
|
15
|
+
CREATE UNIQUE INDEX "Mention_messageId_userId_key" ON "Mention"("messageId", "userId");
|
|
16
|
+
|
|
17
|
+
-- AddForeignKey
|
|
18
|
+
ALTER TABLE "Mention" ADD CONSTRAINT "Mention_messageId_fkey" FOREIGN KEY ("messageId") REFERENCES "Message"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
19
|
+
|
|
20
|
+
-- AddForeignKey
|
|
21
|
+
ALTER TABLE "Mention" ADD CONSTRAINT "Mention_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
package/prisma/schema.prisma
CHANGED
|
@@ -76,6 +76,11 @@ model User {
|
|
|
76
76
|
school School? @relation(fields: [schoolId], references: [id])
|
|
77
77
|
schoolId String?
|
|
78
78
|
|
|
79
|
+
// Chat relations
|
|
80
|
+
conversationMemberships ConversationMember[]
|
|
81
|
+
sentMessages Message[] @relation("SentMessages")
|
|
82
|
+
mentions Mention[] @relation("UserMentions")
|
|
83
|
+
|
|
79
84
|
}
|
|
80
85
|
|
|
81
86
|
model UserProfile {
|
|
@@ -306,4 +311,66 @@ model Notification {
|
|
|
306
311
|
read Boolean @default(false)
|
|
307
312
|
sender User? @relation("SentNotifications", fields: [senderId], references: [id])
|
|
308
313
|
receiver User @relation("ReceivedNotifications", fields: [receiverId], references: [id], onDelete: Cascade)
|
|
309
|
-
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
enum ConversationType {
|
|
317
|
+
DM
|
|
318
|
+
GROUP
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
enum ConversationRole {
|
|
322
|
+
ADMIN
|
|
323
|
+
MEMBER
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
model Conversation {
|
|
327
|
+
id String @id @default(uuid())
|
|
328
|
+
type ConversationType
|
|
329
|
+
name String?
|
|
330
|
+
createdAt DateTime @default(now())
|
|
331
|
+
updatedAt DateTime @updatedAt
|
|
332
|
+
|
|
333
|
+
displayInChat Boolean @default(true)
|
|
334
|
+
|
|
335
|
+
members ConversationMember[]
|
|
336
|
+
messages Message[]
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
model ConversationMember {
|
|
340
|
+
id String @id @default(uuid())
|
|
341
|
+
userId String
|
|
342
|
+
conversationId String
|
|
343
|
+
role ConversationRole @default(MEMBER)
|
|
344
|
+
joinedAt DateTime @default(now())
|
|
345
|
+
lastViewedAt DateTime? // When user last viewed this conversation
|
|
346
|
+
lastViewedMentionAt DateTime? // When user last viewed mentions in this conversation
|
|
347
|
+
|
|
348
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
349
|
+
conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
|
|
350
|
+
|
|
351
|
+
@@unique([userId, conversationId])
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
model Message {
|
|
355
|
+
id String @id @default(uuid())
|
|
356
|
+
content String
|
|
357
|
+
senderId String
|
|
358
|
+
conversationId String
|
|
359
|
+
createdAt DateTime @default(now())
|
|
360
|
+
|
|
361
|
+
sender User @relation("SentMessages", fields: [senderId], references: [id], onDelete: Cascade)
|
|
362
|
+
conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
|
|
363
|
+
mentions Mention[]
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
model Mention {
|
|
367
|
+
id String @id @default(uuid())
|
|
368
|
+
messageId String
|
|
369
|
+
userId String
|
|
370
|
+
createdAt DateTime @default(now())
|
|
371
|
+
|
|
372
|
+
message Message @relation(fields: [messageId], references: [id], onDelete: Cascade)
|
|
373
|
+
user User @relation("UserMentions", fields: [userId], references: [id], onDelete: Cascade)
|
|
374
|
+
|
|
375
|
+
@@unique([messageId, userId])
|
|
376
|
+
}
|
package/src/routers/_app.ts
CHANGED
|
@@ -13,6 +13,8 @@ import { agendaRouter } from "./agenda.js";
|
|
|
13
13
|
import { fileRouter } from "./file.js";
|
|
14
14
|
import { folderRouter } from "./folder.js";
|
|
15
15
|
import { notificationRouter } from "./notifications.js";
|
|
16
|
+
import { conversationRouter } from "./conversation.js";
|
|
17
|
+
import { messageRouter } from "./message.js";
|
|
16
18
|
|
|
17
19
|
export const appRouter = createTRPCRouter({
|
|
18
20
|
class: classRouter,
|
|
@@ -27,6 +29,8 @@ export const appRouter = createTRPCRouter({
|
|
|
27
29
|
file: fileRouter,
|
|
28
30
|
folder: folderRouter,
|
|
29
31
|
notification: notificationRouter,
|
|
32
|
+
conversation: conversationRouter,
|
|
33
|
+
message: messageRouter,
|
|
30
34
|
});
|
|
31
35
|
|
|
32
36
|
// Export type router type definition
|
package/src/routers/auth.ts
CHANGED
package/src/routers/class.ts
CHANGED
|
@@ -54,6 +54,7 @@ export const classRouter = createTRPCRouter({
|
|
|
54
54
|
id: true,
|
|
55
55
|
title: true,
|
|
56
56
|
type: true,
|
|
57
|
+
dueDate: true,
|
|
57
58
|
},
|
|
58
59
|
},
|
|
59
60
|
},
|
|
@@ -547,6 +548,18 @@ export const classRouter = createTRPCRouter({
|
|
|
547
548
|
title: true,
|
|
548
549
|
maxGrade: true,
|
|
549
550
|
weight: true,
|
|
551
|
+
markSchemeId: true,
|
|
552
|
+
markScheme: {
|
|
553
|
+
select: {
|
|
554
|
+
structured: true,
|
|
555
|
+
}
|
|
556
|
+
},
|
|
557
|
+
gradingBoundaryId: true,
|
|
558
|
+
gradingBoundary: {
|
|
559
|
+
select: {
|
|
560
|
+
structured: true,
|
|
561
|
+
}
|
|
562
|
+
},
|
|
550
563
|
}
|
|
551
564
|
},
|
|
552
565
|
}
|