@studious-lms/server 1.4.7 → 1.4.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.
Files changed (58) hide show
  1. package/.github/workflows/npm.yml +28 -0
  2. package/dist/models/assignment.d.ts +32 -0
  3. package/dist/models/assignment.d.ts.map +1 -1
  4. package/dist/models/assignment.js +111 -2
  5. package/dist/models/assignment.js.map +1 -1
  6. package/dist/models/worksheet.d.ts +98 -76
  7. package/dist/models/worksheet.d.ts.map +1 -1
  8. package/dist/models/worksheet.js +38 -5
  9. package/dist/models/worksheet.js.map +1 -1
  10. package/dist/routers/_app.d.ts +140 -0
  11. package/dist/routers/_app.d.ts.map +1 -1
  12. package/dist/routers/assignment.d.ts +35 -0
  13. package/dist/routers/assignment.d.ts.map +1 -1
  14. package/dist/routers/assignment.js +10 -3
  15. package/dist/routers/assignment.js.map +1 -1
  16. package/dist/routers/class.d.ts +19 -0
  17. package/dist/routers/class.d.ts.map +1 -1
  18. package/dist/routers/worksheet.d.ts +16 -0
  19. package/dist/routers/worksheet.d.ts.map +1 -1
  20. package/dist/routers/worksheet.js +10 -3
  21. package/dist/routers/worksheet.js.map +1 -1
  22. package/dist/services/assignment.d.ts +30 -0
  23. package/dist/services/assignment.d.ts.map +1 -1
  24. package/dist/services/assignment.js +37 -3
  25. package/dist/services/assignment.js.map +1 -1
  26. package/dist/services/class.d.ts +19 -0
  27. package/dist/services/class.d.ts.map +1 -1
  28. package/dist/services/class.js +37 -19
  29. package/dist/services/class.js.map +1 -1
  30. package/dist/services/worksheet.d.ts +11 -0
  31. package/dist/services/worksheet.d.ts.map +1 -1
  32. package/dist/services/worksheet.js +37 -3
  33. package/dist/services/worksheet.js.map +1 -1
  34. package/package.json +1 -1
  35. package/prisma/migrations/20260504071500_school_memberships/migration.sql +1 -1
  36. package/src/models/assignment.ts +132 -0
  37. package/src/models/worksheet.ts +54 -0
  38. package/src/routers/assignment.ts +19 -0
  39. package/src/routers/worksheet.ts +18 -0
  40. package/src/services/assignment.ts +47 -1
  41. package/src/services/class.ts +41 -17
  42. package/src/services/worksheet.ts +46 -0
  43. package/dist/lib/notificationHandler.d.ts +0 -25
  44. package/dist/lib/notificationHandler.d.ts.map +0 -1
  45. package/dist/lib/notificationHandler.js +0 -32
  46. package/dist/lib/notificationHandler.js.map +0 -1
  47. package/dist/server/pipelines/aiLabChat.d.ts +0 -21
  48. package/dist/server/pipelines/aiLabChat.d.ts.map +0 -1
  49. package/dist/server/pipelines/aiLabChat.js +0 -456
  50. package/dist/server/pipelines/aiLabChat.js.map +0 -1
  51. package/dist/server/pipelines/aiNewtonChat.d.ts +0 -30
  52. package/dist/server/pipelines/aiNewtonChat.d.ts.map +0 -1
  53. package/dist/server/pipelines/aiNewtonChat.js +0 -285
  54. package/dist/server/pipelines/aiNewtonChat.js.map +0 -1
  55. package/dist/server/pipelines/gradeWorksheet.d.ts +0 -30
  56. package/dist/server/pipelines/gradeWorksheet.d.ts.map +0 -1
  57. package/dist/server/pipelines/gradeWorksheet.js +0 -248
  58. package/dist/server/pipelines/gradeWorksheet.js.map +0 -1
