@studious-lms/server 1.4.4 → 1.4.5

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.
Files changed (52) hide show
  1. package/dist/index.js +3 -17
  2. package/dist/index.js.map +1 -1
  3. package/dist/models/conversation.d.ts +22 -0
  4. package/dist/models/conversation.d.ts.map +1 -1
  5. package/dist/models/conversation.js +13 -2
  6. package/dist/models/conversation.js.map +1 -1
  7. package/dist/models/labChat.d.ts +3 -0
  8. package/dist/models/labChat.d.ts.map +1 -1
  9. package/dist/models/message.d.ts +2 -0
  10. package/dist/models/message.d.ts.map +1 -1
  11. package/dist/models/newtonChat.d.ts +1 -0
  12. package/dist/models/newtonChat.d.ts.map +1 -1
  13. package/dist/routers/_app.d.ts +110 -0
  14. package/dist/routers/_app.d.ts.map +1 -1
  15. package/dist/routers/_app.js +4 -2
  16. package/dist/routers/_app.js.map +1 -1
  17. package/dist/routers/conversation.d.ts +14 -0
  18. package/dist/routers/conversation.d.ts.map +1 -1
  19. package/dist/routers/conversation.js +6 -3
  20. package/dist/routers/conversation.js.map +1 -1
  21. package/dist/routers/labChat.d.ts +2 -0
  22. package/dist/routers/labChat.d.ts.map +1 -1
  23. package/dist/routers/search.d.ts +41 -0
  24. package/dist/routers/search.d.ts.map +1 -0
  25. package/dist/routers/search.js +15 -0
  26. package/dist/routers/search.js.map +1 -0
  27. package/dist/services/conversation.d.ts +8 -0
  28. package/dist/services/conversation.d.ts.map +1 -1
  29. package/dist/services/conversation.js +24 -6
  30. package/dist/services/conversation.js.map +1 -1
  31. package/dist/services/labChat.d.ts +2 -0
  32. package/dist/services/labChat.d.ts.map +1 -1
  33. package/dist/services/message.d.ts.map +1 -1
  34. package/dist/services/message.js +9 -2
  35. package/dist/services/message.js.map +1 -1
  36. package/dist/services/search.d.ts +21 -0
  37. package/dist/services/search.d.ts.map +1 -0
  38. package/dist/services/search.js +352 -0
  39. package/dist/services/search.js.map +1 -0
  40. package/package.json +1 -1
  41. package/prisma/migrations/20260417083000_add_conversation_member_hidden_at/migration.sql +2 -0
  42. package/prisma/schema.prisma +1 -0
  43. package/src/index.ts +1 -17
  44. package/src/models/conversation.ts +12 -0
  45. package/src/routers/_app.ts +2 -0
  46. package/src/routers/conversation.ts +7 -0
  47. package/src/routers/search.ts +14 -0
  48. package/src/services/conversation.ts +27 -3
  49. package/src/services/message.ts +8 -0
  50. package/src/services/search.ts +397 -0
  51. package/tests/routers/conversation.test.ts +114 -2
  52. package/tests/routers/search.test.ts +109 -0
