@studious-lms/server 1.19.0 → 1.21.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.
Files changed (155) hide show
  1. package/Dockerfile +5 -2
  2. package/dist/index.js +8 -2
  3. package/dist/index.js.map +1 -1
  4. package/dist/lib/pusher.d.ts +3 -1
  5. package/dist/lib/pusher.d.ts.map +1 -1
  6. package/dist/lib/pusher.js +5 -3
  7. package/dist/lib/pusher.js.map +1 -1
  8. package/dist/middleware/security.d.ts.map +1 -1
  9. package/dist/middleware/security.js +4 -4
  10. package/dist/middleware/security.js.map +1 -1
  11. package/dist/models/class.d.ts +1 -1
  12. package/dist/models/class.d.ts.map +1 -1
  13. package/dist/models/class.js +5 -4
  14. package/dist/models/class.js.map +1 -1
  15. package/dist/models/conversation.d.ts +17 -17
  16. package/dist/models/notification.d.ts +63 -11
  17. package/dist/models/notification.d.ts.map +1 -1
  18. package/dist/models/notification.js +24 -10
  19. package/dist/models/notification.js.map +1 -1
  20. package/dist/pipelines/aiLabChat.d.ts +1 -0
  21. package/dist/pipelines/aiLabChat.d.ts.map +1 -1
  22. package/dist/pipelines/aiLabChat.js +7 -2
  23. package/dist/pipelines/aiLabChat.js.map +1 -1
  24. package/dist/pipelines/aiNewtonChat.d.ts +2 -2
  25. package/dist/pipelines/aiNewtonChat.d.ts.map +1 -1
  26. package/dist/pipelines/aiNewtonChat.js +12 -4
  27. package/dist/pipelines/aiNewtonChat.js.map +1 -1
  28. package/dist/pipelines/gradeWorksheet.d.ts +3 -3
  29. package/dist/pipelines/gradeWorksheet.d.ts.map +1 -1
  30. package/dist/pipelines/gradeWorksheet.js +13 -9
  31. package/dist/pipelines/gradeWorksheet.js.map +1 -1
  32. package/dist/pipelines/pdfToWorksheet.d.ts +2 -0
  33. package/dist/pipelines/pdfToWorksheet.d.ts.map +1 -1
  34. package/dist/pipelines/pdfToWorksheet.js +3 -2
  35. package/dist/pipelines/pdfToWorksheet.js.map +1 -1
  36. package/dist/routers/_app.d.ts +322 -82
  37. package/dist/routers/_app.d.ts.map +1 -1
  38. package/dist/routers/announcement.d.ts.map +1 -1
  39. package/dist/routers/announcement.js +2 -2
  40. package/dist/routers/announcement.js.map +1 -1
  41. package/dist/routers/assignment.d.ts +74 -0
  42. package/dist/routers/assignment.d.ts.map +1 -1
  43. package/dist/routers/assignment.js +190 -2
  44. package/dist/routers/assignment.js.map +1 -1
  45. package/dist/routers/notifications.d.ts +51 -41
  46. package/dist/routers/notifications.d.ts.map +1 -1
  47. package/dist/routers/notifications.js +22 -18
  48. package/dist/routers/notifications.js.map +1 -1
  49. package/dist/routers/school.d.ts +30 -0
  50. package/dist/routers/school.d.ts.map +1 -1
  51. package/dist/routers/school.js +11 -2
  52. package/dist/routers/school.js.map +1 -1
  53. package/dist/routers/user.d.ts +12 -0
  54. package/dist/routers/user.d.ts.map +1 -1
  55. package/dist/routers/user.js +11 -2
  56. package/dist/routers/user.js.map +1 -1
  57. package/dist/seedDatabase.d.ts +4 -0
  58. package/dist/seedDatabase.d.ts.map +1 -1
  59. package/dist/services/aiChatStream.d.ts +5 -0
  60. package/dist/services/aiChatStream.d.ts.map +1 -1
  61. package/dist/services/aiChatStream.js +32 -3
  62. package/dist/services/aiChatStream.js.map +1 -1
  63. package/dist/services/aiStream.d.ts +5 -0
  64. package/dist/services/aiStream.d.ts.map +1 -1
  65. package/dist/services/aiStream.js +29 -3
  66. package/dist/services/aiStream.js.map +1 -1
  67. package/dist/services/aiUsage.d.ts +72 -0
  68. package/dist/services/aiUsage.d.ts.map +1 -0
  69. package/dist/services/aiUsage.js +150 -0
  70. package/dist/services/aiUsage.js.map +1 -0
  71. package/dist/services/announcement.d.ts.map +1 -1
  72. package/dist/services/announcement.js +7 -2
  73. package/dist/services/announcement.js.map +1 -1
  74. package/dist/services/assignment.d.ts.map +1 -1
  75. package/dist/services/assignment.js +29 -4
  76. package/dist/services/assignment.js.map +1 -1
  77. package/dist/services/attendance.js +10 -2
  78. package/dist/services/attendance.js.map +1 -1
  79. package/dist/services/class.d.ts.map +1 -1
  80. package/dist/services/class.js +6 -2
  81. package/dist/services/class.js.map +1 -1
  82. package/dist/services/discussion.d.ts.map +1 -1
  83. package/dist/services/discussion.js +41 -12
  84. package/dist/services/discussion.js.map +1 -1
  85. package/dist/services/discussionSummary.d.ts.map +1 -1
  86. package/dist/services/discussionSummary.js +3 -2
  87. package/dist/services/discussionSummary.js.map +1 -1
  88. package/dist/services/notification.d.ts +79 -16
  89. package/dist/services/notification.d.ts.map +1 -1
  90. package/dist/services/notification.js +64 -16
  91. package/dist/services/notification.js.map +1 -1
  92. package/dist/services/studentProgress.d.ts.map +1 -1
  93. package/dist/services/studentProgress.js +6 -2
  94. package/dist/services/studentProgress.js.map +1 -1
  95. package/dist/services/userPrivacy.d.ts +4 -0
  96. package/dist/services/userPrivacy.d.ts.map +1 -1
  97. package/dist/services/userPrivacy.js +6 -2
  98. package/dist/services/userPrivacy.js.map +1 -1
  99. package/dist/services/worksheet.js +4 -4
  100. package/dist/services/worksheet.js.map +1 -1
  101. package/dist/utils/inference.d.ts +27 -0
  102. package/dist/utils/inference.d.ts.map +1 -1
  103. package/dist/utils/inference.js +23 -3
  104. package/dist/utils/inference.js.map +1 -1
  105. package/dist/workers/processors/aiLabChat.d.ts.map +1 -1
  106. package/dist/workers/processors/aiLabChat.js +4 -3
  107. package/dist/workers/processors/aiLabChat.js.map +1 -1
  108. package/dist/workers/processors/aiNewtonChat.d.ts.map +1 -1
  109. package/dist/workers/processors/aiNewtonChat.js +5 -5
  110. package/dist/workers/processors/aiNewtonChat.js.map +1 -1
  111. package/dist/workers/processors/gradeWorksheet.js +4 -4
  112. package/dist/workers/processors/gradeWorksheet.js.map +1 -1
  113. package/dist/workers/processors/pdfToWorksheet.d.ts.map +1 -1
  114. package/dist/workers/processors/pdfToWorksheet.js +7 -4
  115. package/dist/workers/processors/pdfToWorksheet.js.map +1 -1
  116. package/docker-entrypoint.sh +7 -0
  117. package/package.json +1 -1
  118. package/prisma/migrations/20260708011456_add_file_annotations/migration.sql +35 -0
  119. package/prisma/migrations/20260708011500_add_ai_usage_attribution/migration.sql +13 -0
  120. package/prisma/migrations/20260708013000_add_notification_type_and_entity/migration.sql +14 -0
  121. package/prisma/schema.prisma +99 -43
  122. package/src/index.ts +7 -0
  123. package/src/lib/pusher.ts +3 -1
  124. package/src/middleware/security.ts +2 -2
  125. package/src/models/class.ts +3 -2
  126. package/src/models/notification.ts +42 -13
  127. package/src/pipelines/aiLabChat.ts +6 -1
  128. package/src/pipelines/aiNewtonChat.ts +10 -0
  129. package/src/pipelines/gradeWorksheet.ts +25 -7
  130. package/src/pipelines/pdfToWorksheet.ts +3 -0
  131. package/src/routers/announcement.ts +0 -1
  132. package/src/routers/assignment.ts +214 -0
  133. package/src/routers/notifications.ts +26 -24
  134. package/src/routers/school.ts +12 -0
  135. package/src/routers/user.ts +12 -0
  136. package/src/services/aiChatStream.ts +35 -0
  137. package/src/services/aiStream.ts +34 -0
  138. package/src/services/aiUsage.ts +202 -0
  139. package/src/services/announcement.ts +5 -0
  140. package/src/services/assignment.ts +29 -2
  141. package/src/services/attendance.ts +12 -0
  142. package/src/services/class.ts +4 -0
  143. package/src/services/discussion.ts +50 -11
  144. package/src/services/discussionSummary.ts +1 -0
  145. package/src/services/notification.ts +83 -20
  146. package/src/services/studentProgress.ts +4 -0
  147. package/src/services/userPrivacy.ts +4 -0
  148. package/src/services/worksheet.ts +2 -2
  149. package/src/utils/inference.ts +40 -2
  150. package/src/workers/processors/aiLabChat.ts +2 -1
  151. package/src/workers/processors/aiNewtonChat.ts +3 -1
  152. package/src/workers/processors/gradeWorksheet.ts +2 -2
  153. package/src/workers/processors/pdfToWorksheet.ts +5 -2
  154. package/tests/integration/routers/notification.test.ts +68 -20
  155. package/tests/unit/services/aiUsage.test.ts +105 -0
