@studious-lms/server 1.1.23 → 1.1.26

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 (43) hide show
  1. package/dist/lib/fileUpload.d.ts.map +1 -1
  2. package/dist/lib/fileUpload.js +68 -11
  3. package/dist/lib/googleCloudStorage.d.ts +7 -0
  4. package/dist/lib/googleCloudStorage.d.ts.map +1 -1
  5. package/dist/lib/googleCloudStorage.js +19 -0
  6. package/dist/lib/notificationHandler.d.ts +25 -0
  7. package/dist/lib/notificationHandler.d.ts.map +1 -0
  8. package/dist/lib/notificationHandler.js +28 -0
  9. package/dist/routers/_app.d.ts +164 -22
  10. package/dist/routers/_app.d.ts.map +1 -1
  11. package/dist/routers/announcement.d.ts.map +1 -1
  12. package/dist/routers/announcement.js +26 -2
  13. package/dist/routers/assignment.d.ts +58 -3
  14. package/dist/routers/assignment.d.ts.map +1 -1
  15. package/dist/routers/assignment.js +265 -125
  16. package/dist/routers/auth.js +1 -1
  17. package/dist/routers/file.d.ts.map +1 -1
  18. package/dist/routers/file.js +9 -6
  19. package/dist/routers/labChat.d.ts.map +1 -1
  20. package/dist/routers/labChat.js +13 -5
  21. package/dist/routers/notifications.d.ts +8 -8
  22. package/dist/routers/section.d.ts +16 -0
  23. package/dist/routers/section.d.ts.map +1 -1
  24. package/dist/routers/section.js +139 -30
  25. package/dist/seedDatabase.d.ts +2 -2
  26. package/dist/seedDatabase.d.ts.map +1 -1
  27. package/dist/seedDatabase.js +2 -1
  28. package/dist/utils/logger.d.ts +1 -0
  29. package/dist/utils/logger.d.ts.map +1 -1
  30. package/dist/utils/logger.js +27 -2
  31. package/package.json +2 -2
  32. package/src/lib/fileUpload.ts +69 -11
  33. package/src/lib/googleCloudStorage.ts +19 -0
  34. package/src/lib/notificationHandler.ts +36 -0
  35. package/src/routers/announcement.ts +30 -2
  36. package/src/routers/assignment.ts +230 -76
  37. package/src/routers/auth.ts +1 -1
  38. package/src/routers/file.ts +10 -7
  39. package/src/routers/labChat.ts +15 -6
  40. package/src/routers/section.ts +158 -36
  41. package/src/seedDatabase.ts +2 -1
  42. package/src/utils/logger.ts +29 -2
  43. package/tests/setup.ts +3 -9
@@ -64,43 +64,137 @@ export const sectionRouter = createTRPCRouter({
64
64
  },
65
65
  });
66
66
 
67
- // find all root items in the class and reorder them
68
- const sections = await prisma.section.findMany({
69
- where: {
70
- classId: input.classId,
71
- },
72
- });
67
+ // Insert new section at top of unified list (sections + assignments) and normalize
68
+ const [sections, assignments] = await Promise.all([
69
+ prisma.section.findMany({
70
+ where: { classId: input.classId },
71
+ select: { id: true, order: true },
72
+ }),
73
+ prisma.assignment.findMany({
74
+ where: { classId: input.classId },
75
+ select: { id: true, order: true },
76
+ }),
77
+ ]);
73
78
 
74
- const assignments = await prisma.assignment.findMany({
75
- where: {
76
- classId: input.classId,
77
- sectionId: null,
78
- },
79
- });
79
+ const unified = [
80
+ ...sections.map(s => ({ id: s.id, order: s.order, type: 'section' as const })),
81
+ ...assignments.map(a => ({ id: a.id, order: a.order, type: 'assignment' as const })),
82
+ ].sort((a, b) => (a.order ?? Number.MAX_SAFE_INTEGER) - (b.order ?? Number.MAX_SAFE_INTEGER));
80
83
 