@@ -0,0 +1,352 @@
1
+
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="9858c6c3-883b-538a-9404-7b225e24d88e")}catch(e){}}();
3
+ import { prisma } from "../lib/prisma.js";
4
+ const SEARCH_LIMIT = 12;
5
+ const contains = (query) => ({
6
+ contains: query,
7
+ mode: "insensitive",
8
+ });
9
+ const includesToken = (query, ...tokens) => tokens.some((token) => token.includes(query.toLowerCase()));
10
+ const excerpt = (value, query) => {
11
+ if (!value)
12
+ return null;
13
+ const trimmed = value.replace(/\s+/g, " ").trim();
14
+ if (!trimmed)
15
+ return null;
16
+ if (!query)
17
+ return trimmed.slice(0, 140);
18
+ const index = trimmed.toLowerCase().indexOf(query.toLowerCase());
19
+ if (index < 0)
20
+ return trimmed.slice(0, 140);
21
+ const start = Math.max(0, index - 48);
22
+ const end = Math.min(trimmed.length, index + query.length + 92);
23
+ return `${start > 0 ? "..." : ""}${trimmed.slice(start, end)}${end < trimmed.length ? "..." : ""}`;
24
+ };
25
+ const trimResults = (results, limit) => results
26
+ .sort((a, b) => {
27
+ const priority = {
28
+ class: 0,
29
+ assignment: 1,
30
+ event: 2,
31
+ syllabus: 3,
32
+ worksheet: 4,
33
+ file: 5,
34
+ announcement: 6,
35
+ section: 7,
36
+ };
37
+ const priorityDiff = priority[a.type] - priority[b.type];
38
+ if (priorityDiff !== 0)
39
+ return priorityDiff;
40
+ return (b.date?.getTime() ?? 0) - (a.date?.getTime() ?? 0);
41
+ })
42
+ .slice(0, limit);
43
+ export async function searchGlobalClasses(userId, input) {
44
+ const rawQuery = input.query?.trim() ?? "";
45
+ const query = rawQuery.slice(0, 80);
46
+ const limit = Math.min(Math.max(input.limit ?? SEARCH_LIMIT, 1), 30);
47
+ const memberships = await prisma.class.findMany({
48
+ where: {
49
+ OR: [
50
+ { teachers: { some: { id: userId } } },
51
+ { students: { some: { id: userId } } },
52
+ ],
53
+ },
54
+ select: {
55
+ id: true,
56
+ name: true,
57
+ section: true,
58
+ subject: true,
59
+ syllabus: true,
60
+ teachers: { where: { id: userId }, select: { id: true } },
61
+ students: { where: { id: userId }, select: { id: true } },
62
+ },
63
+ });
64
+ const allClassIds = memberships.map((cls) => cls.id);
65
+ if (allClassIds.length === 0) {
66
+ return { query, results: [] };
67
+ }
68
+ const teacherClassIds = memberships
69
+ .filter((cls) => cls.teachers.length > 0)
70
+ .map((cls) => cls.id);
71
+ const studentClassIds = memberships
72
+ .filter((cls) => cls.students.length > 0 && cls.teachers.length === 0)
73
+ .map((cls) => cls.id);
74
+ const classById = new Map(memberships.map((cls) => [cls.id, cls]));
75
+ const queryFilters = query
76
+ ? {
77
+ class: [
78
+ { name: contains(query) },
79
+ { section: contains(query) },
80
+ { subject: contains(query) },
81
+ ],
82
+ assignment: [
83
+ { title: contains(query) },
84
+ { instructions: contains(query) },
85
+ ],
86
+ event: [
87
+ { name: contains(query) },
88
+ { location: contains(query) },
89
+ { remarks: contains(query) },
90
+ ],
91
+ worksheet: [{ name: contains(query) }],
92
+ file: [{ name: contains(query) }, { type: contains(query) }],
93
+ announcement: [{ remarks: contains(query) }],
94
+ section: [{ name: contains(query) }],
95
+ }
96
+ : null;
97
+ const assignmentAccess = [
98
+ ...(teacherClassIds.length > 0
99
+ ? [{ classId: { in: teacherClassIds } }]
100
+ : []),
101
+ ...(studentClassIds.length > 0
102
+ ? [
103
+ {
104
+ classId: { in: studentClassIds },
105
+ OR: [
106
+ { assignedTo: { some: { id: userId } } },
107
+ { assignedTo: { none: {} } },
108
+ ],
109
+ },
110
+ ]
111
+ : []),
112
+ ];
113
+ const [classes, assignments, events, worksheets, files, announcements, sections,] = await Promise.all([
114
+ prisma.class.findMany({
115
+ where: {
116
+ id: { in: allClassIds },
117
+ ...(queryFilters ? { OR: queryFilters.class } : {}),
118
+ },
119
+ select: { id: true, name: true, section: true, subject: true },
120
+ take: limit,
121
+ }),
122
+ prisma.assignment.findMany({
123
+ where: {
124
+ template: false,
125
+ AND: [
126
+ ...(queryFilters ? [{ OR: queryFilters.assignment }] : []),
127
+ ...(assignmentAccess.length > 0 ? [{ OR: assignmentAccess }] : []),
128
+ ],
129
+ },
130
+ select: {
131
+ id: true,
132
+ title: true,
133
+ instructions: true,
134
+ dueDate: true,
135
+ modifiedAt: true,
136
+ classId: true,
137
+ class: { select: { name: true } },
138
+ },
139
+ orderBy: { dueDate: "asc" },
140
+ take: limit,
141
+ }),
142
+ prisma.event.findMany({
143
+ where: {
144
+ classId: { in: allClassIds },
145
+ ...(queryFilters ? { OR: queryFilters.event } : {}),
146
+ },
147
+ select: {
148
+ id: true,
149
+ name: true,
150
+ location: true,
151
+ remarks: true,
152
+ startTime: true,
153
+ classId: true,
154
+ class: { select: { name: true } },
155
+ },
156
+ orderBy: { startTime: "asc" },
157
+ take: limit,
158
+ }),
159
+ prisma.worksheet.findMany({
160
+ where: {
161
+ classId: { in: allClassIds },
162
+ ...(queryFilters && !includesToken(query, "worksheet", "worksheets")
163
+ ? { OR: queryFilters.worksheet }
164
+ : {}),
165
+ },
166
+ select: {
167
+ id: true,
168
+ name: true,
169
+ updatedAt: true,
170
+ classId: true,
171
+ class: { select: { name: true } },
172
+ },
173
+ orderBy: { updatedAt: "desc" },
174
+ take: limit,
175
+ }),
176
+ prisma.file.findMany({
177
+ where: {
178
+ ...(queryFilters ? { OR: queryFilters.file } : {}),
179
+ AND: [
180
+ {
181
+ OR: [
182
+ { folder: { classId: { in: allClassIds } } },
183
+ { classDraftId: { in: teacherClassIds } },
184
+ {
185
+ assignment: {
186
+ OR: assignmentAccess,
187
+ },
188
+ },
189
+ { announcement: { classId: { in: allClassIds } } },
190
+ ],
191
+ },
192
+ ],
193
+ },
194
+ select: {
195
+ id: true,
196
+ name: true,
197
+ type: true,
198
+ uploadedAt: true,
199
+ folder: { select: { classId: true, class: { select: { name: true } } } },
200
+ classDraft: { select: { id: true, name: true } },
201
+ assignment: { select: { classId: true, class: { select: { name: true } } } },
202
+ announcement: { select: { classId: true, class: { select: { name: true } } } },
203
+ },
204
+ orderBy: { uploadedAt: "desc" },
205
+ take: limit,
206
+ }),
207
+ prisma.announcement.findMany({
208
+ where: {
209
+ classId: { in: allClassIds },
210
+ ...(queryFilters ? { OR: queryFilters.announcement } : {}),
211
+ },
212
+ select: {
213
+ id: true,
214
+ remarks: true,
215
+ createdAt: true,
216
+ classId: true,
217
+ class: { select: { name: true } },
218
+ },
219
+ orderBy: { createdAt: "desc" },
220
+ take: limit,
221
+ }),
222
+ prisma.section.findMany({
223
+ where: {
224
+ classId: { in: allClassIds },
225
+ ...(queryFilters && !includesToken(query, "section", "sections", "assignment", "assignments")
226
+ ? { OR: queryFilters.section }
227
+ : {}),
228
+ },
229
+ select: {
230
+ id: true,
231
+ name: true,
232
+ classId: true,
233
+ class: { select: { name: true } },
234
+ },
235
+ take: limit,
236
+ }),
237
+ ]);
238
+ const results = [
239
+ ...classes.map((cls) => ({
240
+ id: cls.id,
241
+ type: "class",
242
+ title: cls.name,
243
+ subtitle: [cls.subject, cls.section].filter(Boolean).join(" · "),
244
+ classId: cls.id,
245
+ className: cls.name,
246
+ href: `/class/${cls.id}`,
247
+ })),
248
+ ...assignments.map((assignment) => ({
249
+ id: assignment.id,
250
+ type: "assignment",
251
+ title: assignment.title,
252
+ subtitle: `${assignment.class.name} · Assignment`,
253
+ classId: assignment.classId,
254
+ className: assignment.class.name,
255
+ href: `/class/${assignment.classId}/assignment/${assignment.id}`,
256
+ matchedText: excerpt(assignment.instructions, query),
257
+ date: assignment.dueDate ?? assignment.modifiedAt,
258
+ })),
259
+ ...events.map((event) => ({
260
+ id: event.id,
261
+ type: "event",
262
+ title: event.name || "Untitled event",
263
+ subtitle: `${event.class?.name ?? "Class"} · Event`,
264
+ classId: event.classId,
265
+ className: event.class?.name ?? "Class",
266
+ href: `/class/${event.classId}/attendance/${event.id}`,
267
+ matchedText: excerpt(event.location || event.remarks, query),
268
+ date: event.startTime,
269
+ })),
270
+ ...memberships
271
+ .filter((cls) => {
272
+ if (!cls.syllabus)
273
+ return false;
274
+ if (!query)
275
+ return true;
276
+ return (includesToken(query, "syllabus") ||
277
+ cls.name.toLowerCase().includes(query.toLowerCase()) ||
278
+ cls.subject.toLowerCase().includes(query.toLowerCase()) ||
279
+ cls.syllabus.toLowerCase().includes(query.toLowerCase()));
280
+ })
281
+ .slice(0, limit)
282
+ .map((cls) => ({
283
+ id: `${cls.id}:syllabus`,
284
+ type: "syllabus",
285
+ title: `${cls.name} syllabus`,
286
+ subtitle: `${cls.subject} · Syllabus`,
287
+ classId: cls.id,
288
+ className: cls.name,
289
+ href: `/class/${cls.id}/syllabus`,
290
+ matchedText: excerpt(cls.syllabus, query),
291
+ })),
292
+ ...worksheets.map((worksheet) => ({
293
+ id: worksheet.id,
294
+ type: "worksheet",
295
+ title: worksheet.name,
296
+ subtitle: `${worksheet.class?.name ?? "Class"} · Worksheet`,
297
+ classId: worksheet.classId,
298
+ className: worksheet.class?.name ?? "Class",
299
+ href: `/class/${worksheet.classId}/worksheets`,
300
+ date: worksheet.updatedAt,
301
+ })),
302
+ ...files.flatMap((file) => {
303
+ const classId = file.folder?.classId ??
304
+ file.classDraft?.id ??
305
+ file.assignment?.classId ??
306
+ file.announcement?.classId;
307
+ const className = file.folder?.class?.name ??
308
+ file.classDraft?.name ??
309
+ file.assignment?.class?.name ??
310
+ file.announcement?.class?.name ??
311
+ (classId ? classById.get(classId)?.name : undefined);
312
+ if (!classId || !className)
313
+ return [];
314
+ return [
315
+ {
316
+ id: file.id,
317
+ type: "file",
318
+ title: file.name,
319
+ subtitle: `${className} · File`,
320
+ classId,
321
+ className,
322
+ href: `/class/${classId}/files`,
323
+ matchedText: file.type,
324
+ date: file.uploadedAt,
325
+ },
326
+ ];
327
+ }),
328
+ ...announcements.map((announcement) => ({
329
+ id: announcement.id,
330
+ type: "announcement",
331
+ title: excerpt(announcement.remarks, query) ?? "Announcement",
332
+ subtitle: `${announcement.class.name} · Announcement`,
333
+ classId: announcement.classId,
334
+ className: announcement.class.name,
335
+ href: `/class/${announcement.classId}`,
336
+ matchedText: excerpt(announcement.remarks, query),
337
+ date: announcement.createdAt,
338
+ })),
339
+ ...sections.map((section) => ({
340
+ id: section.id,
341
+ type: "section",
342
+ title: section.name,
343
+ subtitle: `${section.class.name} · Section`,
344
+ classId: section.classId,
345
+ className: section.class.name,
346
+ href: `/class/${section.classId}/assignments`,
347
+ })),
348
+ ];
349
+ return { query, results: trimResults(results, limit) };
350
+ }
351
+ //# sourceMappingURL=search.js.map
352
+ //# debugId=9858c6c3-883b-538a-9404-7b225e24d88e
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search.js","sources":["services/search.ts"],"sourceRoot":"/","sourcesContent":["import { prisma } from \"../lib/prisma.js\";\n\ntype SearchScope =\n | \"class\"\n | \"assignment\"\n | \"event\"\n | \"syllabus\"\n | \"worksheet\"\n | \"file\"\n | \"announcement\"\n | \"section\";\n\ntype SearchResult = {\n id: string;\n type: SearchScope;\n title: string;\n subtitle: string;\n classId: string;\n className: string;\n href: string;\n matchedText?: string | null;\n date?: Date | null;\n};\n\nconst SEARCH_LIMIT = 12;\n\nconst contains = (query: string) => ({\n contains: query,\n mode: \"insensitive\" as const,\n});\n\nconst includesToken = (query: string, ...tokens: string[]) =>\n tokens.some((token) => token.includes(query.toLowerCase()));\n\nconst excerpt = (value: string | null | undefined, query: string) => {\n if (!value) return null;\n const trimmed = value.replace(/\\s+/g, \" \").trim();\n if (!trimmed) return null;\n if (!query) return trimmed.slice(0, 140);\n\n const index = trimmed.toLowerCase().indexOf(query.toLowerCase());\n if (index < 0) return trimmed.slice(0, 140);\n\n const start = Math.max(0, index - 48);\n const end = Math.min(trimmed.length, index + query.length + 92);\n return `${start > 0 ? \"...\" : \"\"}${trimmed.slice(start, end)}${end < trimmed.length ? \"...\" : \"\"}`;\n};\n\nconst trimResults = (results: SearchResult[], limit: number) =>\n results\n .sort((a, b) => {\n const priority: Record<SearchScope, number> = {\n class: 0,\n assignment: 1,\n event: 2,\n syllabus: 3,\n worksheet: 4,\n file: 5,\n announcement: 6,\n section: 7,\n };\n const priorityDiff = priority[a.type] - priority[b.type];\n if (priorityDiff !== 0) return priorityDiff;\n return (b.date?.getTime() ?? 0) - (a.date?.getTime() ?? 0);\n })\n .slice(0, limit);\n\nexport async function searchGlobalClasses(\n userId: string,\n input: { query?: string; limit?: number }\n) {\n const rawQuery = input.query?.trim() ?? \"\";\n const query = rawQuery.slice(0, 80);\n const limit = Math.min(Math.max(input.limit ?? SEARCH_LIMIT, 1), 30);\n\n const memberships = await prisma.class.findMany({\n where: {\n OR: [\n { teachers: { some: { id: userId } } },\n { students: { some: { id: userId } } },\n ],\n },\n select: {\n id: true,\n name: true,\n section: true,\n subject: true,\n syllabus: true,\n teachers: { where: { id: userId }, select: { id: true } },\n students: { where: { id: userId }, select: { id: true } },\n },\n });\n\n const allClassIds = memberships.map((cls) => cls.id);\n if (allClassIds.length === 0) {\n return { query, results: [] as SearchResult[] };\n }\n\n const teacherClassIds = memberships\n .filter((cls) => cls.teachers.length > 0)\n .map((cls) => cls.id);\n const studentClassIds = memberships\n .filter((cls) => cls.students.length > 0 && cls.teachers.length === 0)\n .map((cls) => cls.id);\n const classById = new Map(memberships.map((cls) => [cls.id, cls]));\n\n const queryFilters = query\n ? {\n class: [\n { name: contains(query) },\n { section: contains(query) },\n { subject: contains(query) },\n ],\n assignment: [\n { title: contains(query) },\n { instructions: contains(query) },\n ],\n event: [\n { name: contains(query) },\n { location: contains(query) },\n { remarks: contains(query) },\n ],\n worksheet: [{ name: contains(query) }],\n file: [{ name: contains(query) }, { type: contains(query) }],\n announcement: [{ remarks: contains(query) }],\n section: [{ name: contains(query) }],\n }\n : null;\n\n const assignmentAccess = [\n ...(teacherClassIds.length > 0\n ? [{ classId: { in: teacherClassIds } }]\n : []),\n ...(studentClassIds.length > 0\n ? [\n {\n classId: { in: studentClassIds },\n OR: [\n { assignedTo: { some: { id: userId } } },\n { assignedTo: { none: {} } },\n ],\n },\n ]\n : []),\n ];\n\n const [\n classes,\n assignments,\n events,\n worksheets,\n files,\n announcements,\n sections,\n ] = await Promise.all([\n prisma.class.findMany({\n where: {\n id: { in: allClassIds },\n ...(queryFilters ? { OR: queryFilters.class } : {}),\n },\n select: { id: true, name: true, section: true, subject: true },\n take: limit,\n }),\n prisma.assignment.findMany({\n where: {\n template: false,\n AND: [\n ...(queryFilters ? [{ OR: queryFilters.assignment }] : []),\n ...(assignmentAccess.length > 0 ? [{ OR: assignmentAccess }] : []),\n ],\n },\n select: {\n id: true,\n title: true,\n instructions: true,\n dueDate: true,\n modifiedAt: true,\n classId: true,\n class: { select: { name: true } },\n },\n orderBy: { dueDate: \"asc\" },\n take: limit,\n }),\n prisma.event.findMany({\n where: {\n classId: { in: allClassIds },\n ...(queryFilters ? { OR: queryFilters.event } : {}),\n },\n select: {\n id: true,\n name: true,\n location: true,\n remarks: true,\n startTime: true,\n classId: true,\n class: { select: { name: true } },\n },\n orderBy: { startTime: \"asc\" },\n take: limit,\n }),\n prisma.worksheet.findMany({\n where: {\n classId: { in: allClassIds },\n ...(queryFilters && !includesToken(query, \"worksheet\", \"worksheets\")\n ? { OR: queryFilters.worksheet }\n : {}),\n },\n select: {\n id: true,\n name: true,\n updatedAt: true,\n classId: true,\n class: { select: { name: true } },\n },\n orderBy: { updatedAt: \"desc\" },\n take: limit,\n }),\n prisma.file.findMany({\n where: {\n ...(queryFilters ? { OR: queryFilters.file } : {}),\n AND: [\n {\n OR: [\n { folder: { classId: { in: allClassIds } } },\n { classDraftId: { in: teacherClassIds } },\n {\n assignment: {\n OR: assignmentAccess,\n },\n },\n { announcement: { classId: { in: allClassIds } } },\n ],\n },\n ],\n },\n select: {\n id: true,\n name: true,\n type: true,\n uploadedAt: true,\n folder: { select: { classId: true, class: { select: { name: true } } } },\n classDraft: { select: { id: true, name: true } },\n assignment: { select: { classId: true, class: { select: { name: true } } } },\n announcement: { select: { classId: true, class: { select: { name: true } } } },\n },\n orderBy: { uploadedAt: \"desc\" },\n take: limit,\n }),\n prisma.announcement.findMany({\n where: {\n classId: { in: allClassIds },\n ...(queryFilters ? { OR: queryFilters.announcement } : {}),\n },\n select: {\n id: true,\n remarks: true,\n createdAt: true,\n classId: true,\n class: { select: { name: true } },\n },\n orderBy: { createdAt: \"desc\" },\n take: limit,\n }),\n prisma.section.findMany({\n where: {\n classId: { in: allClassIds },\n ...(queryFilters && !includesToken(query, \"section\", \"sections\", \"assignment\", \"assignments\")\n ? { OR: queryFilters.section }\n : {}),\n },\n select: {\n id: true,\n name: true,\n classId: true,\n class: { select: { name: true } },\n },\n take: limit,\n }),\n ]);\n\n const results: SearchResult[] = [\n ...classes.map((cls) => ({\n id: cls.id,\n type: \"class\" as const,\n title: cls.name,\n subtitle: [cls.subject, cls.section].filter(Boolean).join(\" · \"),\n classId: cls.id,\n className: cls.name,\n href: `/class/${cls.id}`,\n })),\n ...assignments.map((assignment) => ({\n id: assignment.id,\n type: \"assignment\" as const,\n title: assignment.title,\n subtitle: `${assignment.class.name} · Assignment`,\n classId: assignment.classId,\n className: assignment.class.name,\n href: `/class/${assignment.classId}/assignment/${assignment.id}`,\n matchedText: excerpt(assignment.instructions, query),\n date: assignment.dueDate ?? assignment.modifiedAt,\n })),\n ...events.map((event) => ({\n id: event.id,\n type: \"event\" as const,\n title: event.name || \"Untitled event\",\n subtitle: `${event.class?.name ?? \"Class\"} · Event`,\n classId: event.classId!,\n className: event.class?.name ?? \"Class\",\n href: `/class/${event.classId}/attendance/${event.id}`,\n matchedText: excerpt(event.location || event.remarks, query),\n date: event.startTime,\n })),\n ...memberships\n .filter((cls) => {\n if (!cls.syllabus) return false;\n if (!query) return true;\n return (\n includesToken(query, \"syllabus\") ||\n cls.name.toLowerCase().includes(query.toLowerCase()) ||\n cls.subject.toLowerCase().includes(query.toLowerCase()) ||\n cls.syllabus.toLowerCase().includes(query.toLowerCase())\n );\n })\n .slice(0, limit)\n .map((cls) => ({\n id: `${cls.id}:syllabus`,\n type: \"syllabus\" as const,\n title: `${cls.name} syllabus`,\n subtitle: `${cls.subject} · Syllabus`,\n classId: cls.id,\n className: cls.name,\n href: `/class/${cls.id}/syllabus`,\n matchedText: excerpt(cls.syllabus, query),\n })),\n ...worksheets.map((worksheet) => ({\n id: worksheet.id,\n type: \"worksheet\" as const,\n title: worksheet.name,\n subtitle: `${worksheet.class?.name ?? \"Class\"} · Worksheet`,\n classId: worksheet.classId,\n className: worksheet.class?.name ?? \"Class\",\n href: `/class/${worksheet.classId}/worksheets`,\n date: worksheet.updatedAt,\n })),\n ...files.flatMap((file) => {\n const classId =\n file.folder?.classId ??\n file.classDraft?.id ??\n file.assignment?.classId ??\n file.announcement?.classId;\n const className =\n file.folder?.class?.name ??\n file.classDraft?.name ??\n file.assignment?.class?.name ??\n file.announcement?.class?.name ??\n (classId ? classById.get(classId)?.name : undefined);\n\n if (!classId || !className) return [];\n\n return [\n {\n id: file.id,\n type: \"file\" as const,\n title: file.name,\n subtitle: `${className} · File`,\n classId,\n className,\n href: `/class/${classId}/files`,\n matchedText: file.type,\n date: file.uploadedAt,\n },\n ];\n }),\n ...announcements.map((announcement) => ({\n id: announcement.id,\n type: \"announcement\" as const,\n title: excerpt(announcement.remarks, query) ?? \"Announcement\",\n subtitle: `${announcement.class.name} · Announcement`,\n classId: announcement.classId,\n className: announcement.class.name,\n href: `/class/${announcement.classId}`,\n matchedText: excerpt(announcement.remarks, query),\n date: announcement.createdAt,\n })),\n ...sections.map((section) => ({\n id: section.id,\n type: \"section\" as const,\n title: section.name,\n subtitle: `${section.class.name} · Section`,\n classId: section.classId,\n className: section.class.name,\n href: `/class/${section.classId}/assignments`,\n })),\n ];\n\n return { query, results: trimResults(results, limit) };\n}\n"],"names":[],"mappings":"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","debug_id":"9858c6c3-883b-538a-9404-7b225e24d88e"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@studious-lms/server",
3
- "version": "1.4.4",
3
+ "version": "1.4.5",
4
4
  "description": "Backend server for Studious application",