@@ -1,5 +1,5 @@
1
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]="1acdee19-3e17-56ee-ab44-55001b9460f2")}catch(e){}}();
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]="777a2189-86f3-5317-9904-cac13606cce2")}catch(e){}}();
3
3
  import { gradeWorksheetPipeline } from '../../pipelines/gradeWorksheet.js';
4
4
  import { createRunJobWorker } from './createRunJobWorker.js';
5
5
  export const gradeWorksheetWorker = createRunJobWorker({
@@ -10,10 +10,10 @@ export const gradeWorksheetWorker = createRunJobWorker({
10
10
  // throughput modest until we measure real-world usage.
11
11
  limiter: { max: 5, duration: 60000 },
12
12
  },
13
- run: async ({ worksheetResponseId }) => {
14
- await gradeWorksheetPipeline(worksheetResponseId);
13
+ run: async ({ worksheetResponseId }, ctx) => {
14
+ await gradeWorksheetPipeline(worksheetResponseId, ctx.userId);
15
15
  return { worksheetResponseId };
16
16
  },
17
17
  });
18
18
  //# sourceMappingURL=gradeWorksheet.js.map
19
- //# debugId=1acdee19-3e17-56ee-ab44-55001b9460f2
19
+ //# debugId=777a2189-86f3-5317-9904-cac13606cce2
@@ -1 +1 @@
1
- {"version":3,"file":"gradeWorksheet.js","sources":["workers/processors/gradeWorksheet.ts"],"sourceRoot":"/","sourcesContent":["import { gradeWorksheetPipeline } from '../../pipelines/gradeWorksheet.js';\nimport { createRunJobWorker } from './createRunJobWorker.js';\n\nexport const gradeWorksheetWorker = createRunJobWorker({\n type: 'GRADE_WORKSHEET',\n workerOptions: {\n concurrency: 2,\n // Each pipeline run can fan out to many per-question inferences; keep\n // throughput modest until we measure real-world usage.\n limiter: { max: 5, duration: 60_000 },\n },\n run: async ({ worksheetResponseId }) => {\n await gradeWorksheetPipeline(worksheetResponseId);\n return { worksheetResponseId };\n },\n});\n"],"names":[],"mappings":";;AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,mCAAmC,CAAC;AAC3E,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAE7D,MAAM,CAAC,MAAM,oBAAoB,GAAG,kBAAkB,CAAC;IACrD,IAAI,EAAE,iBAAiB;IACvB,aAAa,EAAE;QACb,WAAW,EAAE,CAAC;QACd,sEAAsE;QACtE,uDAAuD;QACvD,OAAO,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,QAAQ,EAAE,KAAM,EAAE;KACtC;IACD,GAAG,EAAE,KAAK,EAAE,EAAE,mBAAmB,EAAE,EAAE,EAAE;QACrC,MAAM,sBAAsB,CAAC,mBAAmB,CAAC,CAAC;QAClD,OAAO,EAAE,mBAAmB,EAAE,CAAC;IACjC,CAAC;CACF,CAAC,CAAC","debug_id":"1acdee19-3e17-56ee-ab44-55001b9460f2"}
1
+ {"version":3,"file":"gradeWorksheet.js","sources":["workers/processors/gradeWorksheet.ts"],"sourceRoot":"/","sourcesContent":["import { gradeWorksheetPipeline } from '../../pipelines/gradeWorksheet.js';\nimport { createRunJobWorker } from './createRunJobWorker.js';\n\nexport const gradeWorksheetWorker = createRunJobWorker({\n type: 'GRADE_WORKSHEET',\n workerOptions: {\n concurrency: 2,\n // Each pipeline run can fan out to many per-question inferences; keep\n // throughput modest until we measure real-world usage.\n limiter: { max: 5, duration: 60_000 },\n },\n run: async ({ worksheetResponseId }, ctx) => {\n await gradeWorksheetPipeline(worksheetResponseId, ctx.userId);\n return { worksheetResponseId };\n },\n});\n"],"names":[],"mappings":";;AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,mCAAmC,CAAC;AAC3E,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAE7D,MAAM,CAAC,MAAM,oBAAoB,GAAG,kBAAkB,CAAC;IACrD,IAAI,EAAE,iBAAiB;IACvB,aAAa,EAAE;QACb,WAAW,EAAE,CAAC;QACd,sEAAsE;QACtE,uDAAuD;QACvD,OAAO,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,QAAQ,EAAE,KAAM,EAAE;KACtC;IACD,GAAG,EAAE,KAAK,EAAE,EAAE,mBAAmB,EAAE,EAAE,GAAG,EAAE,EAAE;QAC1C,MAAM,sBAAsB,CAAC,mBAAmB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;QAC9D,OAAO,EAAE,mBAAmB,EAAE,CAAC;IACjC,CAAC;CACF,CAAC,CAAC","debug_id":"777a2189-86f3-5317-9904-cac13606cce2"}
@@ -1 +1 @@
1
- {"version":3,"file":"pdfToWorksheet.d.ts","sourceRoot":"/","sources":["workers/processors/pdfToWorksheet.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,oBAAoB;;;;;;eAa/B,CAAC"}
1
+ {"version":3,"file":"pdfToWorksheet.d.ts","sourceRoot":"/","sources":["workers/processors/pdfToWorksheet.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,oBAAoB;;;;;;eAgB/B,CAAC"}
@@ -1,5 +1,5 @@
1
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]="3beb4788-6c75-5aae-b6c1-3195cbdeffa3")}catch(e){}}();
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]="83752754-be47-54a0-b136-1b4140250056")}catch(e){}}();
3
3
  import { pdfToWorksheet } from '../../pipelines/pdfToWorksheet.js';
