@studious-lms/server 1.1.24 → 1.2.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.
- package/dist/lib/fileUpload.d.ts +2 -2
- package/dist/lib/fileUpload.d.ts.map +1 -1
- package/dist/lib/fileUpload.js +76 -14
- package/dist/lib/googleCloudStorage.d.ts +7 -0
- package/dist/lib/googleCloudStorage.d.ts.map +1 -1
- package/dist/lib/googleCloudStorage.js +19 -0
- package/dist/lib/notificationHandler.d.ts +25 -0
- package/dist/lib/notificationHandler.d.ts.map +1 -0
- package/dist/lib/notificationHandler.js +28 -0
- package/dist/routers/_app.d.ts +818 -78
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/announcement.d.ts +290 -3
- package/dist/routers/announcement.d.ts.map +1 -1
- package/dist/routers/announcement.js +896 -10
- package/dist/routers/assignment.d.ts +70 -4
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/assignment.js +265 -131
- package/dist/routers/auth.js +1 -1
- package/dist/routers/file.d.ts +2 -0
- package/dist/routers/file.d.ts.map +1 -1
- package/dist/routers/file.js +9 -6
- package/dist/routers/labChat.d.ts.map +1 -1
- package/dist/routers/labChat.js +13 -5
- package/dist/routers/notifications.d.ts +8 -8
- package/dist/routers/section.d.ts +16 -0
- package/dist/routers/section.d.ts.map +1 -1
- package/dist/routers/section.js +139 -30
- package/dist/seedDatabase.d.ts +2 -2
- package/dist/seedDatabase.d.ts.map +1 -1
- package/dist/seedDatabase.js +2 -1
- package/dist/utils/logger.d.ts +1 -0
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +27 -2
- package/package.json +2 -2
- package/prisma/migrations/20251109122857_annuoncements_comments/migration.sql +30 -0
- package/prisma/migrations/20251109135555_reactions_announcements_comments/migration.sql +35 -0
- package/prisma/schema.prisma +50 -0
- package/src/lib/fileUpload.ts +79 -14
- package/src/lib/googleCloudStorage.ts +19 -0
- package/src/lib/notificationHandler.ts +36 -0
- package/src/routers/announcement.ts +1007 -10
- package/src/routers/assignment.ts +230 -82
- package/src/routers/auth.ts +1 -1
- package/src/routers/file.ts +10 -7
- package/src/routers/labChat.ts +15 -6
- package/src/routers/section.ts +158 -36
- package/src/seedDatabase.ts +2 -1
- package/src/utils/logger.ts +29 -2
- package/tests/setup.ts +3 -9
package/src/routers/section.ts
CHANGED
|
@@ -64,43 +64,137 @@ export const sectionRouter = createTRPCRouter({
|
|
|
64
64
|
},
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
-
//
|
|
68
|
-
const sections = await
|
|
69
|
-
|
|
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
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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 };
|
package/src/seedDatabase.ts
CHANGED
|
@@ -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.
|
|
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'),
|
package/src/utils/logger.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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
|
|