@studious-lms/server 1.6.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/models/access.d.ts +21 -66
- package/dist/models/access.d.ts.map +1 -1
- package/dist/models/access.js +10 -26
- package/dist/models/access.js.map +1 -1
- package/dist/models/agenda.d.ts +2 -0
- package/dist/models/agenda.d.ts.map +1 -1
- package/dist/models/announcement.d.ts +2 -1
- package/dist/models/announcement.d.ts.map +1 -1
- package/dist/models/announcement.js +2 -3
- package/dist/models/announcement.js.map +1 -1
- package/dist/models/assignment.d.ts +7 -0
- package/dist/models/assignment.d.ts.map +1 -1
- package/dist/models/auth.d.ts +2 -4
- package/dist/models/auth.d.ts.map +1 -1
- package/dist/models/auth.js +2 -3
- package/dist/models/auth.js.map +1 -1
- package/dist/models/class.d.ts +32 -4
- package/dist/models/class.d.ts.map +1 -1
- package/dist/models/class.js +15 -2
- package/dist/models/class.js.map +1 -1
- package/dist/models/cloudStorage.d.ts +140 -0
- package/dist/models/cloudStorage.d.ts.map +1 -0
- package/dist/models/cloudStorage.js +175 -0
- package/dist/models/cloudStorage.js.map +1 -0
- package/dist/models/comment.d.ts +0 -24
- package/dist/models/comment.d.ts.map +1 -1
- package/dist/models/comment.js +2 -6
- package/dist/models/comment.js.map +1 -1
- package/dist/models/conversation.d.ts +38 -22
- package/dist/models/conversation.d.ts.map +1 -1
- package/dist/models/conversation.js +50 -2
- package/dist/models/conversation.js.map +1 -1
- package/dist/models/department.d.ts +154 -0
- package/dist/models/department.d.ts.map +1 -0
- package/dist/models/department.js +135 -0
- package/dist/models/department.js.map +1 -0
- package/dist/models/event.d.ts +7 -1
- package/dist/models/event.d.ts.map +1 -1
- package/dist/models/event.js +2 -5
- package/dist/models/event.js.map +1 -1
- package/dist/models/file.d.ts +25 -10
- package/dist/models/file.d.ts.map +1 -1
- package/dist/models/file.js +2 -5
- package/dist/models/file.js.map +1 -1
- package/dist/models/folder.d.ts +47 -0
- package/dist/models/folder.d.ts.map +1 -1
- package/dist/models/labChat.d.ts +5 -3
- package/dist/models/labChat.d.ts.map +1 -1
- package/dist/models/message.d.ts +46 -12
- package/dist/models/message.d.ts.map +1 -1
- package/dist/models/message.js +19 -10
- package/dist/models/message.js.map +1 -1
- package/dist/models/newtonChat.d.ts +2 -1
- package/dist/models/newtonChat.d.ts.map +1 -1
- package/dist/models/school.d.ts +45 -12
- package/dist/models/school.d.ts.map +1 -1
- package/dist/models/school.js +19 -2
- package/dist/models/school.js.map +1 -1
- package/dist/models/section.d.ts +1 -0
- package/dist/models/section.d.ts.map +1 -1
- package/dist/models/tag.d.ts +117 -0
- package/dist/models/tag.d.ts.map +1 -0
- package/dist/models/tag.js +113 -0
- package/dist/models/tag.js.map +1 -0
- package/dist/models/worksheet.d.ts +3 -0
- package/dist/models/worksheet.d.ts.map +1 -1
- package/dist/pipelines/aiLabChatContract.d.ts +8 -8
- package/dist/routers/_app.d.ts +3556 -2298
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/_app.js +6 -2
- package/dist/routers/_app.js.map +1 -1
- package/dist/routers/agenda.d.ts +2 -0
- package/dist/routers/agenda.d.ts.map +1 -1
- package/dist/routers/announcement.d.ts.map +1 -1
- package/dist/routers/announcement.js +3 -5
- package/dist/routers/announcement.js.map +1 -1
- package/dist/routers/assignment.d.ts +18 -9
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/assignment.js +2 -3
- package/dist/routers/assignment.js.map +1 -1
- package/dist/routers/class.d.ts +87 -14
- package/dist/routers/class.d.ts.map +1 -1
- package/dist/routers/class.js +5 -2
- package/dist/routers/class.js.map +1 -1
- package/dist/routers/conversation.d.ts +20 -5
- package/dist/routers/conversation.d.ts.map +1 -1
- package/dist/routers/conversation.js +6 -3
- package/dist/routers/conversation.js.map +1 -1
- package/dist/routers/department.d.ts +293 -0
- package/dist/routers/department.d.ts.map +1 -0
- package/dist/routers/department.js +93 -0
- package/dist/routers/department.js.map +1 -0
- package/dist/routers/event.d.ts +2 -1
- package/dist/routers/event.d.ts.map +1 -1
- package/dist/routers/file.d.ts +8 -6
- package/dist/routers/file.d.ts.map +1 -1
- package/dist/routers/file.js +26 -17
- package/dist/routers/file.js.map +1 -1
- package/dist/routers/folder.d.ts +139 -186
- package/dist/routers/folder.d.ts.map +1 -1
- package/dist/routers/folder.js +82 -90
- package/dist/routers/folder.js.map +1 -1
- package/dist/routers/labChat.d.ts +11 -2
- package/dist/routers/labChat.d.ts.map +1 -1
- package/dist/routers/message.d.ts +20 -2
- package/dist/routers/message.d.ts.map +1 -1
- package/dist/routers/newtonChat.d.ts +9 -0
- package/dist/routers/newtonChat.d.ts.map +1 -1
- package/dist/routers/school.d.ts +160 -17
- package/dist/routers/school.d.ts.map +1 -1
- package/dist/routers/school.js +46 -3
- package/dist/routers/school.js.map +1 -1
- package/dist/routers/tag.d.ts +119 -0
- package/dist/routers/tag.d.ts.map +1 -0
- package/dist/routers/tag.js +52 -0
- package/dist/routers/tag.js.map +1 -0
- package/dist/routers/worksheet.d.ts +1 -0
- package/dist/routers/worksheet.d.ts.map +1 -1
- package/dist/seedDatabase.d.ts +0 -1
- package/dist/seedDatabase.d.ts.map +1 -1
- package/dist/services/agenda.d.ts +2 -0
- package/dist/services/agenda.d.ts.map +1 -1
- package/dist/services/assignment.d.ts +18 -9
- package/dist/services/assignment.d.ts.map +1 -1
- package/dist/services/assignment.js +4 -4
- package/dist/services/assignment.js.map +1 -1
- package/dist/services/class.d.ts +87 -14
- package/dist/services/class.d.ts.map +1 -1
- package/dist/services/class.js +91 -4
- package/dist/services/class.js.map +1 -1
- package/dist/services/conversation.d.ts +21 -5
- package/dist/services/conversation.d.ts.map +1 -1
- package/dist/services/conversation.js +22 -3
- package/dist/services/conversation.js.map +1 -1
- package/dist/services/department.d.ts +200 -0
- package/dist/services/department.d.ts.map +1 -0
- package/dist/services/department.js +368 -0
- package/dist/services/department.js.map +1 -0
- package/dist/services/event.d.ts +2 -1
- package/dist/services/event.d.ts.map +1 -1
- package/dist/services/file.d.ts +85 -0
- package/dist/services/file.d.ts.map +1 -1
- package/dist/services/file.js +92 -2
- package/dist/services/file.js.map +1 -1
- package/dist/services/folder.d.ts +125 -149
- package/dist/services/folder.d.ts.map +1 -1
- package/dist/services/folder.js +258 -199
- package/dist/services/folder.js.map +1 -1
- package/dist/services/labChat.d.ts +11 -2
- package/dist/services/labChat.d.ts.map +1 -1
- package/dist/services/labChat.js +8 -7
- package/dist/services/labChat.js.map +1 -1
- package/dist/services/message.d.ts +20 -2
- package/dist/services/message.d.ts.map +1 -1
- package/dist/services/message.js +7 -23
- package/dist/services/message.js.map +1 -1
- package/dist/services/newtonChat.d.ts +9 -0
- package/dist/services/newtonChat.d.ts.map +1 -1
- package/dist/services/newtonChat.js +7 -6
- package/dist/services/newtonChat.js.map +1 -1
- package/dist/services/school.d.ts +142 -15
- package/dist/services/school.d.ts.map +1 -1
- package/dist/services/school.js +308 -54
- package/dist/services/school.js.map +1 -1
- package/dist/services/tag.d.ts +71 -0
- package/dist/services/tag.d.ts.map +1 -0
- package/dist/services/tag.js +178 -0
- package/dist/services/tag.js.map +1 -0
- package/dist/services/worksheet.d.ts +1 -0
- package/dist/services/worksheet.d.ts.map +1 -1
- package/dist/utils/aiUser.d.ts.map +1 -1
- package/dist/utils/aiUser.js +2 -3
- package/dist/utils/aiUser.js.map +1 -1
- package/package.json +1 -1
- package/prisma/migrations/20260509220000_institution_overhaul/migration.sql +184 -0
- package/prisma/schema.prisma +119 -35
- package/src/models/access.ts +8 -24
- package/src/models/announcement.ts +0 -1
- package/src/models/auth.ts +0 -1
- package/src/models/class.ts +13 -0
- package/src/models/cloudStorage.ts +194 -0
- package/src/models/comment.ts +0 -4
- package/src/models/conversation.ts +54 -0
- package/src/models/department.ts +159 -0
- package/src/models/event.ts +0 -3
- package/src/models/file.ts +0 -3
- package/src/models/message.ts +19 -5
- package/src/models/school.ts +26 -0
- package/src/models/tag.ts +140 -0
- package/src/routers/_app.ts +4 -0
- package/src/routers/announcement.ts +1 -3
- package/src/routers/assignment.ts +0 -1
- package/src/routers/class.ts +3 -0
- package/src/routers/conversation.ts +7 -0
- package/src/routers/department.ts +174 -0
- package/src/routers/file.ts +29 -26
- package/src/routers/folder.ts +100 -121
- package/src/routers/school.ts +83 -0
- package/src/routers/tag.ts +78 -0
- package/src/services/assignment.ts +2 -3
- package/src/services/class.ts +108 -3
- package/src/services/conversation.ts +30 -0
- package/src/services/department.ts +478 -0
- package/src/services/file.ts +100 -0
- package/src/services/folder.ts +313 -240
- package/src/services/labChat.ts +7 -6
- package/src/services/message.ts +5 -21
- package/src/services/newtonChat.ts +5 -5
- package/src/services/school.ts +369 -57
- package/src/services/tag.ts +229 -0
- package/src/utils/aiUser.ts +0 -1
- package/tests/integration/routers/conversation.test.ts +121 -2
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tag service — institution-scoped tag CRUD and assignment to taggable
|
|
3
|
+
* targets (users, classes, rubrics, boundaries).
|
|
4
|
+
*/
|
|
5
|
+
import { TRPCError } from "@trpc/server";
|
|
6
|
+
import { prisma } from "../lib/prisma.js";
|
|
7
|
+
import {
|
|
8
|
+
attachTagToTarget,
|
|
9
|
+
createTag,
|
|
10
|
+
deleteTag,
|
|
11
|
+
detachTagFromTarget,
|
|
12
|
+
getTag,
|
|
13
|
+
listTagsAttachedTo,
|
|
14
|
+
listTagsForSchool,
|
|
15
|
+
type TagTarget,
|
|
16
|
+
updateTag,
|
|
17
|
+
} from "../models/tag.js";
|
|
18
|
+
import {
|
|
19
|
+
findActiveSchoolAdminMembership,
|
|
20
|
+
findSchoolMembership,
|
|
21
|
+
} from "../models/school.js";
|
|
22
|
+
|
|
23
|
+
async function requireAdmin(schoolId: string, userId: string) {
|
|
24
|
+
const membership = await findActiveSchoolAdminMembership(schoolId, userId);
|
|
25
|
+
if (!membership) {
|
|
26
|
+
throw new TRPCError({
|
|
27
|
+
code: "FORBIDDEN",
|
|
28
|
+
message: "Only school admins can manage tags",
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function requireMember(schoolId: string, userId: string) {
|
|
34
|
+
const membership = await findSchoolMembership(schoolId, userId);
|
|
35
|
+
if (!membership || membership.status !== "ACTIVE") {
|
|
36
|
+
throw new TRPCError({
|
|
37
|
+
code: "FORBIDDEN",
|
|
38
|
+
message: "You aren't a member of this institution",
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return membership;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function listTags(
|
|
45
|
+
userId: string,
|
|
46
|
+
input: { schoolId: string; departmentId?: string | null }
|
|
47
|
+
) {
|
|
48
|
+
await requireMember(input.schoolId, userId);
|
|
49
|
+
return listTagsForSchool(input.schoolId, {
|
|
50
|
+
departmentId: input.departmentId,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function createTagRecord(
|
|
55
|
+
userId: string,
|
|
56
|
+
input: {
|
|
57
|
+
schoolId: string;
|
|
58
|
+
departmentId?: string | null;
|
|
59
|
+
name: string;
|
|
60
|
+
color?: string;
|
|
61
|
+
}
|
|
62
|
+
) {
|
|
63
|
+
await requireAdmin(input.schoolId, userId);
|
|
64
|
+
if (input.departmentId) {
|
|
65
|
+
const dept = await prisma.department.findUnique({
|
|
66
|
+
where: { id: input.departmentId },
|
|
67
|
+
select: { schoolId: true },
|
|
68
|
+
});
|
|
69
|
+
if (!dept || dept.schoolId !== input.schoolId) {
|
|
70
|
+
throw new TRPCError({
|
|
71
|
+
code: "BAD_REQUEST",
|
|
72
|
+
message: "Department does not belong to this institution",
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return createTag(input);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function updateTagRecord(
|
|
80
|
+
userId: string,
|
|
81
|
+
input: {
|
|
82
|
+
tagId: string;
|
|
83
|
+
name?: string;
|
|
84
|
+
color?: string;
|
|
85
|
+
departmentId?: string | null;
|
|
86
|
+
}
|
|
87
|
+
) {
|
|
88
|
+
const tag = await getTag(input.tagId);
|
|
89
|
+
if (!tag) {
|
|
90
|
+
throw new TRPCError({ code: "NOT_FOUND", message: "Tag not found" });
|
|
91
|
+
}
|
|
92
|
+
await requireAdmin(tag.schoolId, userId);
|
|
93
|
+
if (input.departmentId) {
|
|
94
|
+
const dept = await prisma.department.findUnique({
|
|
95
|
+
where: { id: input.departmentId },
|
|
96
|
+
select: { schoolId: true },
|
|
97
|
+
});
|
|
98
|
+
if (!dept || dept.schoolId !== tag.schoolId) {
|
|
99
|
+
throw new TRPCError({
|
|
100
|
+
code: "BAD_REQUEST",
|
|
101
|
+
message: "Department does not belong to this institution",
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return updateTag(input.tagId, {
|
|
106
|
+
name: input.name,
|
|
107
|
+
color: input.color,
|
|
108
|
+
departmentId: input.departmentId,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function deleteTagRecord(userId: string, tagId: string) {
|
|
113
|
+
const tag = await getTag(tagId);
|
|
114
|
+
if (!tag) {
|
|
115
|
+
throw new TRPCError({ code: "NOT_FOUND", message: "Tag not found" });
|
|
116
|
+
}
|
|
117
|
+
await requireAdmin(tag.schoolId, userId);
|
|
118
|
+
await deleteTag(tagId);
|
|
119
|
+
return { success: true };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function ensureTargetInScope(
|
|
123
|
+
schoolId: string,
|
|
124
|
+
target: TagTarget,
|
|
125
|
+
targetId: string
|
|
126
|
+
) {
|
|
127
|
+
switch (target) {
|
|
128
|
+
case "USER": {
|
|
129
|
+
const inSchool = await prisma.schoolMembership.findFirst({
|
|
130
|
+
where: { schoolId, userId: targetId, status: "ACTIVE" },
|
|
131
|
+
select: { id: true },
|
|
132
|
+
});
|
|
133
|
+
if (!inSchool) {
|
|
134
|
+
throw new TRPCError({
|
|
135
|
+
code: "BAD_REQUEST",
|
|
136
|
+
message: "User is not in this institution",
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
case "CLASS": {
|
|
142
|
+
const inSchool = await prisma.class.findFirst({
|
|
143
|
+
where: { id: targetId, schoolId },
|
|
144
|
+
select: { id: true },
|
|
145
|
+
});
|
|
146
|
+
if (!inSchool) {
|
|
147
|
+
throw new TRPCError({
|
|
148
|
+
code: "BAD_REQUEST",
|
|
149
|
+
message: "Class is not in this institution",
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
case "RUBRIC": {
|
|
155
|
+
const inSchool = await prisma.markScheme.findFirst({
|
|
156
|
+
where: {
|
|
157
|
+
id: targetId,
|
|
158
|
+
OR: [
|
|
159
|
+
{ class: { schoolId } },
|
|
160
|
+
{ department: { schoolId } },
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
select: { id: true },
|
|
164
|
+
});
|
|
165
|
+
if (!inSchool) {
|
|
166
|
+
throw new TRPCError({
|
|
167
|
+
code: "BAD_REQUEST",
|
|
168
|
+
message: "Rubric is not in this institution",
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
case "BOUNDARY": {
|
|
174
|
+
const inSchool = await prisma.gradingBoundary.findFirst({
|
|
175
|
+
where: {
|
|
176
|
+
id: targetId,
|
|
177
|
+
OR: [
|
|
178
|
+
{ class: { schoolId } },
|
|
179
|
+
{ department: { schoolId } },
|
|
180
|
+
],
|
|
181
|
+
},
|
|
182
|
+
select: { id: true },
|
|
183
|
+
});
|
|
184
|
+
if (!inSchool) {
|
|
185
|
+
throw new TRPCError({
|
|
186
|
+
code: "BAD_REQUEST",
|
|
187
|
+
message: "Boundary is not in this institution",
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export async function attachTag(
|
|
196
|
+
userId: string,
|
|
197
|
+
input: { tagId: string; target: TagTarget; targetId: string }
|
|
198
|
+
) {
|
|
199
|
+
const tag = await getTag(input.tagId);
|
|
200
|
+
if (!tag) {
|
|
201
|
+
throw new TRPCError({ code: "NOT_FOUND", message: "Tag not found" });
|
|
202
|
+
}
|
|
203
|
+
await requireMember(tag.schoolId, userId);
|
|
204
|
+
await ensureTargetInScope(tag.schoolId, input.target, input.targetId);
|
|
205
|
+
return attachTagToTarget(input);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export async function detachTag(
|
|
209
|
+
userId: string,
|
|
210
|
+
input: { tagId: string; target: TagTarget; targetId: string }
|
|
211
|
+
) {
|
|
212
|
+
const tag = await getTag(input.tagId);
|
|
213
|
+
if (!tag) {
|
|
214
|
+
throw new TRPCError({ code: "NOT_FOUND", message: "Tag not found" });
|
|
215
|
+
}
|
|
216
|
+
await requireMember(tag.schoolId, userId);
|
|
217
|
+
return detachTagFromTarget(input);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export async function getAttachedTags(
|
|
221
|
+
userId: string,
|
|
222
|
+
input: { target: TagTarget; targetId: string }
|
|
223
|
+
) {
|
|
224
|
+
const tags = await listTagsAttachedTo(input.target, input.targetId);
|
|
225
|
+
if (tags[0]) {
|
|
226
|
+
await requireMember(tags[0].schoolId, userId);
|
|
227
|
+
}
|
|
228
|
+
return tags;
|
|
229
|
+
}
|
package/src/utils/aiUser.ts
CHANGED
|
@@ -26,7 +26,6 @@ export async function ensureAIUserExists(): Promise<void> {
|
|
|
26
26
|
email: 'ai@studious-lms.com',
|
|
27
27
|
password: 'ai-system-user', // Not used for login
|
|
28
28
|
verified: true,
|
|
29
|
-
role: 'NONE', // Special role for AI
|
|
30
29
|
profile: {
|
|
31
30
|
create: {
|
|
32
31
|
displayName: 'AI Assistant',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { test, expect, describe, beforeAll } from 'vitest';
|
|
1
|
+
import { test, expect, describe, beforeAll, afterAll } from 'vitest';
|
|
2
2
|
import { prisma } from '../../../src/lib/prisma';
|
|
3
3
|
import { user1Caller, user2Caller, user3Caller } from '../setup';
|
|
4
4
|
import { expectUnauth } from '../helpers';
|
|
@@ -7,16 +7,19 @@ describe('Conversation Router', () => {
|
|
|
7
7
|
let dmConversation: any;
|
|
8
8
|
let groupConversation: any;
|
|
9
9
|
let user1Id: string;
|
|
10
|
+
let user2Id: string;
|
|
10
11
|
let user3Id: string;
|
|
11
12
|
let hideConversationCounter = 0;
|
|
12
13
|
|
|
13
14
|
beforeAll(async () => {
|
|
14
|
-
const [user1Profile, user3Profile] = await Promise.all([
|
|
15
|
+
const [user1Profile, user2Profile, user3Profile] = await Promise.all([
|
|
15
16
|
user1Caller.user.getProfile(),
|
|
17
|
+
user2Caller.user.getProfile(),
|
|
16
18
|
user3Caller.user.getProfile(),
|
|
17
19
|
]);
|
|
18
20
|
|
|
19
21
|
user1Id = user1Profile.id;
|
|
22
|
+
user2Id = user2Profile.id;
|
|
20
23
|
user3Id = user3Profile.id;
|
|
21
24
|
});
|
|
22
25
|
|
|
@@ -89,6 +92,122 @@ describe('Conversation Router', () => {
|
|
|
89
92
|
});
|
|
90
93
|
});
|
|
91
94
|
|
|
95
|
+
describe('searchMembers', () => {
|
|
96
|
+
let schoolId: string;
|
|
97
|
+
let previousUser2DisplayName: string | null = null;
|
|
98
|
+
let user2ProfileExisted = false;
|
|
99
|
+
|
|
100
|
+
beforeAll(async () => {
|
|
101
|
+
const existingProfile = await prisma.userProfile.findUnique({
|
|
102
|
+
where: { userId: user2Id },
|
|
103
|
+
select: { displayName: true },
|
|
104
|
+
});
|
|
105
|
+
user2ProfileExisted = Boolean(existingProfile);
|
|
106
|
+
previousUser2DisplayName = existingProfile?.displayName ?? null;
|
|
107
|
+
|
|
108
|
+
const school = await prisma.school.create({
|
|
109
|
+
data: {
|
|
110
|
+
name: 'Conversation Search School',
|
|
111
|
+
memberships: {
|
|
112
|
+
create: [
|
|
113
|
+
{ userId: user1Id, role: 'TEACHER', status: 'ACTIVE' },
|
|
114
|
+
{ userId: user2Id, role: 'STUDENT', status: 'ACTIVE' },
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
schoolId = school.id;
|
|
120
|
+
|
|
121
|
+
await prisma.userProfile.upsert({
|
|
122
|
+
where: { userId: user2Id },
|
|
123
|
+
update: { displayName: 'Searchable Student' },
|
|
124
|
+
create: { userId: user2Id, displayName: 'Searchable Student' },
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('should search active institution members by username', async () => {
|
|
129
|
+
const results = await user1Caller.conversation.searchMembers({
|
|
130
|
+
query: 'testuser2',
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
expect(results.map((user) => user.id)).toContain(user2Id);
|
|
134
|
+
expect(results.map((user) => user.id)).not.toContain(user1Id);
|
|
135
|
+
expect(results.map((user) => user.id)).not.toContain(user3Id);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('should not search active institution members for one-character queries', async () => {
|
|
139
|
+
const results = await user1Caller.conversation.searchMembers({
|
|
140
|
+
query: 't',
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
expect(results).toEqual([]);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('should search active institution members by display name', async () => {
|
|
147
|
+
const results = await user1Caller.conversation.searchMembers({
|
|
148
|
+
query: 'searchable',
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
expect(results).toEqual(
|
|
152
|
+
expect.arrayContaining([
|
|
153
|
+
expect.objectContaining({
|
|
154
|
+
id: user2Id,
|
|
155
|
+
username: 'testuser2',
|
|
156
|
+
profile: expect.objectContaining({
|
|
157
|
+
displayName: 'Searchable Student',
|
|
158
|
+
}),
|
|
159
|
+
}),
|
|
160
|
+
]),
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('should not expose users outside the requester institution', async () => {
|
|
165
|
+
const results = await user1Caller.conversation.searchMembers({
|
|
166
|
+
query: 'testuser3',
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
expect(results).toEqual([]);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('should not expose inactive members from the requester institution', async () => {
|
|
173
|
+
await prisma.schoolMembership.create({
|
|
174
|
+
data: {
|
|
175
|
+
schoolId,
|
|
176
|
+
userId: user3Id,
|
|
177
|
+
role: 'STUDENT',
|
|
178
|
+
status: 'INACTIVE',
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const results = await user1Caller.conversation.searchMembers({
|
|
183
|
+
query: 'testuser3',
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
expect(results).toEqual([]);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('should return no institution search results for users without an active school', async () => {
|
|
190
|
+
const results = await user3Caller.conversation.searchMembers({
|
|
191
|
+
query: 'testuser1',
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
expect(results).toEqual([]);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
afterAll(async () => {
|
|
198
|
+
if (user2ProfileExisted) {
|
|
199
|
+
await prisma.userProfile.update({
|
|
200
|
+
where: { userId: user2Id },
|
|
201
|
+
data: { displayName: previousUser2DisplayName },
|
|
202
|
+
});
|
|
203
|
+
} else {
|
|
204
|
+
await prisma.userProfile.deleteMany({ where: { userId: user2Id } });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
await prisma.school.delete({ where: { id: schoolId } });
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
92
211
|
describe('get', () => {
|
|
93
212
|
test('should get conversation by ID', async () => {
|
|
94
213
|
const conversation = await user1Caller.conversation.get({
|