5
5
  "main": "dist/exportType.js",
6
6
  "types": "dist/exportType.d.ts",
@@ -0,0 +1,2 @@
1
+ ALTER TABLE "ConversationMember"
2
+ ADD COLUMN "hiddenAt" TIMESTAMP(3);
@@ -512,6 +512,7 @@ model ConversationMember {
512
512
  conversationId String
513
513
  role ConversationRole @default(MEMBER)
514
514
  joinedAt DateTime @default(now())
515
+ hiddenAt DateTime? // When user last hid this conversation from their list
515
516
  lastViewedAt DateTime? // When user last viewed this conversation
516
517
  lastViewedMentionAt DateTime? // When user last viewed mentions in this conversation
517
518
 
package/src/index.ts CHANGED
@@ -520,22 +520,6 @@ async function handleFileUpload(req: Request, res: Response) {
520
520
  }
521
521
 
522
522
  res.header('Access-Control-Allow-Credentials', 'true');
523
-
524
- const user = await getAuthenticatedRequestUser(req);
525
- if (!user) {
526
- return res.status(401).json({ error: 'Authentication required' });
527
- }
528
-
529
- const uploadSessionId = getUploadSessionId(req);
530
- if (!uploadSessionId) {
531
- return res.status(401).json({ error: 'Upload session is required' });
532
- }
533
-
534
- await validateDirectUploadIntent({
535
- filePath,
536
- uploadSessionId,
537
- userId: user.id,
538
- });
539
523
 
540
524
  const contentType = req.headers['content-type'] || 'application/octet-stream';
541
525
  const file = bucket.file(filePath);
@@ -547,7 +531,7 @@ async function handleFileUpload(req: Request, res: Response) {
547
531
  });