81
- const stack = [...sections, ...assignments].sort((a, b) => (a.order || 0) - (b.order || 0)).map((item, index) => ({
82
- id: item.id,
83
- order: index + 1,
84
- })).map((item) => ({
85
- where: { id: item.id },
86
- data: { order: item.order },
87
- }));
88
-
89
- // Update sections and assignments with their new order
90
- await Promise.all([
91
- ...stack.filter(item => sections.some(s => s.id === item.where.id))
92
- .map(({ where, data }) =>
93
- prisma.section.update({ where, data })
94
- ),
95
- ...stack.filter(item => assignments.some(a => a.id === item.where.id))
96
- .map(({ where, data }) =>
97
- prisma.assignment.update({ where, data })
98
- )
99
- ]);
84
+ const withoutNew = unified.filter(item => !(item.id === section.id && item.type === 'section'));
85
+ const reindexed = [{ id: section.id, type: 'section' as const }, ...withoutNew.map(item => ({ id: item.id, type: item.type }))];
86
+
87
+ await Promise.all(
88
+ reindexed.map((item, index) => {
89
+ if (item.type === 'section') {
90
+ return prisma.section.update({ where: { id: item.id }, data: { order: index + 1 } });
91
+ } else {
92
+ return prisma.assignment.update({ where: { id: item.id }, data: { order: index + 1 } });
93
+ }
94
+ })
95
+ );
100
96
 
101
97
  return section;
102
98
  }),
103
99
 
100
+ reorder: protectedProcedure
101
+ .input(z.object({
102
+ classId: z.string(),
103
+ movedId: z.string(), // Section ID
104
+ // One of: place at start/end of unified list, or relative to targetId (can be section or assignment)
105
+ position: z.enum(['start', 'end', 'before', 'after']),
106
+ targetId: z.string().optional(), // Can be a section ID or assignment ID
107
+ }))
108
+ .mutation(async ({ ctx, input }) => {
109
+ if (!ctx.user) {
110
+ throw new TRPCError({
111
+ code: "UNAUTHORIZED",
112
+ message: "User must be authenticated",
113
+ });
114
+ }
115
+
116
+ const { classId, movedId, position, targetId } = input;
117
+
118
+ const moved = await prisma.section.findFirst({
119
+ where: { id: movedId, classId },
120
+ select: { id: true, classId: true },
121
+ });
122
+
123
+ if (!moved) {
124
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Section not found' });
125
+ }
126
+
127
+ if ((position === 'before' || position === 'after') && !targetId) {
128
+ throw new TRPCError({ code: 'BAD_REQUEST', message: 'targetId required for before/after' });
129
+ }
130
+
131
+ const result = await prisma.$transaction(async (tx) => {
132
+ const [sections, assignments] = await Promise.all([
133
+ tx.section.findMany({
134
+ where: { classId },
135
+ select: { id: true, order: true },
136
+ }),
137
+ tx.assignment.findMany({
138
+ where: { classId },
139
+ select: { id: true, order: true },
140
+ }),
141
+ ]);
142
+
143
+ const unified = [
144
+ ...sections.map(s => ({ id: s.id, order: s.order, type: 'section' as const })),
145
+ ...assignments.map(a => ({ id: a.id, order: a.order, type: 'assignment' as const })),
146
+ ].sort((a, b) => (a.order ?? Number.MAX_SAFE_INTEGER) - (b.order ?? Number.MAX_SAFE_INTEGER));
147
+
148
+ const movedIdx = unified.findIndex(item => item.id === movedId && item.type === 'section');
149
+ if (movedIdx === -1) {
150
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Section not found in unified list' });
151
+ }
152
+
153
+ const withoutMoved = unified.filter(item => !(item.id === movedId && item.type === 'section'));
154
+
155
+ let next: Array<{ id: string; type: 'section' | 'assignment' }> = [];
156
+
157
+ if (position === 'start') {
158
+ next = [{ id: movedId, type: 'section' }, ...withoutMoved.map(item => ({ id: item.id, type: item.type }))];
159
+ } else if (position === 'end') {
160
+ next = [...withoutMoved.map(item => ({ id: item.id, type: item.type })), { id: movedId, type: 'section' }];
161
+ } else {
162
+ const targetIdx = withoutMoved.findIndex(item => item.id === targetId);
163
+ if (targetIdx === -1) {
164
+ throw new TRPCError({ code: 'BAD_REQUEST', message: 'targetId not found in unified list' });
165
+ }
166
+ if (position === 'before') {
167
+ next = [
168
+ ...withoutMoved.slice(0, targetIdx).map(item => ({ id: item.id, type: item.type })),
169
+ { id: movedId, type: 'section' },
170
+ ...withoutMoved.slice(targetIdx).map(item => ({ id: item.id, type: item.type })),
171
+ ];
172
+ } else {
173
+ next = [
174
+ ...withoutMoved.slice(0, targetIdx + 1).map(item => ({ id: item.id, type: item.type })),
175
+ { id: movedId, type: 'section' },
176
+ ...withoutMoved.slice(targetIdx + 1).map(item => ({ id: item.id, type: item.type })),
177
+ ];
178
+ }
179
+ }
180
+
181
+ // Normalize to 1..n
182
+ await Promise.all(
183
+ next.map((item, index) => {
184
+ if (item.type === 'section') {
185
+ return tx.section.update({ where: { id: item.id }, data: { order: index + 1 } });
186
+ } else {
187
+ return tx.assignment.update({ where: { id: item.id }, data: { order: index + 1 } });
188
+ }
189
+ })
190
+ );
191
+
192
+ return tx.section.findUnique({ where: { id: movedId } });
193
+ });
194
+
195
+ return result;
196
+ }),
197
+
104
198
  update: protectedProcedure
