@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
|
@@ -31,6 +31,22 @@ export declare const sectionRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
31
31
|
};
|
|
32
32
|
meta: object;
|
|
33
33
|
}>;
|
|
34
|
+
reorder: import("@trpc/server").TRPCMutationProcedure<{
|
|
35
|
+
input: {
|
|
36
|
+
classId: string;
|
|
37
|
+
movedId: string;
|
|
38
|
+
position: "start" | "end" | "before" | "after";
|
|
39
|
+
targetId?: string | undefined;
|
|
40
|
+
};
|
|
41
|
+
output: {
|
|
42
|
+
id: string;
|
|
43
|
+
name: string;
|
|
44
|
+
color: string | null;
|
|
45
|
+
classId: string;
|
|
46
|
+
order: number | null;
|
|
47
|
+
} | null;
|
|
48
|
+
meta: object;
|
|
49
|
+
}>;
|
|
34
50
|
update: import("@trpc/server").TRPCMutationProcedure<{
|
|
35
51
|
input: {
|
|
36
52
|
id: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"section.d.ts","sourceRoot":"","sources":["../../src/routers/section.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAuBxB,eAAO,MAAM,aAAa
|
|
1
|
+
{"version":3,"file":"section.d.ts","sourceRoot":"","sources":["../../src/routers/section.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAuBxB,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkUxB,CAAC"}
|
package/dist/routers/section.js
CHANGED
|
@@ -56,34 +56,119 @@ export const sectionRouter = createTRPCRouter({
|
|
|
56
56
|
}),
|
|
57
57
|
},
|
|
58
58
|
});
|
|
59
|
-
//
|
|
60
|
-
const sections = await
|
|
61
|
-
|
|
62
|
-
classId: input.classId,
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
},
|
|
70
|
-
});
|
|
71
|
-
const stack = [...sections, ...assignments].sort((a, b) => (a.order || 0) - (b.order || 0)).map((item, index) => ({
|
|
72
|
-
id: item.id,
|
|
73
|
-
order: index + 1,
|
|
74
|
-
})).map((item) => ({
|
|
75
|
-
where: { id: item.id },
|
|
76
|
-
data: { order: item.order },
|
|
77
|
-
}));
|
|
78
|
-
// Update sections and assignments with their new order
|
|
79
|
-
await Promise.all([
|
|
80
|
-
...stack.filter(item => sections.some(s => s.id === item.where.id))
|
|
81
|
-
.map(({ where, data }) => prisma.section.update({ where, data })),
|
|
82
|
-
...stack.filter(item => assignments.some(a => a.id === item.where.id))
|
|
83
|
-
.map(({ where, data }) => prisma.assignment.update({ where, data }))
|
|
59
|
+
// Insert new section at top of unified list (sections + assignments) and normalize
|
|
60
|
+
const [sections, assignments] = await Promise.all([
|
|
61
|
+
prisma.section.findMany({
|
|
62
|
+
where: { classId: input.classId },
|
|
63
|
+
select: { id: true, order: true },
|
|
64
|
+
}),
|
|
65
|
+
prisma.assignment.findMany({
|
|
66
|
+
where: { classId: input.classId },
|
|
67
|
+
select: { id: true, order: true },
|
|
68
|
+
}),
|
|
84
69
|
]);
|
|
70
|
+
const unified = [
|
|
71
|
+
...sections.map(s => ({ id: s.id, order: s.order, type: 'section' })),
|
|
72
|
+
...assignments.map(a => ({ id: a.id, order: a.order, type: 'assignment' })),
|
|
73
|
+
].sort((a, b) => (a.order ?? Number.MAX_SAFE_INTEGER) - (b.order ?? Number.MAX_SAFE_INTEGER));
|
|
74
|
+
const withoutNew = unified.filter(item => !(item.id === section.id && item.type === 'section'));
|
|
75
|
+
const reindexed = [{ id: section.id, type: 'section' }, ...withoutNew.map(item => ({ id: item.id, type: item.type }))];
|
|
76
|
+
await Promise.all(reindexed.map((item, index) => {
|
|
77
|
+
if (item.type === 'section') {
|
|
78
|
+
return prisma.section.update({ where: { id: item.id }, data: { order: index + 1 } });
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
return prisma.assignment.update({ where: { id: item.id }, data: { order: index + 1 } });
|
|
82
|
+
}
|
|
83
|
+
}));
|
|
85
84
|
return section;
|
|
86
85
|
}),
|
|
86
|
+
reorder: protectedProcedure
|
|
87
|
+
.input(z.object({
|
|
88
|
+
classId: z.string(),
|
|
89
|
+
movedId: z.string(), // Section ID
|
|
90
|
+
// One of: place at start/end of unified list, or relative to targetId (can be section or assignment)
|
|
91
|
+
position: z.enum(['start', 'end', 'before', 'after']),
|
|
92
|
+
targetId: z.string().optional(), // Can be a section ID or assignment ID
|
|
93
|
+
}))
|
|
94
|
+
.mutation(async ({ ctx, input }) => {
|
|
95
|
+
if (!ctx.user) {
|
|
96
|
+
throw new TRPCError({
|
|
97
|
+
code: "UNAUTHORIZED",
|
|
98
|
+
message: "User must be authenticated",
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
const { classId, movedId, position, targetId } = input;
|
|
102
|
+
const moved = await prisma.section.findFirst({
|
|
103
|
+
where: { id: movedId, classId },
|
|
104
|
+
select: { id: true, classId: true },
|
|
105
|
+
});
|
|
106
|
+
if (!moved) {
|
|
107
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'Section not found' });
|
|
108
|
+
}
|
|
109
|
+
if ((position === 'before' || position === 'after') && !targetId) {
|
|
110
|
+
throw new TRPCError({ code: 'BAD_REQUEST', message: 'targetId required for before/after' });
|
|
111
|
+
}
|
|
112
|
+
const result = await prisma.$transaction(async (tx) => {
|
|
113
|
+
const [sections, assignments] = await Promise.all([
|
|
114
|
+
tx.section.findMany({
|
|
115
|
+
where: { classId },
|
|
116
|
+
select: { id: true, order: true },
|
|
117
|
+
}),
|
|
118
|
+
tx.assignment.findMany({
|
|
119
|
+
where: { classId },
|
|
120
|
+
select: { id: true, order: true },
|
|
121
|
+
}),
|
|
122
|
+
]);
|
|
123
|
+
const unified = [
|
|
124
|
+
...sections.map(s => ({ id: s.id, order: s.order, type: 'section' })),
|
|
125
|
+
...assignments.map(a => ({ id: a.id, order: a.order, type: 'assignment' })),
|
|
126
|
+
].sort((a, b) => (a.order ?? Number.MAX_SAFE_INTEGER) - (b.order ?? Number.MAX_SAFE_INTEGER));
|
|
127
|
+
const movedIdx = unified.findIndex(item => item.id === movedId && item.type === 'section');
|
|
128
|
+
if (movedIdx === -1) {
|
|
129
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'Section not found in unified list' });
|
|
130
|
+
}
|
|
131
|
+
const withoutMoved = unified.filter(item => !(item.id === movedId && item.type === 'section'));
|
|
132
|
+
let next = [];
|
|
133
|
+
if (position === 'start') {
|
|
134
|
+
next = [{ id: movedId, type: 'section' }, ...withoutMoved.map(item => ({ id: item.id, type: item.type }))];
|
|
135
|
+
}
|
|
136
|
+
else if (position === 'end') {
|
|
137
|
+
next = [...withoutMoved.map(item => ({ id: item.id, type: item.type })), { id: movedId, type: 'section' }];
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
const targetIdx = withoutMoved.findIndex(item => item.id === targetId);
|
|
141
|
+
if (targetIdx === -1) {
|
|
142
|
+
throw new TRPCError({ code: 'BAD_REQUEST', message: 'targetId not found in unified list' });
|
|
143
|
+
}
|
|
144
|
+
if (position === 'before') {
|
|
145
|
+
next = [
|
|
146
|
+
...withoutMoved.slice(0, targetIdx).map(item => ({ id: item.id, type: item.type })),
|
|
147
|
+
{ id: movedId, type: 'section' },
|
|
148
|
+
...withoutMoved.slice(targetIdx).map(item => ({ id: item.id, type: item.type })),
|
|
149
|
+
];
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
next = [
|
|
153
|
+
...withoutMoved.slice(0, targetIdx + 1).map(item => ({ id: item.id, type: item.type })),
|
|
154
|
+
{ id: movedId, type: 'section' },
|
|
155
|
+
...withoutMoved.slice(targetIdx + 1).map(item => ({ id: item.id, type: item.type })),
|
|
156
|
+
];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Normalize to 1..n
|
|
160
|
+
await Promise.all(next.map((item, index) => {
|
|
161
|
+
if (item.type === 'section') {
|
|
162
|
+
return tx.section.update({ where: { id: item.id }, data: { order: index + 1 } });
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
return tx.assignment.update({ where: { id: item.id }, data: { order: index + 1 } });
|
|
166
|
+
}
|
|
167
|
+
}));
|
|
168
|
+
return tx.section.findUnique({ where: { id: movedId } });
|
|
169
|
+
});
|
|
170
|
+
return result;
|
|
171
|
+
}),
|
|
87
172
|
update: protectedProcedure
|
|
88
173
|
.input(updateSectionSchema)
|
|
89
174
|
.mutation(async ({ ctx, input }) => {
|
|
@@ -151,11 +236,35 @@ export const sectionRouter = createTRPCRouter({
|
|
|
151
236
|
message: "Class not found or you are not a teacher",
|
|
152
237
|
});
|
|
153
238
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
239
|
+
// Update order and normalize unified list
|
|
240
|
+
await prisma.$transaction(async (tx) => {
|
|
241
|
+
await tx.section.update({
|
|
242
|
+
where: { id: input.id },
|
|
243
|
+
data: { order: input.order },
|
|
244
|
+
});
|
|
245
|
+
// Normalize entire unified list
|
|
246
|
+
const [sections, assignments] = await Promise.all([
|
|
247
|
+
tx.section.findMany({
|
|
248
|
+
where: { classId: input.classId },
|
|
249
|
+
select: { id: true, order: true },
|
|
250
|
+
}),
|
|
251
|
+
tx.assignment.findMany({
|
|
252
|
+
where: { classId: input.classId },
|
|
253
|
+
select: { id: true, order: true },
|
|
254
|
+
}),
|
|
255
|
+
]);
|
|
256
|
+
const unified = [
|
|
257
|
+
...sections.map(s => ({ id: s.id, order: s.order, type: 'section' })),
|
|
258
|
+
...assignments.map(a => ({ id: a.id, order: a.order, type: 'assignment' })),
|
|
259
|
+
].sort((a, b) => (a.order ?? Number.MAX_SAFE_INTEGER) - (b.order ?? Number.MAX_SAFE_INTEGER));
|
|
260
|
+
await Promise.all(unified.map((item, index) => {
|
|
261
|
+
if (item.type === 'section') {
|
|
262
|
+
return tx.section.update({ where: { id: item.id }, data: { order: index + 1 } });
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
return tx.assignment.update({ where: { id: item.id }, data: { order: index + 1 } });
|
|
266
|
+
}
|
|
267
|
+
}));
|
|
159
268
|
});
|
|
160
269
|
return { id: input.id };
|
|
161
270
|
}),
|
package/dist/seedDatabase.d.ts
CHANGED
|
@@ -14,9 +14,9 @@ export declare function addNotification(userId: string, title: string, content:
|
|
|
14
14
|
title: string;
|
|
15
15
|
content: string;
|
|
16
16
|
createdAt: Date;
|
|
17
|
-
read: boolean;
|
|
18
|
-
receiverId: string;
|
|
19
17
|
senderId: string | null;
|
|
18
|
+
receiverId: string;
|
|
19
|
+
read: boolean;
|
|
20
20
|
}>;
|
|
21
21
|
export declare const seedDatabase: () => Promise<void>;
|
|
22
22
|
//# sourceMappingURL=seedDatabase.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"seedDatabase.d.ts","sourceRoot":"","sources":["../src/seedDatabase.ts"],"names":[],"mappings":"AAIA,wBAAsB,aAAa,
|
|
1
|
+
{"version":3,"file":"seedDatabase.d.ts","sourceRoot":"","sources":["../src/seedDatabase.ts"],"names":[],"mappings":"AAIA,wBAAsB,aAAa,kBAuClC;AAED,wBAAsB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;;;;;;;;;GAOjF;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;;;;;;;;GAQnF;AAED,eAAO,MAAM,YAAY,qBAq+CxB,CAAC"}
|
package/dist/seedDatabase.js
CHANGED
|
@@ -4,6 +4,7 @@ import { logger } from "./utils/logger.js";
|
|
|
4
4
|
export async function clearDatabase() {
|
|
5
5
|
// Delete in order to respect foreign key constraints
|
|
6
6
|
// Delete notifications first (they reference users)
|
|
7
|
+
logger.info('Clearing database');
|
|
7
8
|
await prisma.notification.deleteMany();
|
|
8
9
|
// Delete chat-related records
|
|
9
10
|
await prisma.mention.deleteMany();
|
|
@@ -77,7 +78,7 @@ export const seedDatabase = async () => {
|
|
|
77
78
|
]);
|
|
78
79
|
// 3. Create Students (realistic names)
|
|
79
80
|
const students = await Promise.all([
|
|
80
|
-
createUser('alex.martinez@student.
|
|
81
|
+
createUser('alex.martinez@student.rverside.eidu', 'student123', 'alex.martinez'),
|
|
81
82
|
createUser('sophia.williams@student.riverside.edu', 'student123', 'sophia.williams'),
|
|
82
83
|
createUser('james.brown@student.riverside.edu', 'student123', 'james.brown'),
|
|
83
84
|
createUser('olivia.taylor@student.riverside.edu', 'student123', 'olivia.taylor'),
|
package/dist/utils/logger.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"AAAA,oBAAY,QAAQ;IAClB,IAAI,SAAS;IACb,IAAI,SAAS;IACb,KAAK,UAAU;IACf,KAAK,UAAU;CAChB;AAED,KAAK,OAAO,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"AAAA,oBAAY,QAAQ;IAClB,IAAI,SAAS;IACb,IAAI,SAAS;IACb,KAAK,UAAU;IACf,KAAK,UAAU;CAChB;AAED,KAAK,OAAO,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;AAyC3D,cAAM,MAAM;IACV,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAS;IAChC,OAAO,CAAC,aAAa,CAAU;IAC/B,OAAO,CAAC,IAAI,CAAU;IACtB,OAAO,CAAC,WAAW,CAA2B;IAC9C,OAAO,CAAC,aAAa,CAA2B;IAChD,OAAO,CAAC,WAAW,CAA2B;IAE9C,OAAO;WA8BO,WAAW,IAAI,MAAM;IAO5B,OAAO,CAAC,IAAI,EAAE,OAAO;IAI5B,OAAO,CAAC,SAAS;IAsBjB,OAAO,CAAC,aAAa;IAmBrB,OAAO,CAAC,GAAG;IAqCJ,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;IAInD,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;IAInD,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;IAIpD,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;CAG5D;AAED,eAAO,MAAM,MAAM,QAAuB,CAAC"}
|
package/dist/utils/logger.js
CHANGED
|
@@ -17,7 +17,24 @@ const colors = {
|
|
|
17
17
|
magenta: '\x1b[35m',
|
|
18
18
|
cyan: '\x1b[36m',
|
|
19
19
|
white: '\x1b[37m',
|
|
20
|
-
gray: '\x1b[90m'
|
|
20
|
+
gray: '\x1b[90m',
|
|
21
|
+
// Background colors
|
|
22
|
+
bgRed: '\x1b[41m',
|
|
23
|
+
bgGreen: '\x1b[42m',
|
|
24
|
+
bgYellow: '\x1b[43m',
|
|
25
|
+
bgBlue: '\x1b[44m',
|
|
26
|
+
bgMagenta: '\x1b[45m',
|
|
27
|
+
bgCyan: '\x1b[46m',
|
|
28
|
+
bgWhite: '\x1b[47m',
|
|
29
|
+
bgGray: '\x1b[100m',
|
|
30
|
+
// Bright background colors
|
|
31
|
+
bgBrightRed: '\x1b[101m',
|
|
32
|
+
bgBrightGreen: '\x1b[102m',
|
|
33
|
+
bgBrightYellow: '\x1b[103m',
|
|
34
|
+
bgBrightBlue: '\x1b[104m',
|
|
35
|
+
bgBrightMagenta: '\x1b[105m',
|
|
36
|
+
bgBrightCyan: '\x1b[106m',
|
|
37
|
+
bgBrightWhite: '\x1b[107m'
|
|
21
38
|
};
|
|
22
39
|
class Logger {
|
|
23
40
|
constructor() {
|
|
@@ -30,6 +47,12 @@ class Logger {
|
|
|
30
47
|
[LogLevel.ERROR]: colors.red,
|
|
31
48
|
[LogLevel.DEBUG]: colors.magenta
|
|
32
49
|
};
|
|
50
|
+
this.levelBgColors = {
|
|
51
|
+
[LogLevel.INFO]: colors.bgBlue,
|
|
52
|
+
[LogLevel.WARN]: colors.bgYellow,
|
|
53
|
+
[LogLevel.ERROR]: colors.bgRed,
|
|
54
|
+
[LogLevel.DEBUG]: colors.bgMagenta
|
|
55
|
+
};
|
|
33
56
|
this.levelEmojis = {
|
|
34
57
|
[LogLevel.INFO]: 'ℹ️',
|
|
35
58
|
[LogLevel.WARN]: '⚠️',
|
|
@@ -70,9 +93,11 @@ class Logger {
|
|
|
70
93
|
formatMessage(logMessage) {
|
|
71
94
|
const { level, message, timestamp, context } = logMessage;
|
|
72
95
|
const color = this.levelColors[level];
|
|
96
|
+
const bgColor = this.levelBgColors[level];
|
|
73
97
|
const emoji = this.levelEmojis[level];
|
|
74
98
|
const timestampStr = colors.gray + `[${timestamp}]` + colors.reset;
|
|
75
|
-
|
|
99
|
+
// Use background color for level badge like Vitest
|
|
100
|
+
const levelStr = colors.white + bgColor + ` ${level.toUpperCase()} ` + colors.reset;
|
|
76
101
|
const emojiStr = emoji + ' ';
|
|
77
102
|
const messageStr = colors.bright + message + colors.reset;
|
|
78
103
|
const contextStr = context
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@studious-lms/server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.26",
|
|
4
4
|
"description": "Backend server for Studious application",
|
|
5
5
|
"main": "dist/exportType.js",
|
|
6
6
|
"types": "dist/exportType.d.ts",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"generate": "npx prisma generate",
|
|
19
19
|
"prepublishOnly": "npm run generate && npm run build",
|
|
20
20
|
"test": "vitest",
|
|
21
|
-
"seed": "
|
|
21
|
+
"seed": "tsx src/seedDatabase.ts"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@google-cloud/storage": "^7.16.0",
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
-- AlterTable
|
|
2
|
+
ALTER TABLE "public"."Announcement" ADD COLUMN "modifiedAt" TIMESTAMP(3);
|
|
3
|
+
|
|
4
|
+
-- AlterTable
|
|
5
|
+
ALTER TABLE "public"."File" ADD COLUMN "announcementId" TEXT;
|
|
6
|
+
|
|
7
|
+
-- CreateTable
|
|
8
|
+
CREATE TABLE "public"."AnnouncementComment" (
|
|
9
|
+
"id" TEXT NOT NULL,
|
|
10
|
+
"content" TEXT NOT NULL,
|
|
11
|
+
"authorId" TEXT NOT NULL,
|
|
12
|
+
"announcementId" TEXT NOT NULL,
|
|
13
|
+
"parentCommentId" TEXT,
|
|
14
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
15
|
+
"modifiedAt" TIMESTAMP(3),
|
|
16
|
+
|
|
17
|
+
CONSTRAINT "AnnouncementComment_pkey" PRIMARY KEY ("id")
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
-- AddForeignKey
|
|
21
|
+
ALTER TABLE "public"."File" ADD CONSTRAINT "File_announcementId_fkey" FOREIGN KEY ("announcementId") REFERENCES "public"."Announcement"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
22
|
+
|
|
23
|
+
-- AddForeignKey
|
|
24
|
+
ALTER TABLE "public"."AnnouncementComment" ADD CONSTRAINT "AnnouncementComment_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
25
|
+
|
|
26
|
+
-- AddForeignKey
|
|
27
|
+
ALTER TABLE "public"."AnnouncementComment" ADD CONSTRAINT "AnnouncementComment_announcementId_fkey" FOREIGN KEY ("announcementId") REFERENCES "public"."Announcement"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
28
|
+
|
|
29
|
+
-- AddForeignKey
|
|
30
|
+
ALTER TABLE "public"."AnnouncementComment" ADD CONSTRAINT "AnnouncementComment_parentCommentId_fkey" FOREIGN KEY ("parentCommentId") REFERENCES "public"."AnnouncementComment"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
-- CreateEnum
|
|
2
|
+
CREATE TYPE "public"."ReactionType" AS ENUM ('THUMBSUP', 'CELEBRATE', 'CARE', 'HEART', 'IDEA', 'HAPPY');
|
|
3
|
+
|
|
4
|
+
-- CreateTable
|
|
5
|
+
CREATE TABLE "public"."Reaction" (
|
|
6
|
+
"id" TEXT NOT NULL,
|
|
7
|
+
"type" "public"."ReactionType" NOT NULL,
|
|
8
|
+
"userId" TEXT NOT NULL,
|
|
9
|
+
"announcementId" TEXT,
|
|
10
|
+
"commentId" TEXT,
|
|
11
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
12
|
+
|
|
13
|
+
CONSTRAINT "Reaction_pkey" PRIMARY KEY ("id")
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
-- CreateIndex
|
|
17
|
+
CREATE INDEX "Reaction_announcementId_idx" ON "public"."Reaction"("announcementId");
|
|
18
|
+
|
|
19
|
+
-- CreateIndex
|
|
20
|
+
CREATE INDEX "Reaction_commentId_idx" ON "public"."Reaction"("commentId");
|
|
21
|
+
|
|
22
|
+
-- CreateIndex
|
|
23
|
+
CREATE UNIQUE INDEX "Reaction_userId_announcementId_key" ON "public"."Reaction"("userId", "announcementId");
|
|
24
|
+
|
|
25
|
+
-- CreateIndex
|
|
26
|
+
CREATE UNIQUE INDEX "Reaction_userId_commentId_key" ON "public"."Reaction"("userId", "commentId");
|
|
27
|
+
|
|
28
|
+
-- AddForeignKey
|
|
29
|
+
ALTER TABLE "public"."Reaction" ADD CONSTRAINT "Reaction_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
30
|
+
|
|
31
|
+
-- AddForeignKey
|
|
32
|
+
ALTER TABLE "public"."Reaction" ADD CONSTRAINT "Reaction_announcementId_fkey" FOREIGN KEY ("announcementId") REFERENCES "public"."Announcement"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
33
|
+
|
|
34
|
+
-- AddForeignKey
|
|
35
|
+
ALTER TABLE "public"."Reaction" ADD CONSTRAINT "Reaction_commentId_fkey" FOREIGN KEY ("commentId") REFERENCES "public"."AnnouncementComment"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
package/prisma/schema.prisma
CHANGED
|
@@ -42,6 +42,15 @@ enum UploadStatus {
|
|
|
42
42
|
CANCELLED
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
enum ReactionType {
|
|
46
|
+
THUMBSUP
|
|
47
|
+
CELEBRATE
|
|
48
|
+
CARE
|
|
49
|
+
HEART
|
|
50
|
+
IDEA
|
|
51
|
+
HAPPY
|
|
52
|
+
}
|
|
53
|
+
|
|
45
54
|
model School {
|
|
46
55
|
id String @id @default(uuid())
|
|
47
56
|
name String
|
|
@@ -89,6 +98,8 @@ model User {
|
|
|
89
98
|
sentMessages Message[] @relation("SentMessages")
|
|
90
99
|
mentions Mention[] @relation("UserMentions")
|
|
91
100
|
createdLabChats LabChat[] @relation("CreatedLabChats")
|
|
101
|
+
announcementComments AnnouncementComment[]
|
|
102
|
+
reactions Reaction[]
|
|
92
103
|
|
|
93
104
|
}
|
|
94
105
|
|
|
@@ -207,6 +218,9 @@ model File {
|
|
|
207
218
|
messageId String?
|
|
208
219
|
message Message? @relation("MessageAttachments", fields: [messageId], references: [id], onDelete: Cascade)
|
|
209
220
|
|
|
221
|
+
announcement Announcement? @relation("AnnouncementAttachments", fields: [announcementId], references: [id], onDelete: Cascade)
|
|
222
|
+
announcementId String?
|
|
223
|
+
|
|
210
224
|
schools School[]
|
|
211
225
|
|
|
212
226
|
schoolDevelopementProgram SchoolDevelopementProgram? @relation("SchoolDevelopementProgramSupportingDocumentation", fields: [schoolDevelopementProgramId], references: [id], onDelete: Cascade)
|
|
@@ -251,8 +265,44 @@ model Announcement {
|
|
|
251
265
|
teacher User @relation(fields: [teacherId], references: [id])
|
|
252
266
|
teacherId String
|
|
253
267
|
createdAt DateTime @default(now())
|
|
268
|
+
modifiedAt DateTime? @updatedAt
|
|
254
269
|
class Class @relation(fields: [classId], references: [id], onDelete: Cascade)
|
|
255
270
|
classId String
|
|
271
|
+
attachments File[] @relation("AnnouncementAttachments")
|
|
272
|
+
comments AnnouncementComment[]
|
|
273
|
+
reactions Reaction[]
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
model AnnouncementComment {
|
|
277
|
+
id String @id @default(uuid())
|
|
278
|
+
content String
|
|
279
|
+
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
|
|
280
|
+
authorId String
|
|
281
|
+
announcement Announcement @relation(fields: [announcementId], references: [id], onDelete: Cascade)
|
|
282
|
+
announcementId String
|
|
283
|
+
parentComment AnnouncementComment? @relation("CommentReplies", fields: [parentCommentId], references: [id], onDelete: Cascade)
|
|
284
|
+
parentCommentId String?
|
|
285
|
+
replies AnnouncementComment[] @relation("CommentReplies")
|
|
286
|
+
reactions Reaction[]
|
|
287
|
+
createdAt DateTime @default(now())
|
|
288
|
+
modifiedAt DateTime? @updatedAt
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
model Reaction {
|
|
292
|
+
id String @id @default(uuid())
|
|
293
|
+
type ReactionType
|
|
294
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
295
|
+
userId String
|
|
296
|
+
announcement Announcement? @relation(fields: [announcementId], references: [id], onDelete: Cascade)
|
|
297
|
+
announcementId String?
|
|
298
|
+
comment AnnouncementComment? @relation(fields: [commentId], references: [id], onDelete: Cascade)
|
|
299
|
+
commentId String?
|
|
300
|
+
createdAt DateTime @default(now())
|
|
301
|
+
|
|
302
|
+
@@unique([userId, announcementId])
|
|
303
|
+
@@unique([userId, commentId])
|
|
304
|
+
@@index([announcementId])
|
|
305
|
+
@@index([commentId])
|
|
256
306
|
}
|
|
257
307
|
|
|
258
308
|
model Submission {
|
package/src/lib/fileUpload.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TRPCError } from "@trpc/server";
|
|
2
2
|
import { v4 as uuidv4 } from "uuid";
|
|
3
|
-
import { getSignedUrl } from "./googleCloudStorage.js";
|
|
3
|
+
import { getSignedUrl, objectExists } from "./googleCloudStorage.js";
|
|
4
4
|
import { generateMediaThumbnail } from "./thumbnailGenerator.js";
|
|
5
5
|
import { prisma } from "./prisma.js";
|
|
6
6
|
import { logger } from "../utils/logger.js";
|
|
@@ -102,7 +102,8 @@ export async function createDirectUploadFile(
|
|
|
102
102
|
userId: string,
|
|
103
103
|
directory?: string,
|
|
104
104
|
assignmentId?: string,
|
|
105
|
-
submissionId?: string
|
|
105
|
+
submissionId?: string,
|
|
106
|
+
announcementId?: string
|
|
106
107
|
): Promise<DirectUploadFile> {
|
|
107
108
|
try {
|
|
108
109
|
// Validate file extension matches MIME type
|
|
@@ -167,6 +168,11 @@ export async function createDirectUploadFile(
|
|
|
167
168
|
submission: {
|
|
168
169
|
connect: { id: submissionId }
|
|
169
170
|
}
|
|
171
|
+
}),
|
|
172
|
+
...(announcementId && {
|
|
173
|
+
announcement: {
|
|
174
|
+
connect: { id: announcementId }
|
|
175
|
+
}
|
|
170
176
|
})
|
|
171
177
|
},
|
|
172
178
|
});
|
|
@@ -182,7 +188,11 @@ export async function createDirectUploadFile(
|
|
|
182
188
|
uploadSessionId
|
|
183
189
|
};
|
|
184
190
|
} catch (error) {
|
|
185
|
-
|
|
191
|
+
logger.error('Error creating direct upload file:', {error: error instanceof Error ? {
|
|
192
|
+
name: error.name,
|
|
193
|
+
message: error.message,
|
|
194
|
+
stack: error.stack,
|
|
195
|
+
} : error});
|
|
186
196
|
throw new TRPCError({
|
|
187
197
|
code: 'INTERNAL_SERVER_ERROR',
|
|
188
198
|
message: 'Failed to create direct upload file',
|
|
@@ -202,17 +212,53 @@ export async function confirmDirectUpload(
|
|
|
202
212
|
errorMessage?: string
|
|
203
213
|
): Promise<void> {
|
|
204
214
|
try {
|
|
215
|
+
// First fetch the file record to get the object path
|
|
216
|
+
const fileRecord = await prisma.file.findUnique({
|
|
217
|
+
where: { id: fileId },
|
|
218
|
+
select: { path: true }
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
if (!fileRecord) {
|
|
222
|
+
throw new TRPCError({
|
|
223
|
+
code: 'NOT_FOUND',
|
|
224
|
+
message: 'File record not found',
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
let actualUploadSuccess = uploadSuccess;
|
|
229
|
+
let actualErrorMessage = errorMessage;
|
|
230
|
+
|
|
231
|
+
// If uploadSuccess is true, verify the object actually exists in GCS
|
|
232
|
+
if (uploadSuccess) {
|
|
233
|
+
try {
|
|
234
|
+
const exists = await objectExists(process.env.GOOGLE_CLOUD_BUCKET_NAME!, fileRecord.path);
|
|
235
|
+
if (!exists) {
|
|
236
|
+
actualUploadSuccess = false;
|
|
237
|
+
actualErrorMessage = 'File upload reported as successful but object not found in Google Cloud Storage';
|
|
238
|
+
logger.error(`File upload verification failed for ${fileId}: object ${fileRecord.path} not found in GCS`);
|
|
239
|
+
}
|
|
240
|
+
} catch (error) {
|
|
241
|
+
logger.error(`Error verifying file existence in GCS for ${fileId}:`, {error: error instanceof Error ? {
|
|
242
|
+
name: error.name,
|
|
243
|
+
message: error.message,
|
|
244
|
+
stack: error.stack,
|
|
245
|
+
} : error});
|
|
246
|
+
actualUploadSuccess = false;
|
|
247
|
+
actualErrorMessage = 'Failed to verify file existence in Google Cloud Storage';
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
205
251
|
const updateData: any = {
|
|
206
|
-
uploadStatus:
|
|
207
|
-
uploadProgress:
|
|
252
|
+
uploadStatus: actualUploadSuccess ? 'COMPLETED' : 'FAILED',
|
|
253
|
+
uploadProgress: actualUploadSuccess ? 100 : 0,
|
|
208
254
|
};
|
|
209
255
|
|
|
210
|
-
if (!
|
|
211
|
-
updateData.uploadError =
|
|
256
|
+
if (!actualUploadSuccess && actualErrorMessage) {
|
|
257
|
+
updateData.uploadError = actualErrorMessage;
|
|
212
258
|
updateData.uploadRetryCount = { increment: 1 };
|
|
213
259
|
}
|
|
214
260
|
|
|
215
|
-
if (
|
|
261
|
+
if (actualUploadSuccess) {
|
|
216
262
|
updateData.uploadedAt = new Date();
|
|
217
263
|
}
|
|
218
264
|
|
|
@@ -221,7 +267,7 @@ export async function confirmDirectUpload(
|
|
|
221
267
|
data: updateData
|
|
222
268
|
});
|
|
223
269
|
} catch (error) {
|
|
224
|
-
|
|
270
|
+
logger.error('Error confirming direct upload:', {error});
|
|
225
271
|
throw new TRPCError({
|
|
226
272
|
code: 'INTERNAL_SERVER_ERROR',
|
|
227
273
|
message: 'Failed to confirm upload',
|
|
@@ -239,15 +285,29 @@ export async function updateUploadProgress(
|
|
|
239
285
|
progress: number
|
|
240
286
|
): Promise<void> {
|
|
241
287
|
try {
|
|
288
|
+
// await prisma.file.update({
|
|
289
|
+
// where: { id: fileId },
|
|
290
|
+
// data: {
|
|
291
|
+
// uploadStatus: 'UPLOADING',
|
|
292
|
+
// uploadProgress: Math.min(100, Math.max(0, progress))
|
|
293
|
+
// }
|
|
294
|
+
// });
|
|
295
|
+
const current = await prisma.file.findUnique({ where: { id: fileId }, select: { uploadStatus: true } });
|
|
296
|
+
if (!current || ['COMPLETED','FAILED','CANCELLED'].includes(current.uploadStatus as string)) return;
|
|
297
|
+
const clamped = Math.min(100, Math.max(0, progress));
|
|
242
298
|
await prisma.file.update({
|
|
243
299
|
where: { id: fileId },
|
|
244
300
|
data: {
|
|
245
301
|
uploadStatus: 'UPLOADING',
|
|
246
|
-
uploadProgress:
|
|
302
|
+
uploadProgress: clamped
|
|
247
303
|
}
|
|
248
304
|
});
|
|
249
305
|
} catch (error) {
|
|
250
|
-
|
|
306
|
+
logger.error('Error updating upload progress:', {error: error instanceof Error ? {
|
|
307
|
+
name: error.name,
|
|
308
|
+
message: error.message,
|
|
309
|
+
stack: error.stack,
|
|
310
|
+
} : error});
|
|
251
311
|
throw new TRPCError({
|
|
252
312
|
code: 'INTERNAL_SERVER_ERROR',
|
|
253
313
|
message: 'Failed to update upload progress',
|
|
@@ -269,15 +329,20 @@ export async function createDirectUploadFiles(
|
|
|
269
329
|
userId: string,
|
|
270
330
|
directory?: string,
|
|
271
331
|
assignmentId?: string,
|
|
272
|
-
submissionId?: string
|
|
332
|
+
submissionId?: string,
|
|
333
|
+
announcementId?: string
|
|
273
334
|
): Promise<DirectUploadFile[]> {
|
|
274
335
|
try {
|
|
275
336
|
const uploadPromises = files.map(file =>
|
|
276
|
-
createDirectUploadFile(file, userId, directory, assignmentId, submissionId)
|
|
337
|
+
createDirectUploadFile(file, userId, directory, assignmentId, submissionId, announcementId)
|
|
277
338
|
);
|
|
278
339
|
return await Promise.all(uploadPromises);
|
|
279
340
|
} catch (error) {
|
|
280
|
-
|
|
341
|
+
logger.error('Error creating direct upload files:', {error: error instanceof Error ? {
|
|
342
|
+
name: error.name,
|
|
343
|
+
message: error.message,
|
|
344
|
+
stack: error.stack,
|
|
345
|
+
} : error});
|
|
281
346
|
throw new TRPCError({
|
|
282
347
|
code: 'INTERNAL_SERVER_ERROR',
|
|
283
348
|
message: 'Failed to create direct upload files',
|