4
4
  import { createRunJobWorker } from './createRunJobWorker.js';
5
5
  export const pdfToWorksheetWorker = createRunJobWorker({
@@ -9,12 +9,15 @@ export const pdfToWorksheetWorker = createRunJobWorker({
9
9
  // 5 jobs / minute, e.g. to stay under OpenAI rate limits.
10
10
  limiter: { max: 5, duration: 60000 },
11
11
  },
12
- run: async ({ gcsUrl, classId, worksheetId }) => {
13
- const worksheet = await pdfToWorksheet(gcsUrl, classId, { worksheetId });
12
+ run: async ({ gcsUrl, classId, worksheetId }, ctx) => {
13
+ const worksheet = await pdfToWorksheet(gcsUrl, classId, {
14
+ worksheetId,
15
+ userId: ctx.userId,
16
+ });
14
17
  return worksheet
15
18
  ? { worksheetId: worksheet.id, name: worksheet.name }
16
19
  : null;
17
20
  },
18
21
  });
19
22
  //# sourceMappingURL=pdfToWorksheet.js.map
20
- //# debugId=3beb4788-6c75-5aae-b6c1-3195cbdeffa3
23
+ //# debugId=83752754-be47-54a0-b136-1b4140250056
@@ -1 +1 @@
1
- {"version":3,"file":"pdfToWorksheet.js","sources":["workers/processors/pdfToWorksheet.ts"],"sourceRoot":"/","sourcesContent":["import { pdfToWorksheet } from '../../pipelines/pdfToWorksheet.js';\nimport { createRunJobWorker } from './createRunJobWorker.js';\n\nexport const pdfToWorksheetWorker = createRunJobWorker({\n type: 'PDF_TO_WORKSHEET',\n workerOptions: {\n concurrency: 2,\n // 5 jobs / minute, e.g. to stay under OpenAI rate limits.\n limiter: { max: 5, duration: 60_000 },\n },\n run: async ({ gcsUrl, classId, worksheetId }) => {\n const worksheet = await pdfToWorksheet(gcsUrl, classId, { worksheetId });\n return worksheet\n ? { worksheetId: worksheet.id, name: worksheet.name }\n : null;\n },\n});\n"],"names":[],"mappings":";;AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AACnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAE7D,MAAM,CAAC,MAAM,oBAAoB,GAAG,kBAAkB,CAAC;IACrD,IAAI,EAAE,kBAAkB;IACxB,aAAa,EAAE;QACb,WAAW,EAAE,CAAC;QACd,0DAA0D;QAC1D,OAAO,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,QAAQ,EAAE,KAAM,EAAE;KACtC;IACD,GAAG,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,EAAE,EAAE;QAC9C,MAAM,SAAS,GAAG,MAAM,cAAc,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE,WAAW,EAAE,CAAC,CAAC;QACzE,OAAO,SAAS;YACd,CAAC,CAAC,EAAE,WAAW,EAAE,SAAS,CAAC,EAAE,EAAE,IAAI,EAAE,SAAS,CAAC,IAAI,EAAE;YACrD,CAAC,CAAC,IAAI,CAAC;IACX,CAAC;CACF,CAAC,CAAC","debug_id":"3beb4788-6c75-5aae-b6c1-3195cbdeffa3"}
1
+ {"version":3,"file":"pdfToWorksheet.js","sources":["workers/processors/pdfToWorksheet.ts"],"sourceRoot":"/","sourcesContent":["import { pdfToWorksheet } from '../../pipelines/pdfToWorksheet.js';\nimport { createRunJobWorker } from './createRunJobWorker.js';\n\nexport const pdfToWorksheetWorker = createRunJobWorker({\n type: 'PDF_TO_WORKSHEET',\n workerOptions: {\n concurrency: 2,\n // 5 jobs / minute, e.g. to stay under OpenAI rate limits.\n limiter: { max: 5, duration: 60_000 },\n },\n run: async ({ gcsUrl, classId, worksheetId }, ctx) => {\n const worksheet = await pdfToWorksheet(gcsUrl, classId, {\n worksheetId,\n userId: ctx.userId,\n });\n return worksheet\n ? { worksheetId: worksheet.id, name: worksheet.name }\n : null;\n },\n});\n"],"names":[],"mappings":";;AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AACnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAE7D,MAAM,CAAC,MAAM,oBAAoB,GAAG,kBAAkB,CAAC;IACrD,IAAI,EAAE,kBAAkB;IACxB,aAAa,EAAE;QACb,WAAW,EAAE,CAAC;QACd,0DAA0D;QAC1D,OAAO,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,QAAQ,EAAE,KAAM,EAAE;KACtC;IACD,GAAG,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,EAAE,GAAG,EAAE,EAAE;QACnD,MAAM,SAAS,GAAG,MAAM,cAAc,CAAC,MAAM,EAAE,OAAO,EAAE;YACtD,WAAW;YACX,MAAM,EAAE,GAAG,CAAC,MAAM;SACnB,CAAC,CAAC;QACH,OAAO,SAAS;YACd,CAAC,CAAC,EAAE,WAAW,EAAE,SAAS,CAAC,EAAE,EAAE,IAAI,EAAE,SAAS,CAAC,IAAI,EAAE;YACrD,CAAC,CAAC,IAAI,CAAC;IACX,CAAC;CACF,CAAC,CAAC","debug_id":"83752754-be47-54a0-b136-1b4140250056"}
@@ -0,0 +1,7 @@
1
+ #!/bin/sh
2
+ set -e
3
+
4
+ # Apply pending database migrations before starting the API.
5
+ ./node_modules/.bin/prisma migrate deploy
6
+
7
+ exec node dist/index.js
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@studious-lms/server",
3
- "version": "1.19.0",
3
+ "version": "1.21.0",
4
4
  "description": "Backend server for Studious application",
5
5
  "main": "dist/exportType.js",
6
6
  "types": "dist/exportType.d.ts",
@@ -0,0 +1,35 @@
1
+ -- CreateEnum
2
+ CREATE TYPE "FileAnnotationType" AS ENUM ('HIGHLIGHT', 'INK', 'COMMENT', 'TEXT');
3
+
4
+ -- CreateTable
5
+ CREATE TABLE "FileAnnotation" (
6
+ "id" TEXT NOT NULL,
7
+ "fileId" TEXT NOT NULL,
8
+ "submissionId" TEXT NOT NULL,
9
+ "authorId" TEXT NOT NULL,
10
+ "page" INTEGER NOT NULL,
11
+ "type" "FileAnnotationType" NOT NULL,
12
+ "geometry" JSONB NOT NULL,
13
+ "text" TEXT,
14
+ "color" TEXT,
15
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
16
+ "updatedAt" TIMESTAMP(3) NOT NULL,
17
+
18
+ CONSTRAINT "FileAnnotation_pkey" PRIMARY KEY ("id")
19
+ );
20
+
21
+ -- CreateIndex
22
+ CREATE INDEX "FileAnnotation_fileId_idx" ON "FileAnnotation"("fileId");
23
+
24
+ -- CreateIndex
25
+ CREATE INDEX "FileAnnotation_submissionId_idx" ON "FileAnnotation"("submissionId");
26
+
27
+ -- AddForeignKey
28
+ ALTER TABLE "FileAnnotation" ADD CONSTRAINT "FileAnnotation_fileId_fkey" FOREIGN KEY ("fileId") REFERENCES "File"("id") ON DELETE CASCADE ON UPDATE CASCADE;
29
+
30
+ -- AddForeignKey
31
+ ALTER TABLE "FileAnnotation" ADD CONSTRAINT "FileAnnotation_submissionId_fkey" FOREIGN KEY ("submissionId") REFERENCES "Submission"("id") ON DELETE CASCADE ON UPDATE CASCADE;
32
+
33
+ -- AddForeignKey
34
+ ALTER TABLE "FileAnnotation" ADD CONSTRAINT "FileAnnotation_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
35
+
@@ -0,0 +1,13 @@
1
+ -- AlterTable
2
+ ALTER TABLE "AiUsageLog" ADD COLUMN "userId" TEXT,
3
+ ADD COLUMN "schoolId" TEXT,
4
+ ADD COLUMN "classId" TEXT;
5
+
6
+ -- CreateIndex
7
+ CREATE INDEX "AiUsageLog_userId_createdAt_idx" ON "AiUsageLog"("userId", "createdAt");
8
+
9
+ -- CreateIndex
10
+ CREATE INDEX "AiUsageLog_schoolId_createdAt_idx" ON "AiUsageLog"("schoolId", "createdAt");
11
+
12
+ -- CreateIndex
13
+ CREATE INDEX "AiUsageLog_classId_createdAt_idx" ON "AiUsageLog"("classId", "createdAt");
@@ -0,0 +1,14 @@
1
+ -- CreateEnum
2
+ CREATE TYPE "NotificationType" AS ENUM ('ASSIGNMENT_POSTED', 'ASSIGNMENT_RETURNED', 'COMMENT_REPLY', 'DISCUSSION_REPLY', 'DISCUSSION_MENTION', 'ANNOUNCEMENT', 'ATTENDANCE', 'SYSTEM');
3
+
4
+ -- AlterTable
5
+ ALTER TABLE "Notification" ADD COLUMN "type" "NotificationType" NOT NULL DEFAULT 'SYSTEM',
6
+ ADD COLUMN "entityType" TEXT,
7
+ ADD COLUMN "entityId" TEXT,
8
+ ADD COLUMN "classId" TEXT;
9
+
10
+ -- CreateIndex
11
+ CREATE INDEX "Notification_receiverId_createdAt_idx" ON "Notification"("receiverId", "createdAt");
12
+
13
+ -- CreateIndex
14
+ CREATE INDEX "Notification_receiverId_read_idx" ON "Notification"("receiverId", "read");
@@ -128,14 +128,14 @@ model School {
128
128
  createdAt DateTime @default(now())
129
129
  updatedAt DateTime @updatedAt
130
130
 
131
- users User[]
132
- memberships SchoolMembership[]
133
- invites SchoolInvite[]
134
- classes Class[]
135
- departments Department[]
136
- tags Tag[]
137
- storageRoot Folder? @relation("SchoolStorageRoot")
138
- permissions Permission[] @relation("SchoolPermissions")
131
+ users User[]
132
+ memberships SchoolMembership[]
133
+ invites SchoolInvite[]
134
+ classes Class[]
135
+ departments Department[]
136
+ tags Tag[]
137
+ storageRoot Folder? @relation("SchoolStorageRoot")
138
+ permissions Permission[] @relation("SchoolPermissions")
139
139
  }
140
140
 
141
141
  model Department {
@@ -175,13 +175,13 @@ model Tag {
175
175
  createdAt DateTime @default(now())
176
176
  updatedAt DateTime @updatedAt
177
177
 
178
- school School @relation(fields: [schoolId], references: [id], onDelete: Cascade)
179
- department Department? @relation(fields: [departmentId], references: [id], onDelete: SetNull)
180
- users User[] @relation("UserTags")
181
- classes Class[] @relation("ClassTags")
182
- rubrics MarkScheme[] @relation("MarkSchemeTags")
183
- boundaries GradingBoundary[] @relation("GradingBoundaryTags")
184
- permissions Permission[] @relation("TagPermissions")
178
+ school School @relation(fields: [schoolId], references: [id], onDelete: Cascade)
179
+ department Department? @relation(fields: [departmentId], references: [id], onDelete: SetNull)
180
+ users User[] @relation("UserTags")
181
+ classes Class[] @relation("ClassTags")
182
+ rubrics MarkScheme[] @relation("MarkSchemeTags")
183
+ boundaries GradingBoundary[] @relation("GradingBoundaryTags")
184
+ permissions Permission[] @relation("TagPermissions")
185
185
 
186
186
  @@unique([schoolId, name])
187
187
  @@index([schoolId])
@@ -197,7 +197,7 @@ model User {
197
197
  verified Boolean @default(false)
198
198
 
199
199
  termsAcceptedAt DateTime?
200
- termsVersion String?
200
+ termsVersion String?
201
201
 
202
202
  teacherIn Class[] @relation("UserTeacherToClass")
203
203
  studentIn Class[] @relation("UserStudentToClass")
@@ -241,6 +241,7 @@ model User {
241
241
  discussions Discussion[]
242
242
  discussionReplies DiscussionReply[]
243
243
  discussionReads DiscussionRead[]
244
+ fileAnnotations FileAnnotation[] @relation("AuthoredFileAnnotations")
244
245
  }
245
246
 
246
247
  model SchoolMembership {
@@ -330,9 +331,9 @@ model Class {
330
331
  classTags ClassTag[]
331
332
  embeddings Embedding[]
332
333
 
333
- discussionsEnabled Boolean @default(true)
334
- studentsCanCreateDiscussions Boolean @default(true)
335
- studentsCanReply Boolean @default(true)
334
+ discussionsEnabled Boolean @default(true)
335
+ studentsCanCreateDiscussions Boolean @default(true)
336
+ studentsCanReply Boolean @default(true)
336
337
 
337
338
  @@index([schoolId])
338
339
  @@index([departmentId])
@@ -454,6 +455,35 @@ model File {
454
455
 
455
456
  schoolDevelopementProgram SchoolDevelopementProgram? @relation("SchoolDevelopementProgramSupportingDocumentation", fields: [schoolDevelopementProgramId], references: [id], onDelete: Cascade)
456
457
  schoolDevelopementProgramId String?
458
+
459
+ fileAnnotations FileAnnotation[] @relation("FileAnnotations")
460
+ }
461
+
462
+ enum FileAnnotationType {
463
+ HIGHLIGHT
464
+ INK
465
+ COMMENT
466
+ TEXT
467
+ }
468
+
469
+ model FileAnnotation {
470
+ id String @id @default(uuid())
471
+ file File @relation("FileAnnotations", fields: [fileId], references: [id], onDelete: Cascade)
472
+ fileId String
473
+ submission Submission @relation("SubmissionFileAnnotations", fields: [submissionId], references: [id], onDelete: Cascade)
474
+ submissionId String
475
+ author User @relation("AuthoredFileAnnotations", fields: [authorId], references: [id], onDelete: Cascade)
476
+ authorId String
477
+ page Int
478
+ type FileAnnotationType
479
+ geometry Json
480
+ text String?
481
+ color String?
482
+ createdAt DateTime @default(now())
483
+ updatedAt DateTime @updatedAt
484
+
485
+ @@index([fileId])
486
+ @@index([submissionId])
457
487
  }
458
488
 
459
489
  model Assignment {
@@ -607,19 +637,19 @@ model DiscussionRead {
607
637
  }
608
638
 
609
639
  model Reaction {
610
- id String @id @default(uuid())
611
- type ReactionType
612
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
613
- userId String
614
- announcement Announcement? @relation(fields: [announcementId], references: [id], onDelete: Cascade)
615
- announcementId String?
616
- comment Comment? @relation(fields: [commentId], references: [id], onDelete: Cascade)
617
- commentId String?
640
+ id String @id @default(uuid())
641
+ type ReactionType
642
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
643
+ userId String
644
+ announcement Announcement? @relation(fields: [announcementId], references: [id], onDelete: Cascade)
645
+ announcementId String?
646
+ comment Comment? @relation(fields: [commentId], references: [id], onDelete: Cascade)
647
+ commentId String?
618
648
  discussion Discussion? @relation(fields: [discussionId], references: [id], onDelete: Cascade)
619
649
  discussionId String?
620
650
  discussionReply DiscussionReply? @relation(fields: [discussionReplyId], references: [id], onDelete: Cascade)
621
651
  discussionReplyId String?
622
- createdAt DateTime @default(now())
652
+ createdAt DateTime @default(now())
623
653
 
624
654
  @@unique([userId, announcementId])
625
655
  @@unique([userId, commentId])
@@ -661,6 +691,8 @@ model Submission {
661
691
  returned Boolean? @default(false)
662
692
  newtonTutors NewtonChat[]
663
693
 
694
+ fileAnnotations FileAnnotation[] @relation("SubmissionFileAnnotations")
695
+
664
696
  @@index([studentId, recommendationState])
665
697
  }
666
698
 
@@ -734,16 +766,34 @@ model Attendance {
734
766
  absent User[] @relation("AbsentAttendance")
735
767
  }
736
768
 
769
+ enum NotificationType {
770
+ ASSIGNMENT_POSTED
771
+ ASSIGNMENT_RETURNED
772
+ COMMENT_REPLY
773
+ DISCUSSION_REPLY
774
+ DISCUSSION_MENTION
775
+ ANNOUNCEMENT
776
+ ATTENDANCE
777
+ SYSTEM
778
+ }
779
+
737
780
  model Notification {
738
- id String @id @default(uuid())
781
+ id String @id @default(uuid())
739
782
  title String
740
783
  content String
741
- createdAt DateTime @default(now())
784
+ createdAt DateTime @default(now())
742
785
  senderId String? // no ID means sys
743
786
  receiverId String
744
- read Boolean @default(false)
745
- sender User? @relation("SentNotifications", fields: [senderId], references: [id])
746
- receiver User @relation("ReceivedNotifications", fields: [receiverId], references: [id], onDelete: Cascade)
787
+ read Boolean @default(false)
788
+ type NotificationType @default(SYSTEM)
789
+ entityType String? // e.g. ASSIGNMENT, ANNOUNCEMENT, DISCUSSION, SUBMISSION
790
+ entityId String?
791
+ classId String?
792
+ sender User? @relation("SentNotifications", fields: [senderId], references: [id])
793
+ receiver User @relation("ReceivedNotifications", fields: [receiverId], references: [id], onDelete: Cascade)
794
+
795
+ @@index([receiverId, createdAt])
796
+ @@index([receiverId, read])
747
797
  }
748
798
 
749
799
  enum ConversationType {
@@ -837,9 +887,15 @@ model AiUsageLog {
837
887
  estimatedInputTokens Int?
838
888
  metadata Json? @default("{}")
839
889
  error String?
890
+ userId String?
891
+ schoolId String?
892
+ classId String?
840
893
  createdAt DateTime @default(now())
841
894
 
842
895
  @@index([reason, createdAt])
896
+ @@index([userId, createdAt])
897
+ @@index([schoolId, createdAt])
898
+ @@index([classId, createdAt])
843
899
  }
844
900
 
845
901
  model Mention {
@@ -866,11 +922,11 @@ model Worksheet {
866
922
  questions WorksheetQuestion[]
867
923
  studentWorksheetResponses StudentWorksheetResponse[]
868
924
 
869
- runJobId String?
870
- runJob RunJob? @relation("WorksheetRunJob", fields: [runJobId], references: [id], onDelete: SetNull)
925
+ runJobId String?
926
+ runJob RunJob? @relation("WorksheetRunJob", fields: [runJobId], references: [id], onDelete: SetNull)
871
927
 
872
- status JobStatus? @default(NOT_STARTED)
873
- tags ClassTag[] @relation("WorksheetToTag")
928
+ status JobStatus? @default(NOT_STARTED)
929
+ tags ClassTag[] @relation("WorksheetToTag")
874
930
 
875
931
  @@unique([classId, sourceUrl])
876
932
  }
@@ -987,14 +1043,14 @@ enum EmbeddingSourceType {
987
1043
  }
988
1044
 
989
1045
  model Embedding {
990
- id String @id @default(uuid())
1046
+ id String @id @default(uuid())
991
1047
  classId String
992
1048
  sourceType EmbeddingSourceType
993
1049
  sourceId String
994
1050
  content String
995
1051
  vector Unsupported("vector(1536)")
996
- metadata Json? @default("{}")
997
- createdAt DateTime @default(now())
1052
+ metadata Json? @default("{}")
1053
+ createdAt DateTime @default(now())
998
1054
 
999
1055
  class Class @relation(fields: [classId], references: [id], onDelete: Cascade)
1000
1056
 
@@ -1038,7 +1094,7 @@ model RunJob {
1038
1094
  /// most jobs touch zero or one of these.
1039
1095
  messages Message[] @relation("MessageRunJob")
1040
1096
  studentQuestionProgresses StudentQuestionProgress[] @relation("StudentQuestionProgressRunJob")
1041
- worksheet Worksheet[] @relation("WorksheetRunJob")
1097
+ worksheet Worksheet[] @relation("WorksheetRunJob")
1042
1098
 
1043
1099
  @@index([userId, type, status])
1044
1100
  @@index([classId, status])
@@ -1074,9 +1130,9 @@ enum PermissionLevel {
1074
1130
  /// `schoolId` is set, matching `principalType`. CLASS, CLASS_TEACHERS,
1075
1131
  /// CLASS_STUDENTS all share `classId`.
1076
1132
  model Permission {
1077
- id String @id @default(uuid())
1133
+ id String @id @default(uuid())
1078
1134
  folderId String
1079
- folder Folder @relation("FolderPermissions", fields: [folderId], references: [id], onDelete: Cascade)
1135
+ folder Folder @relation("FolderPermissions", fields: [folderId], references: [id], onDelete: Cascade)
1080
1136
 
1081
1137
  principalType PermissionPrincipalType
1082
1138
  userId String?
package/src/index.ts CHANGED
@@ -272,6 +272,13 @@ app.post('/api/pusher/auth', async (req, res) => {
272
272
  // }
273
273
  // }
274
274
 
275
+ // Per-user notification channel: only the user themselves may subscribe
276
+ if (channel_name.startsWith('private-user-')) {
277
+ if (channel_name !== `private-user-${user.id}-notifications`) {
278
+ return res.status(403).json({ error: 'No access to this user channel' });
279
+ }
280
+ }
281
+
275
282
  if (channel_name.startsWith('presence-')) {
276
283
  const authResponse = pusher.authorizeChannel(socket_id, channel_name, {
277
284
  user_id: user.id,
package/src/lib/pusher.ts CHANGED
@@ -19,5 +19,7 @@ const discussionChannel = (classId: string) => `private-class-${classId}-discuss
19
19
  * worksheet-response channel so both can co-exist without auth collisions.
20
20
  */
21
21
  const worksheetJobChannel = (worksheetId: string) => `private-worksheet-job-${worksheetId}`;
22
+ /** Per-user private channel for in-app notification pushes. */
23
+ const userNotificationChannel = (userId: string) => `private-user-${userId}-notifications`;
22
24
 
23
- export { pusher, chatChannel, worksheetChannel, teacherChannel, discussionChannel, worksheetJobChannel };
25
+ export { pusher, chatChannel, worksheetChannel, teacherChannel, discussionChannel, worksheetJobChannel, userNotificationChannel };
@@ -20,7 +20,7 @@ const rateLimitHandler = (req: Request, res: Response) => {
20
20
  // General API rate limiter - applies to all routes
21
21
  export const generalLimiter = rateLimit({
22
22
  windowMs: 10 * 60 * 1000, // 10 minutes
23
- max: 1000, // Limit each IP to 100 requests per windowMs
23
+ max: 10000, // Limit each IP to 10000 requests per windowMs (schools often share one NAT IP)
24
24
  message: 'Too many requests from this IP, please try again later.',
25
25
  standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
26
26
  legacyHeaders: false, // Disable the `X-RateLimit-*` headers
@@ -45,7 +45,7 @@ export const authLimiter = rateLimit({
45
45
  // File upload rate limiter
46
46
  export const uploadLimiter = rateLimit({
47
47
  windowMs: 30 * 60 * 1000, // 30 minutes
48
- max: 50, // Limit each IP to 50 uploads per hour
48
+ max: 500, // Limit each IP to 500 uploads per windowMs (a whole class may submit at once behind one IP)
49
49
  message: 'Too many file uploads, please try again later.',
50
50
  standardHeaders: true,
51
51
  legacyHeaders: false,
@@ -298,10 +298,11 @@ export function findSessionByClassCode(classCode: string) {
298
298
  });
299
299
  }
300
300
 
301
- /** @returns Session for a class. */
301
+ /** @returns Invite session for a class. */
302
302
  export function findSessionByClassId(classId: string) {
303
303
  return prisma.session.findFirst({
304
- where: { classId },
304
+ where: { classId, type: SessionType.CLASS_INVITE },
305
+ orderBy: { expiresAt: "desc" },
305
306
  });
306
307
  }
307
308
 
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Notification model – in-app notifications, create, mark read.
3
3
  */
4
+ import type { NotificationType } from "@prisma/client";
4
5
  import { prisma } from "../lib/prisma.js";
5
6
 
6
7
  const notificationInclude = {
@@ -8,11 +9,28 @@ const notificationInclude = {
8
9
  receiver: { select: { username: true } },
9
10
  };
10
11
 
11
- /** @returns Notifications for a receiver, newest first. */
12
- export function findNotificationsByReceiverId(receiverId: string) {
12
+ export type NotificationCreateData = {
13
+ receiverId: string;
14
+ title: string;
15
+ content: string;
16
+ senderId?: string | null;
17
+ type?: NotificationType;
18
+ entityType?: string | null;
19
+ entityId?: string | null;
20
+ classId?: string | null;
21
+ };
22
+
23
+ /** @returns A page of notifications for a receiver, newest first. */
24
+ export function findNotificationsByReceiverIdPaginated(
25
+ receiverId: string,
26
+ limit: number,
27
+ cursor?: string | null
28
+ ) {
13
29
  return prisma.notification.findMany({
14
30
  where: { receiverId },
15
- orderBy: { createdAt: "desc" },
31
+ orderBy: [{ createdAt: "desc" }, { id: "desc" }],
32
+ take: limit + 1,
33
+ ...(cursor ? { cursor: { id: cursor }, skip: 1 } : {}),
16
34
  include: notificationInclude,
17
35
  });
18
36
  }
@@ -25,23 +43,26 @@ export function findNotificationByIdForReceiver(id: string, receiverId: string)
25
43
  });
26
44
  }
27
45
 
46
+ /** @returns Number of unread notifications for a receiver. */
47
+ export function countUnreadNotifications(receiverId: string) {
48
+ return prisma.notification.count({
49
+ where: { receiverId, read: false },
50
+ });
51
+ }
52
+
28
53
  /** Create a notification for one receiver. */
29
- export function createNotification(data: {
30
- receiverId: string;
31
- title: string;
32
- content: string;
33
- }) {
54
+ export function createNotification(data: NotificationCreateData) {
34
55
  return prisma.notification.create({
35
56
  data,
57
+ include: notificationInclude,
36
58
  });
37
59
  }
38
60
 
39
- /** Create multiple notifications in one call. */
40
- export function createManyNotifications(
41
- data: Array<{ receiverId: string; title: string; content: string }>
42
- ) {
43
- return prisma.notification.createMany({
61
+ /** Create multiple notifications in one call and return the created rows. */
62
+ export function createManyNotifications(data: NotificationCreateData[]) {
63
+ return prisma.notification.createManyAndReturn({
44
64
  data,
65
+ include: notificationInclude,
45
66
  });
46
67
  }
47
68
 
@@ -58,3 +79,11 @@ export async function markNotificationAsReadForReceiver(id: string, receiverId:
58
79
 
59
80
  return findNotificationByIdForReceiver(id, receiverId);
60
81
  }
82
+
83
+ /** Mark all of a receiver's notifications as read. */
84
+ export function markAllNotificationsAsReadForReceiver(receiverId: string) {
85
+ return prisma.notification.updateMany({
86
+ where: { receiverId, read: false },
87
+ data: { read: true },
88
+ });
89
+ }
@@ -378,7 +378,7 @@ export const generateAndSendLabIntroduction = async (
378
378
  export const generateAndSendLabResponse = async (
379
379
  labChatId: string,
380
380
  teacherMessage: string,
381
- emitOptions: { classId: string; aiMessageId: string }
381
+ emitOptions: { classId: string; aiMessageId: string; userId?: string | null }
382
382
  ): Promise<void> => {
383
383
  try {
384
384
  // Get lab context from database
@@ -499,6 +499,11 @@ export const generateAndSendLabIntroduction = async (
499
499
  model: AI_CHAT_MODEL,
500
500
  system,
501
501
  messages,
502
+ usageReason: 'lab_chat_response',
503
+ attribution: {
504
+ userId: emitOptions.userId ?? fullLabChat.createdById,
505
+ classId: emitOptions.classId,
506
+ },
502
507
  tools,
503
508
  temperature: 0.7,
504
509
  maxOutputTokens: 4096,
@@ -99,6 +99,7 @@ export const generateAndSendNewtonIntroduction = async (
99
99
  conversationId: string,
100
100
  submissionId: string,
101
101
  aiMessageId: string,
102
+ actingUserId?: string | null,
102
103
  ): Promise<void> => {
103
104
  try {
104
105
  // Get submission details for context
@@ -109,6 +110,7 @@ export const generateAndSendNewtonIntroduction = async (
109
110
  select: {
110
111
  title: true,
111
112
  instructions: true,
113
+ classId: true,
112
114
  class: {
113
115
  select: {
114
116
  subject: true,
@@ -160,6 +162,11 @@ export const generateAndSendNewtonIntroduction = async (
160
162
  ],
161
163
  temperature: 0.8,
162
164
  maxOutputTokens: 400,
165
+ usageReason: 'newton_introduction',
166
+ attribution: {
167
+ userId: actingUserId,
168
+ classId: submission.assignment.classId,
169
+ },
163
170
  });
164
171
 
165
172
  if (!response) {
@@ -237,6 +244,7 @@ export const generateAndSendNewtonIntroduction = async (
237
244
  };
238
245
  },
239
246
  aiMessageId: string,
247
+ actingUserId?: string | null,
240
248
  ): Promise<void> => {
241
249
  try {
242
250
  // Get recent conversation history (excluding the empty AI placeholder
@@ -339,6 +347,8 @@ export const generateAndSendNewtonIntroduction = async (
339
347
  ...(newtonClassId ? { tools: aiTools(newtonClassId), maxSteps: 5 } : {}),
340
348
  temperature: 0.7,
341
349
  maxOutputTokens: 1200,
350
+ usageReason: 'newton_response',
351
+ attribution: { userId: actingUserId, classId: newtonClassId },
342
352
  });
343
353
 
344
354
  if (!response) {