548
532
 
549
533
  writeStream.on('error', (error) => {
550
- logger.error('Error uploading file:', {error, filePath, userId: user.id});
534
+ logger.error('Error uploading file:', {error, filePath});
551
535
  if (!res.headersSent) {
552
536
  res.status(500).json({ error: 'Error uploading file' });
553
537
  }
@@ -190,6 +190,18 @@ export function createConversationMember(data: {
190
190
  });
191
191
  }
192
192
 
193
+ /** Hide a conversation for a single member until new activity occurs. */
194
+ export function hideConversationMember(userId: string, conversationId: string) {
195
+ return prisma.conversationMember.update({
196
+ where: {
197
+ userId_conversationId: { userId, conversationId },
198
+ },
199
+ data: {
200
+ hiddenAt: new Date(),
201
+ },
202
+ });
203
+ }
204
+
193
205
  /** Remove a member from a conversation. */
194
206
  export function deleteConversationMember(userId: string, conversationId: string) {
195
207
  return prisma.conversationMember.delete({
@@ -21,6 +21,7 @@ import { marketingRouter } from "./marketing.js";
21
21
  import { worksheetRouter } from "./worksheet.js";
22
22
  import { commentRouter } from "./comment.js";
23
23
  import { studentProgressRouter } from "./studentProgress.js";
24
+ import { searchRouter } from "./search.js";
24
25
 
25
26
  export const appRouter = createTRPCRouter({
26
27
  class: classRouter,
@@ -43,6 +44,7 @@ export const appRouter = createTRPCRouter({
43
44
  worksheet: worksheetRouter,
44
45
  comment: commentRouter,
45
46
  studentProgress: studentProgressRouter,
47
+ search: searchRouter,
46
48
  });
47
49
 
48
50
  // Export type router type definition
@@ -5,6 +5,7 @@ import {
5
5
  createConversationRecord,
6
6
  getConversation,
7
7
  addMemberToConversation,
8
+ hideConversation,
8
9
  removeMemberFromConversation,
9
10
  } from "../services/conversation.js";
10
11
 
@@ -36,6 +37,12 @@ export const conversationRouter = createTRPCRouter({
36
37
  getConversation(ctx.user!.id, input.conversationId)
37
38
  ),
38
39
 
40
+ hide: protectedProcedure
41
+ .input(z.object({ conversationId: z.string() }))
42
+ .mutation(({ ctx, input }) =>
43
+ hideConversation(ctx.user!.id, input.conversationId)
44
+ ),
45
+
39
46
  addMember: protectedProcedure
40
47
  .input(
41
48
  z.object({
@@ -0,0 +1,14 @@
1
+ import { z } from "zod";
2
+ import { createTRPCRouter, protectedProcedure } from "../trpc.js";
3
+ import { searchGlobalClasses } from "../services/search.js";
4
+
5
+ export const searchRouter = createTRPCRouter({
6
+ global: protectedProcedure
7
+ .input(
8
+ z.object({
9
+ query: z.string().max(80).optional(),
10
+ limit: z.number().int().min(1).max(30).optional(),
11
+ })
12
+ )
13
+ .query(({ ctx, input }) => searchGlobalClasses(ctx.user!.id, input)),
14
+ });
@@ -13,15 +13,25 @@ import {
13
13
  createConversation,
14
14
  findConversationByIdAndMember,
15
15
  createConversationMember,
16
+ hideConversationMember,
16
17
  deleteConversationMember,
17
18
  } from "../models/conversation.js";
18
19
 
19
20
  /** List user's conversations with unread and mention counts. */
20
21
  export async function listConversations(userId: string) {
21
22
  const conversations = await findConversationsByUserId(userId);
23
+ const visibleConversations = conversations.filter((conversation) => {
24
+ const userMembership = conversation.members.find((member) => member.userId === userId);
25
+
26
+ if (!userMembership?.hiddenAt) {
27
+ return true;
28
+ }
29
+
30
+ return conversation.updatedAt > userMembership.hiddenAt;
31
+ });
22
32
 
23
33
  const conversationsWithUnread = await Promise.all(
24
- conversations.map(async (conversation) => {
34
+ visibleConversations.map(async (conversation) => {
25
35
  const userMembership = conversation.members.find(
26
36
  (m) => m.userId === userId
27
37
  );
@@ -65,6 +75,20 @@ export async function listConversations(userId: string) {
65
75
  return conversationsWithUnread;
66
76
  }
67
77
 
78
+ export async function hideConversation(userId: string, conversationId: string) {
79
+ const conversation = await findConversationByIdAndMember(conversationId, userId);
80
+ if (!conversation) {
81
+ throw new TRPCError({
82
+ code: "FORBIDDEN",
83
+ message: "Not a member of this conversation",
84
+ });
85
+ }
86
+
87
+ await hideConversationMember(userId, conversationId);
88
+
89
+ return { success: true };
90
+ }
91
+
68
92
  /** Create a DM or group conversation with the given members. */
69
93
  export async function createConversationRecord(
70
94
  userId: string,
@@ -87,11 +111,11 @@ export async function createConversationRecord(
87
111
  }
88
112
 
89
113
  if (type === "DM") {
90
- const targetUser = await findUserByUsername(memberIds[0]);
114
+ const [targetUser] = await findUsersByIdsOrUsernames([memberIds[0]]);
91
115
  if (!targetUser) {
92
116
  throw new TRPCError({
93
117
  code: "BAD_REQUEST",
94
- message: `User "${memberIds[0]}" not found`,
118
+ message: `User not found`,
95
119
  });
96
120
  }
97
121
 
@@ -259,6 +259,14 @@ export async function sendMessage(
259
259
  });
260
260
  }
261
261
 
262
+ await tx.conversationMember.updateMany({
263
+ where: {
264
+ conversationId,
265
+ hiddenAt: { not: null },
266
+ },
267
+ data: { hiddenAt: null },
268
+ });
269
+
262
270
  await tx.conversation.update({
263
271
  where: { id: conversationId },
264
272
  data: { updatedAt: new Date() },