@@ -18,6 +18,7 @@ import {
18
18
  getSubmissionById,
19
19
  getSubmissions,
20
20
  createAssignmentRecord,
21
+ duplicateAssignmentRecord,
21
22
  updateAssignmentRecord,
22
23
  deleteAssignmentRecord,
23
24
  updateSubmissionRecord,
@@ -273,6 +274,24 @@ export const assignmentRouter = createTRPCRouter({
273
274
  create: protectedTeacherProcedure
274
275
  .input(createAssignmentSchema)
275
276
  .mutation(({ ctx, input }) => createAssignmentRecord(ctx.user!.id, input)),
277
+
278
+ duplicateAssignment: protectedTeacherProcedure
279
+ .input(
280
+ z.object({
281
+ classId: z.string(),
282
+ title: z.string(),
283
+ assignmentId: z.string(),
284
+ })
285
+ )
286
+ .mutation(({ ctx, input }) =>
287
+ duplicateAssignmentRecord(
288
+ input.assignmentId,
289
+ input.classId,
290
+ input.title,
291
+ ctx.user!.id
292
+ )
293
+ ),
294
+
276
295
  update: protectedTeacherProcedure
277
296
  .input(updateAssignmentSchema)
278
297
  .mutation(({ ctx, input }) => updateAssignmentRecord(ctx.user!.id, input)),
@@ -19,6 +19,7 @@ import {
19
19
  updateWorksheetRecord,
20
20
  deleteWorksheetRecord,
21
21
  createWorksheetRecord,
22
+ duplicateWorksheetRecord,
22
23
  createWorksheetFromPdf,
23
24
  createWorksheetFromUploadedPdf,
24
25
  addQuestionToWorksheet,
@@ -108,6 +109,23 @@ export const worksheetRouter = createTRPCRouter({
108
109
  createWorksheetRecord(input.classId, input.name)
109
110
  ),
110
111
 
112
+ duplicateWorksheet: protectedTeacherProcedure
113
+ .input(
114
+ z.object({
115
+ classId: z.string(),
116
+ name: z.string(),
117
+ worksheetId: z.string(),
118
+ })
119
+ )
120
+ .mutation(({ ctx, input }) =>
121
+ duplicateWorksheetRecord(
122
+ input.worksheetId,
123
+ input.classId,
124
+ input.name,
125
+ ctx.user!.id
126
+ )
127
+ ),
128
+
111
129
  createFromPdf: protectedTeacherProcedure
112
130
  .input(z.object({ classId: z.string(), url: z.string().url() }))
113
131
  .mutation(({ input }) => createWorksheetFromPdf(input.classId, input.url)),
@@ -6,6 +6,7 @@ import { prisma } from "../lib/prisma.js";
6
6
  import {
7
7
  findAssignmentById,
8
8
  findAssignmentWithDetails,
9
+ findAssignmentWithClassForTeacher,
9
10
  findAssignmentForDelete,
10
11
  findAssignmentsDueToday,
11
12
  findSubmissionByAssignmentAndStudent,
@@ -15,10 +16,10 @@ import {
15
16
  findSubmissionsByAssignmentForTeacher,
16
17
  createSubmission,
17
18
  findSectionsByClassId,
18
- findAssignmentWithClassForTeacher,
19
19
  findAvailableEventsForAssignment,
20
20
  attachAssignmentToEvent,
21
21
  detachAssignmentFromEvent,
22
+ duplicateAssignment,
22
23
  } from "../models/assignment.js";
23
24
  import { createDirectUploadFiles, type UploadedFile } from "../lib/fileUpload.js";
24
25
  import { deleteFile } from "../lib/googleCloudStorage.js";
@@ -320,6 +321,51 @@ export async function createAssignmentRecord(
320
321
  return assignment;
321
322
  }
322
323
 
324
+ /**
325
+ * Duplicate an assignment into another class. Caller must teach both the source assignment’s
326
+ * class and the target class.
327
+ */
328
+ export async function duplicateAssignmentRecord(
329
+ assignmentId: string,
330
+ classId: string,
331
+ title: string,
332
+ userId: string
333
+ ) {
334
+ const source = await findAssignmentWithClassForTeacher(assignmentId, userId);
335
+ if (!source) {
336
+ throw new TRPCError({
337
+ code: "NOT_FOUND",
338
+ message:
339
+ "Assignment not found or you are not a teacher of the class it belongs to",
340
+ });
341
+ }
342
+
343
+ const targetOk = await prisma.class.findFirst({
344
+ where: {
345
+ id: classId,
346
+ teachers: { some: { id: userId } },
347
+ },
348
+ select: { id: true },
349
+ });
350
+
351
+ if (!targetOk) {
352
+ throw new TRPCError({
353
+ code: "FORBIDDEN",
354
+ message: "You are not a teacher of the target class",
355
+ });
356
+ }
357
+
358
+ const created = await duplicateAssignment(assignmentId, classId, title, userId);
359
+ if (!created) {
360
+ throw new TRPCError({
361
+ code: "NOT_FOUND",
362
+ message: "Assignment not found or could not be duplicated",
363
+ });
364
+ }
365
+
366
+ return created;
367
+ }
368
+
323
369
  export async function updateAssignmentRecord(
324
370
  userId: string,
325
371
  input: {
@@ -25,9 +25,35 @@ import {
25
25
  findFullExportableClass,
26
26
  createClassByImport,
27
27
  } from "../models/class.js";
28
- import { listUserSchoolMemberships } from "../models/school.js";
29
28
  import { Class, Folder } from "@prisma/client";
30
29
 
30
+ async function requireActiveSchoolMembers(schoolId: string, userIds: string[]) {
31
+ const uniqueUserIds = [...new Set(userIds)];
32
+ if (!uniqueUserIds.length) {
33
+ return;
34
+ }
35
+
36
+ const memberships = await prisma.schoolMembership.findMany({
37
+ where: {
38
+ schoolId,
39
+ status: "ACTIVE",
40
+ userId: { in: uniqueUserIds },
41
+ },
42
+ select: { userId: true },
43
+ });
44
+ const activeMemberIds = new Set(
45
+ memberships.map((membership) => membership.userId)
46
+ );
47
+ const hasNonMember = uniqueUserIds.some((id) => !activeMemberIds.has(id));
48
+
49
+ if (hasNonMember) {
50
+ throw new TRPCError({
51
+ code: "FORBIDDEN",
52
+ message: "All class members must belong to this school",
53
+ });
54
+ }
55
+ }
56
+
31
57
  /** Get all classes where user is teacher or student, with assignments and members. */
32
58
  export async function getAllClasses(userId: string) {
33
59
  const [teacherClasses, studentClasses] = await Promise.all([
@@ -128,8 +154,10 @@ export async function createClass(
128
154
  const { students, teachers, name, section, subject, color, schoolId } = input;
129
155
 
130
156
  if (schoolId) {
131
- const memberships = await listUserSchoolMemberships(userId);
132
- const membership = memberships.find((item) => item.schoolId === schoolId);
157
+ const membership = await prisma.schoolMembership.findFirst({
158
+ where: { userId, schoolId, status: "ACTIVE" },
159
+ select: { role: true },
160
+ });
133
161
 
134
162
  if (!membership) {
135
163
  throw new TRPCError({
@@ -146,19 +174,11 @@ export async function createClass(
146
174
  }
147
175
  }
148
176
 
149
- if (teachers && teachers.length > 0 && students && students.length > 0) {
150
- return prisma.class.create({
151
- data: {
152
- name,
153
- section,
154
- subject,
155
- color,
156
- schoolId,
157
- teachers: { connect: teachers.map((id) => ({ id })) },
158
- students: { connect: students.map((id) => ({ id })) },
159
- },
160
- include: { teachers: true, students: true },
161
- });
177
+ const teacherIds = teachers?.length ? teachers : [userId];
178
+ const studentIds = students?.length ? students : [];
179
+
180
+ if (schoolId) {
181
+ await requireActiveSchoolMembers(schoolId, [...teacherIds, ...studentIds]);
162
182
  }
163
183
 
164
184
  return prisma.class.create({
@@ -168,8 +188,12 @@ export async function createClass(
168
188
  subject,
169
189
  color,
170
190
  schoolId,
171
- teachers: { connect: { id: userId } },
191
+ teachers: { connect: teacherIds.map((id) => ({ id })) },
192
+ ...(studentIds.length
193
+ ? { students: { connect: studentIds.map((id) => ({ id })) } }
194
+ : {}),
172
195
  },
196
+ include: { teachers: true, students: true },
173
197
  });
174
198
  }
175
199
 
@@ -14,6 +14,7 @@ import {
14
14
  findWorksheetByIdForTeacher,
15
15
  findWorksheetsByClassId,
16
16
  createWorksheet,
17
+ duplicateWorksheet,
17
18
  updateWorksheet,
18
19
  deleteWorksheet,
19
20
  findQuestionsByWorksheetId,
@@ -123,6 +124,51 @@ export async function createWorksheetRecord(classId: string, name: string) {
123
124
  return createWorksheet({ classId, name });
124
125
  }
125
126
 
127
+ /**
128
+ * Duplicate a worksheet into another class. Caller must teach both the source worksheet's class
129
+ * and the target class.
130
+ */
131
+ export async function duplicateWorksheetRecord(
132
+ worksheetId: string,
133
+ classId: string,
134
+ name: string,
135
+ userId: string
136
+ ) {
137
+ const source = await findWorksheetByIdForTeacher(worksheetId, userId);
138
+ if (!source) {
139
+ throw new TRPCError({
140
+ code: "NOT_FOUND",
141
+ message:
142
+ "Worksheet not found or you are not a teacher of the class it belongs to",
143
+ });
144
+ }
145
+
146
+ const targetOk = await prisma.class.findFirst({
147
+ where: {
148
+ id: classId,
149
+ teachers: { some: { id: userId } },
150
+ },
151
+ select: { id: true },
152
+ });
153
+
154
+ if (!targetOk) {
155
+ throw new TRPCError({
156
+ code: "FORBIDDEN",
157
+ message: "You are not a teacher of the target class",
158
+ });
159
+ }
160
+
161
+ const created = await duplicateWorksheet(worksheetId, classId, name);
162
+ if (!created) {
163
+ throw new TRPCError({
164
+ code: "NOT_FOUND",
165
+ message: "Worksheet not found",
166
+ });
167
+ }
168
+
169
+ return created;
170
+ }
171
+
126
172
  /**
127
173
  * Create a worksheet by converting a PDF (URL) into structured questions.
128
174
  * Uses the `pdfToWorksheet` pipeline to do inference + persistence.
@@ -1,25 +0,0 @@
1
- interface notificationData {
2
- title: string;
3
- content: string;
4
- }
5
- export declare function sendNotification(receiver: string, data: notificationData): Promise<{
6
- id: string;
7
- title: string;
8
- content: string;
9
- createdAt: Date;
10
- read: boolean;
11
- senderId: string | null;
12
- receiverId: string;
13
- }>;
14
- export declare function sendNotifications(receiverIds: Array<string>, data: notificationData): Promise<import(".prisma/client").Prisma.BatchPayload>;
15
- export declare function markRead(id: string, read?: boolean): Promise<{
16
- id: string;
17
- title: string;
18
- content: string;
19
- createdAt: Date;
20
- read: boolean;
21
- senderId: string | null;
22
- receiverId: string;
23
- }>;
24
- export {};
25
- //# sourceMappingURL=notificationHandler.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"notificationHandler.d.ts","sourceRoot":"/","sources":["lib/notificationHandler.ts"],"names":[],"mappings":"AAEA,UAAU,gBAAgB;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAA;CAClB;AAED,wBAAsB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB;;;;;;;;GAS9E;AAED,wBAAsB,iBAAiB,CAAC,WAAW,EAAE,KAAK,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,gBAAgB,yDASzF;AAED,wBAAsB,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,GAAE,OAAc;;;;;;;;GAM9D"}
@@ -1,32 +0,0 @@
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]="e0e01d37-e6fd-5751-a9d2-fece72c46422")}catch(e){}}();
3
- import { prisma } from "./prisma.js";
4
- export async function sendNotification(receiver, data) {
5
- const notification = await prisma.notification.create({
6
- data: {
7
- receiverId: receiver,
8
- title: data.title,
9
- content: data.content,
10
- },
11
- });
12
- return notification;
13
- }
14
- export async function sendNotifications(receiverIds, data) {
15
- const notifications = await prisma.notification.createMany({
16
- data: receiverIds.map(receiverId => ({
17
- receiverId: receiverId,
18
- title: data.title,
19
- content: data.content,
20
- })),
21
- });
22
- return notifications;
23
- }
24
- export async function markRead(id, read = true) {
25
- const notification = await prisma.notification.update({
26
- where: { id },
27
- data: { read: read },
28
- });
29
- return notification;
30
- }
31
- //# sourceMappingURL=notificationHandler.js.map
32
- //# debugId=e0e01d37-e6fd-5751-a9d2-fece72c46422
@@ -1 +0,0 @@
1
- {"version":3,"file":"notificationHandler.js","sources":["lib/notificationHandler.ts"],"sourceRoot":"/","sourcesContent":["import { prisma } from \"./prisma.js\";\n\ninterface notificationData {\n title: string,\n content: string\n}\n\nexport async function sendNotification(receiver: string, data: notificationData) {\n const notification = await prisma.notification.create({\n data: {\n receiverId: receiver,\n title: data.title,\n content: data.content,\n },\n });\n return notification;\n}\n\nexport async function sendNotifications(receiverIds: Array<string>, data: notificationData) {\n const notifications = await prisma.notification.createMany({\n data: receiverIds.map(receiverId => ({\n receiverId: receiverId,\n title: data.title,\n content: data.content,\n })),\n });\n return notifications;\n}\n\nexport async function markRead(id: string, read: boolean = true) {\n const notification = await prisma.notification.update({\n where: {id},\n data: {read: read},\n });\n return notification;\n}\n"],"names":[],"mappings":";;AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAOrC,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,QAAgB,EAAE,IAAsB;IAC3E,MAAM,YAAY,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC;QAClD,IAAI,EAAE;YACF,UAAU,EAAE,QAAQ;YACpB,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,OAAO,EAAE,IAAI,CAAC,OAAO;SACxB;KACJ,CAAC,CAAC;IACH,OAAO,YAAY,CAAC;AACxB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,WAA0B,EAAE,IAAsB;IACtF,MAAM,aAAa,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,UAAU,CAAC;QACvD,IAAI,EAAE,WAAW,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;YACjC,UAAU,EAAE,UAAU;YACtB,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,OAAO,EAAE,IAAI,CAAC,OAAO;SACxB,CAAC,CAAC;KACN,CAAC,CAAC;IACH,OAAO,aAAa,CAAC;AACzB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,EAAU,EAAE,OAAgB,IAAI;IAC3D,MAAM,YAAY,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC;QAClD,KAAK,EAAE,EAAC,EAAE,EAAC;QACX,IAAI,EAAE,EAAC,IAAI,EAAE,IAAI,EAAC;KACrB,CAAC,CAAC;IACH,OAAO,YAAY,CAAC;AACxB,CAAC","debug_id":"e0e01d37-e6fd-5751-a9d2-fece72c46422"}
@@ -1,21 +0,0 @@
1
- import { Assignment, Class, File, Section, User } from "@prisma/client";
2
- export declare const getBaseSystemPrompt: (context: Class, members: User[], assignments: Assignment[], files: File[], sections: Section[]) => string;
3
- /**
4
- * Generate labchat responses
5
- * Allow for the generation of the following:
6
- * - Assignment(s) either individual or bulk as an lesson / course plan.
7
- * - Worksheet(s) either individual or bulk as an lesson / course plan.
8
- * - Files (PDFs)
9
- * @param labChatId
10
- */
11
- /**
12
- * Generate and send AI introduction for lab chat
13
- * Uses the stored context directly from database
14
- */
15
- export declare const generateAndSendLabIntroduction: (labChatId: string, conversationId: string, contextString: string, subject: string) => Promise<void>;
16
- /**
17
- * Generate and send AI response to teacher message
18
- * Uses the stored context directly from database
19
- */
20
- export declare const generateAndSendLabResponse: (labChatId: string, teacherMessage: string, conversationId: string) => Promise<void>;
21
- //# sourceMappingURL=aiLabChat.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"aiLabChat.d.ts","sourceRoot":"/","sources":["server/pipelines/aiLabChat.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AA4ExE,eAAO,MAAM,mBAAmB,GAAI,SAAS,KAAK,EAAE,SAAS,IAAI,EAAE,EAAE,aAAa,UAAU,EAAE,EAAE,OAAO,IAAI,EAAE,EAAE,UAAU,OAAO,EAAE,WA6BjI,CAAA;AAID;;;;;;;GAOG;AAoBH;;;GAGG;AACH,eAAO,MAAM,8BAA8B,GACvC,WAAW,MAAM,EACjB,gBAAgB,MAAM,EACtB,eAAe,MAAM,EACrB,SAAS,MAAM,KACd,OAAO,CAAC,IAAI,CA2Dd,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,0BAA0B,GACrC,WAAW,MAAM,EACjB,gBAAgB,MAAM,EACtB,gBAAgB,MAAM,KACrB,OAAO,CAAC,IAAI,CAkSd,CAAA"}