105
199
  .input(updateSectionSchema)
106
200
  .mutation(async ({ ctx, input }) => {
@@ -176,11 +270,39 @@ export const sectionRouter = createTRPCRouter({
176
270
  });
177
271
  }
178
272
 
179
- await prisma.section.update({
180
- where: { id: input.id },
181
- data: {
182
- order: input.order,
183
- },
273
+ // Update order and normalize unified list
274
+ await prisma.$transaction(async (tx) => {
275
+ await tx.section.update({
276
+ where: { id: input.id },
277
+ data: { order: input.order },
278
+ });
279
+
280
+ // Normalize entire unified list
281
+ const [sections, assignments] = await Promise.all([
282
+ tx.section.findMany({
283
+ where: { classId: input.classId },
284
+ select: { id: true, order: true },
285
+ }),
286
+ tx.assignment.findMany({
287
+ where: { classId: input.classId },
288
+ select: { id: true, order: true },
289
+ }),
290
+ ]);
291
+
292
+ const unified = [
293
+ ...sections.map(s => ({ id: s.id, order: s.order, type: 'section' as const })),
294
+ ...assignments.map(a => ({ id: a.id, order: a.order, type: 'assignment' as const })),
295
+ ].sort((a, b) => (a.order ?? Number.MAX_SAFE_INTEGER) - (b.order ?? Number.MAX_SAFE_INTEGER));
296
+
297
+ await Promise.all(
298
+ unified.map((item, index) => {
299
+ if (item.type === 'section') {
300
+ return tx.section.update({ where: { id: item.id }, data: { order: index + 1 } });
301
+ } else {
302
+ return tx.assignment.update({ where: { id: item.id }, data: { order: index + 1 } });
303
+ }
304
+ })
305
+ );
184
306
  });
185
307
 
186
308
  return { id: input.id };
@@ -5,6 +5,7 @@ import { logger } from "./utils/logger.js";
5
5
  export async function clearDatabase() {
6
6
  // Delete in order to respect foreign key constraints
7
7
  // Delete notifications first (they reference users)
8
+ logger.info('Clearing database');
8
9
  await prisma.notification.deleteMany();
9
10
 
10
11
  // Delete chat-related records
@@ -94,7 +95,7 @@ export const seedDatabase = async () => {
94
95
 
95
96
  // 3. Create Students (realistic names)
96
97
  const students = await Promise.all([
97
- createUser('alex.martinez@student.riverside.edu', 'student123', 'alex.martinez'),
98
+ createUser('alex.martinez@student.rverside.eidu', 'student123', 'alex.martinez'),
98
99
  createUser('sophia.williams@student.riverside.edu', 'student123', 'sophia.williams'),
99
100
  createUser('james.brown@student.riverside.edu', 'student123', 'james.brown'),
100
101
  createUser('olivia.taylor@student.riverside.edu', 'student123', 'olivia.taylor'),
@@ -26,7 +26,24 @@ const colors = {
26
26
  magenta: '\x1b[35m',
27
27
  cyan: '\x1b[36m',
28
28
  white: '\x1b[37m',
29
- gray: '\x1b[90m'
29
+ gray: '\x1b[90m',
30
+ // Background colors
31
+ bgRed: '\x1b[41m',
32
+ bgGreen: '\x1b[42m',
33
+ bgYellow: '\x1b[43m',
34
+ bgBlue: '\x1b[44m',
35
+ bgMagenta: '\x1b[45m',
36
+ bgCyan: '\x1b[46m',
37
+ bgWhite: '\x1b[47m',
38
+ bgGray: '\x1b[100m',
39
+ // Bright background colors
40
+ bgBrightRed: '\x1b[101m',
41
+ bgBrightGreen: '\x1b[102m',
42
+ bgBrightYellow: '\x1b[103m',
43
+ bgBrightBlue: '\x1b[104m',
44
+ bgBrightMagenta: '\x1b[105m',
45
+ bgBrightCyan: '\x1b[106m',
46
+ bgBrightWhite: '\x1b[107m'
30
47
  };
31
48
 
32
49
  class Logger {
@@ -34,6 +51,7 @@ class Logger {
34
51
  private isDevelopment: boolean;
35
52
  private mode: LogMode;
36
53
  private levelColors: Record<LogLevel, string>;
54
+ private levelBgColors: Record<LogLevel, string>;
37
55
  private levelEmojis: Record<LogLevel, string>;
38
56
 
39
57
  private constructor() {
@@ -51,6 +69,13 @@ class Logger {
51
69
  [LogLevel.DEBUG]: colors.magenta
52
70
  };
53
71
 
72
+ this.levelBgColors = {
73
+ [LogLevel.INFO]: colors.bgBlue,
74
+ [LogLevel.WARN]: colors.bgYellow,
75
+ [LogLevel.ERROR]: colors.bgRed,
76
+ [LogLevel.DEBUG]: colors.bgMagenta
77
+ };
78
+
54
79
  this.levelEmojis = {
55
80
  [LogLevel.INFO]: 'ℹ️',
56
81
  [LogLevel.WARN]: '⚠️',
@@ -95,10 +120,12 @@ class Logger {
95
120
  private formatMessage(logMessage: LogMessage): string {
96
121
  const { level, message, timestamp, context } = logMessage;
97
122
  const color = this.levelColors[level];
123
+ const bgColor = this.levelBgColors[level];
98
124
  const emoji = this.levelEmojis[level];
99
125
 
100
126
  const timestampStr = colors.gray + `[${timestamp}]` + colors.reset;
101
- const levelStr = color + `[${level.toUpperCase()}]` + colors.reset;
127
+ // Use background color for level badge like Vitest
128
+ const levelStr = colors.white + bgColor + ` ${level.toUpperCase()} ` + colors.reset;
102
129
  const emojiStr = emoji + ' ';
103
130
  const messageStr = colors.bright + message + colors.reset;
104
131
 
package/tests/setup.ts CHANGED
@@ -5,6 +5,7 @@ import { logger } from '../src/utils/logger';
5
5
  import { appRouter } from '../src/routers/_app';
6
6
  import { createTRPCContext } from '../src/trpc';
7
7
  import { Session } from '@prisma/client';
8
+ import { clearDatabase } from '../src/seedDatabase';
8
9
 
9
10
  const getCaller = async (token: string) => {
10
11
  const ctx = await createTRPCContext({
@@ -19,15 +20,8 @@ const getCaller = async (token: string) => {
19
20
 
20
21
  // Before the entire test suite runs
21
22
  beforeAll(async () => {
22
- // // Run migrations so the test DB has the latest schema
23
- // try {
24
- // logger.info('Setting up test database');
25
- // execSync('rm -f prisma/test.db');
26
- // execSync('npx prisma db push --force-reset --schema=prisma/schema.prisma');
27
-
28
- // } catch (error) {
29
- // logger.error('Error initializing test database');
30
- // }
23
+
24
+ await clearDatabase();
31
25
 
32
26
  logger.info('Getting caller');
33
27