@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.
- package/CHAT_API_SPEC.md +579 -0
- package/dist/index.js +68 -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 +548 -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 +2 -0
- package/dist/routers/class.d.ts.map +1 -1
- package/dist/routers/class.js +1 -0
- package/dist/routers/conversation.d.ts +134 -0
- package/dist/routers/conversation.d.ts.map +1 -0
- package/dist/routers/conversation.js +267 -0
- package/dist/routers/message.d.ts +142 -0
- package/dist/routers/message.d.ts.map +1 -0
- package/dist/routers/message.js +531 -0
- package/dist/routers/user.d.ts.map +1 -1
- package/dist/routers/user.js +5 -4
- 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/index.ts +79 -0
- 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 +1 -0
- package/src/routers/conversation.ts +292 -0
- package/src/routers/message.ts +596 -0
- package/src/routers/user.ts +5 -3
|
@@ -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/index.ts
CHANGED
|
@@ -22,6 +22,8 @@ app.use(cors({
|
|
|
22
22
|
'http://localhost:3001', // Server port
|
|
23
23
|
'http://127.0.0.1:3000', // Alternative localhost
|
|
24
24
|
'http://127.0.0.1:3001', // Alternative localhost
|
|
25
|
+
'https://www.studious.sh', // Production frontend
|
|
26
|
+
'https://studious.sh', // Production frontend (without www)
|
|
25
27
|
process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
|
|
26
28
|
],
|
|
27
29
|
credentials: true,
|
|
@@ -37,6 +39,8 @@ app.options('*', (req, res) => {
|
|
|
37
39
|
'http://localhost:3001',
|
|
38
40
|
'http://127.0.0.1:3000',
|
|
39
41
|
'http://127.0.0.1:3001',
|
|
42
|
+
'https://www.studious.sh', // Production frontend
|
|
43
|
+
'https://studious.sh', // Production frontend (without www)
|
|
40
44
|
process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
|
|
41
45
|
];
|
|
42
46
|
|
|
@@ -92,6 +96,8 @@ const io = new Server(httpServer, {
|
|
|
92
96
|
'http://localhost:3001', // Server port
|
|
93
97
|
'http://127.0.0.1:3000', // Alternative localhost
|
|
94
98
|
'http://127.0.0.1:3001', // Alternative localhost
|
|
99
|
+
'https://www.studious.sh', // Production frontend
|
|
100
|
+
'https://studious.sh', // Production frontend (without www)
|
|
95
101
|
process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
|
|
96
102
|
],
|
|
97
103
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
|
@@ -158,6 +164,79 @@ app.get('/api/files/:filePath', async (req, res) => {
|
|
|
158
164
|
}
|
|
159
165
|
});
|
|
160
166
|
|
|
167
|
+
// File upload endpoint for secure file uploads (supports both POST and PUT)
|
|
168
|
+
app.post('/api/upload/:filePath', async (req, res) => {
|
|
169
|
+
handleFileUpload(req, res);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
app.put('/api/upload/:filePath', async (req, res) => {
|
|
173
|
+
handleFileUpload(req, res);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
function handleFileUpload(req: any, res: any) {
|
|
177
|
+
try {
|
|
178
|
+
const filePath = decodeURIComponent(req.params.filePath);
|
|
179
|
+
console.log('File upload request:', { filePath, originalPath: req.params.filePath, method: req.method });
|
|
180
|
+
|
|
181
|
+
// Set CORS headers for upload endpoint
|
|
182
|
+
const origin = req.headers.origin;
|
|
183
|
+
const allowedOrigins = [
|
|
184
|
+
'http://localhost:3000',
|
|
185
|
+
'http://localhost:3001',
|
|
186
|
+
'http://127.0.0.1:3000',
|
|
187
|
+
'http://127.0.0.1:3001',
|
|
188
|
+
'https://www.studious.sh', // Production frontend
|
|
189
|
+
'https://studious.sh', // Production frontend (without www)
|
|
190
|
+
process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
if (origin && allowedOrigins.includes(origin)) {
|
|
194
|
+
res.header('Access-Control-Allow-Origin', origin);
|
|
195
|
+
} else {
|
|
196
|
+
res.header('Access-Control-Allow-Origin', 'http://localhost:3000');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
res.header('Access-Control-Allow-Credentials', 'true');
|
|
200
|
+
|
|
201
|
+
// Get content type from headers
|
|
202
|
+
const contentType = req.headers['content-type'] || 'application/octet-stream';
|
|
203
|
+
|
|
204
|
+
// Create a new file in the bucket
|
|
205
|
+
const file = bucket.file(filePath);
|
|
206
|
+
|
|
207
|
+
// Create a write stream to Google Cloud Storage
|
|
208
|
+
const writeStream = file.createWriteStream({
|
|
209
|
+
metadata: {
|
|
210
|
+
contentType,
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Handle stream events
|
|
215
|
+
writeStream.on('error', (error) => {
|
|
216
|
+
console.error('Error uploading file:', error);
|
|
217
|
+
if (!res.headersSent) {
|
|
218
|
+
res.status(500).json({ error: 'Error uploading file' });
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
writeStream.on('finish', () => {
|
|
223
|
+
console.log('File uploaded successfully:', filePath);
|
|
224
|
+
res.status(200).json({
|
|
225
|
+
success: true,
|
|
226
|
+
filePath,
|
|
227
|
+
message: 'File uploaded successfully'
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Pipe the request body to the write stream
|
|
232
|
+
req.pipe(writeStream);
|
|
233
|
+
|
|
234
|
+
} catch (error) {
|
|
235
|
+
console.error('Error handling file upload:', error);
|
|
236
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
161
240
|
// Create caller
|
|
162
241
|
const createCaller = createCallerFactory(appRouter);
|
|
163
242
|
|
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
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { createTRPCRouter, protectedProcedure } from '../trpc.js';
|
|
3
|
+
import { prisma } from '../lib/prisma.js';
|
|
4
|
+
import { TRPCError } from '@trpc/server';
|
|
5
|
+
|
|
6
|
+
export const conversationRouter = createTRPCRouter({
|
|
7
|
+
list: protectedProcedure.query(async ({ ctx }) => {
|
|
8
|
+
const userId = ctx.user!.id;
|
|
9
|
+
|
|
10
|
+
const conversations = await prisma.conversation.findMany({
|
|
11
|
+
where: {
|
|
12
|
+
members: {
|
|
13
|
+
some: {
|
|
14
|
+
userId,
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
include: {
|
|
19
|
+
members: {
|
|
20
|
+
include: {
|
|
21
|
+
user: {
|
|
22
|
+
select: {
|
|
23
|
+
id: true,
|
|
24
|
+
username: true,
|
|
25
|
+
profile: {
|
|
26
|
+
select: {
|
|
27
|
+
displayName: true,
|
|
28
|
+
profilePicture: true,
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
messages: {
|
|
36
|
+
orderBy: {
|
|
37
|
+
createdAt: 'desc',
|
|
38
|
+
},
|
|
39
|
+
take: 1,
|
|
40
|
+
include: {
|
|
41
|
+
sender: {
|
|
42
|
+
select: {
|
|
43
|
+
id: true,
|
|
44
|
+
username: true,
|
|
45
|
+
profile: {
|
|
46
|
+
select: {
|
|
47
|
+
displayName: true,
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
orderBy: {
|
|
56
|
+
updatedAt: 'desc',
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Calculate unread counts for each conversation
|
|
61
|
+
const conversationsWithUnread = await Promise.all(
|
|
62
|
+
conversations.map(async (conversation) => {
|
|
63
|
+
const userMembership = conversation.members.find(m => m.userId === userId);
|
|
64
|
+
const lastViewedAt = userMembership?.lastViewedAt;
|
|
65
|
+
const lastViewedMentionAt = userMembership?.lastViewedMentionAt;
|
|
66
|
+
|
|
67
|
+
// Count regular unread messages
|
|
68
|
+
const unreadCount = await prisma.message.count({
|
|
69
|
+
where: {
|
|
70
|
+
conversationId: conversation.id,
|
|
71
|
+
senderId: { not: userId },
|
|
72
|
+
...(lastViewedAt && {
|
|
73
|
+
createdAt: { gt: lastViewedAt }
|
|
74
|
+
}),
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Count unread mentions
|
|
79
|
+
// Use the later of lastViewedAt or lastViewedMentionAt
|
|
80
|
+
// This means if user viewed conversation after mention, mention is considered read
|
|
81
|
+
const mentionCutoffTime = lastViewedMentionAt && lastViewedAt
|
|
82
|
+
? (lastViewedMentionAt > lastViewedAt ? lastViewedMentionAt : lastViewedAt)
|
|
83
|
+
: (lastViewedMentionAt || lastViewedAt);
|
|
84
|
+
|
|
85
|
+
const unreadMentionCount = await prisma.mention.count({
|
|
86
|
+
where: {
|
|
87
|
+
userId,
|
|
88
|
+
message: {
|
|
89
|
+
conversationId: conversation.id,
|
|
90
|
+
senderId: { not: userId },
|
|
91
|
+
...(mentionCutoffTime && {
|
|
92
|
+
createdAt: { gt: mentionCutoffTime }
|
|
93
|
+
}),
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
id: conversation.id,
|
|
100
|
+
type: conversation.type,
|
|
101
|
+
name: conversation.name,
|
|
102
|
+
createdAt: conversation.createdAt,
|
|
103
|
+
updatedAt: conversation.updatedAt,
|
|
104
|
+
members: conversation.members,
|
|
105
|
+
lastMessage: conversation.messages[0] || null,
|
|
106
|
+
unreadCount,
|
|
107
|
+
unreadMentionCount,
|
|
108
|
+
};
|
|
109
|
+
})
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
return conversationsWithUnread;
|
|
113
|
+
}),
|
|
114
|
+
|
|
115
|
+
create: protectedProcedure
|
|
116
|
+
.input(
|
|
117
|
+
z.object({
|
|
118
|
+
type: z.enum(['DM', 'GROUP']),
|
|
119
|
+
name: z.string().optional(),
|
|
120
|
+
memberIds: z.array(z.string()),
|
|
121
|
+
})
|
|
122
|
+
)
|
|
123
|
+
.mutation(async ({ input, ctx }) => {
|
|
124
|
+
const userId = ctx.user!.id;
|
|
125
|
+
const { type, name, memberIds } = input;
|
|
126
|
+
|
|
127
|
+
// Validate input
|
|
128
|
+
if (type === 'GROUP' && !name) {
|
|
129
|
+
throw new TRPCError({
|
|
130
|
+
code: 'BAD_REQUEST',
|
|
131
|
+
message: 'Group conversations must have a name',
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (type === 'DM' && memberIds.length !== 1) {
|
|
136
|
+
throw new TRPCError({
|
|
137
|
+
code: 'BAD_REQUEST',
|
|
138
|
+
message: 'DM conversations must have exactly one other member',
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// For DMs, check if conversation already exists
|
|
143
|
+
if (type === 'DM') {
|
|
144
|
+
const existingDM = await prisma.conversation.findFirst({
|
|
145
|
+
where: {
|
|
146
|
+
type: 'DM',
|
|
147
|
+
members: {
|
|
148
|
+
every: {
|
|
149
|
+
userId: {
|
|
150
|
+
in: [userId, memberIds[0]],
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
AND: {
|
|
155
|
+
members: {
|
|
156
|
+
some: {
|
|
157
|
+
userId,
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
include: {
|
|
163
|
+
members: {
|
|
164
|
+
include: {
|
|
165
|
+
user: {
|
|
166
|
+
select: {
|
|
167
|
+
id: true,
|
|
168
|
+
username: true,
|
|
169
|
+
profile: {
|
|
170
|
+
select: {
|
|
171
|
+
displayName: true,
|
|
172
|
+
profilePicture: true,
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
if (existingDM) {
|
|
183
|
+
return existingDM;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Verify all members exist
|
|
188
|
+
const members = await prisma.user.findMany({
|
|
189
|
+
where: {
|
|
190
|
+
username: {
|
|
191
|
+
in: memberIds,
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
select: {
|
|
195
|
+
id: true,
|
|
196
|
+
username: true,
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
if (members.length !== memberIds.length) {
|
|
201
|
+
throw new TRPCError({
|
|
202
|
+
code: 'BAD_REQUEST',
|
|
203
|
+
message: 'One or more members not found',
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Create conversation with members
|
|
208
|
+
const conversation = await prisma.conversation.create({
|
|
209
|
+
data: {
|
|
210
|
+
type,
|
|
211
|
+
name,
|
|
212
|
+
members: {
|
|
213
|
+
create: [
|
|
214
|
+
{
|
|
215
|
+
userId,
|
|
216
|
+
role: type === 'GROUP' ? 'ADMIN' : 'MEMBER',
|
|
217
|
+
},
|
|
218
|
+
...memberIds.map((memberId) => ({
|
|
219
|
+
userId: members.find((member) => member.username === memberId)!.id,
|
|
220
|
+
role: 'MEMBER' as const,
|
|
221
|
+
})),
|
|
222
|
+
],
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
include: {
|
|
226
|
+
members: {
|
|
227
|
+
include: {
|
|
228
|
+
user: {
|
|
229
|
+
select: {
|
|
230
|
+
id: true,
|
|
231
|
+
username: true,
|
|
232
|
+
profile: {
|
|
233
|
+
select: {
|
|
234
|
+
displayName: true,
|
|
235
|
+
profilePicture: true,
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
return conversation;
|
|
246
|
+
}),
|
|
247
|
+
|
|
248
|
+
get: protectedProcedure
|
|
249
|
+
.input(z.object({ conversationId: z.string() }))
|
|
250
|
+
.query(async ({ input, ctx }) => {
|
|
251
|
+
const userId = ctx.user!.id;
|
|
252
|
+
const { conversationId } = input;
|
|
253
|
+
|
|
254
|
+
const conversation = await prisma.conversation.findFirst({
|
|
255
|
+
where: {
|
|
256
|
+
id: conversationId,
|
|
257
|
+
members: {
|
|
258
|
+
some: {
|
|
259
|
+
userId,
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
include: {
|
|
264
|
+
members: {
|
|
265
|
+
include: {
|
|
266
|
+
user: {
|
|
267
|
+
select: {
|
|
268
|
+
id: true,
|
|
269
|
+
username: true,
|
|
270
|
+
profile: {
|
|
271
|
+
select: {
|
|
272
|
+
displayName: true,
|
|
273
|
+
profilePicture: true,
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
if (!conversation) {
|
|
284
|
+
throw new TRPCError({
|
|
285
|
+
code: 'NOT_FOUND',
|
|
286
|
+
message: 'Conversation not found or access denied',
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return conversation;
|
|
291
|
+
}),
|
|
292
|
+
});
|