@studious-lms/server 1.0.6 → 1.0.8

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 (115) hide show
  1. package/API_SPECIFICATION.md +1461 -0
  2. package/dist/exportType.d.ts +3 -3
  3. package/dist/exportType.d.ts.map +1 -1
  4. package/dist/exportType.js +1 -2
  5. package/dist/index.js +25 -30
  6. package/dist/lib/fileUpload.d.ts.map +1 -1
  7. package/dist/lib/fileUpload.js +31 -29
  8. package/dist/lib/googleCloudStorage.js +9 -14
  9. package/dist/lib/prisma.js +4 -7
  10. package/dist/lib/thumbnailGenerator.js +12 -20
  11. package/dist/middleware/auth.d.ts.map +1 -1
  12. package/dist/middleware/auth.js +17 -22
  13. package/dist/middleware/logging.js +5 -9
  14. package/dist/routers/_app.d.ts +3619 -1937
  15. package/dist/routers/_app.d.ts.map +1 -1
  16. package/dist/routers/_app.js +28 -27
  17. package/dist/routers/agenda.d.ts +14 -9
  18. package/dist/routers/agenda.d.ts.map +1 -1
  19. package/dist/routers/agenda.js +14 -17
  20. package/dist/routers/announcement.d.ts +5 -4
  21. package/dist/routers/announcement.d.ts.map +1 -1
  22. package/dist/routers/announcement.js +28 -31
  23. package/dist/routers/assignment.d.ts +283 -197
  24. package/dist/routers/assignment.d.ts.map +1 -1
  25. package/dist/routers/assignment.js +256 -202
  26. package/dist/routers/attendance.d.ts +6 -5
  27. package/dist/routers/attendance.d.ts.map +1 -1
  28. package/dist/routers/attendance.js +31 -34
  29. package/dist/routers/auth.d.ts +2 -1
  30. package/dist/routers/auth.d.ts.map +1 -1
  31. package/dist/routers/auth.js +80 -75
  32. package/dist/routers/class.d.ts +285 -15
  33. package/dist/routers/class.d.ts.map +1 -1
  34. package/dist/routers/class.js +440 -164
  35. package/dist/routers/event.d.ts +48 -39
  36. package/dist/routers/event.d.ts.map +1 -1
  37. package/dist/routers/event.js +76 -79
  38. package/dist/routers/file.d.ts +72 -2
  39. package/dist/routers/file.d.ts.map +1 -1
  40. package/dist/routers/file.js +260 -32
  41. package/dist/routers/folder.d.ts +296 -0
  42. package/dist/routers/folder.d.ts.map +1 -0
  43. package/dist/routers/folder.js +693 -0
  44. package/dist/routers/notifications.d.ts +103 -0
  45. package/dist/routers/notifications.d.ts.map +1 -0
  46. package/dist/routers/notifications.js +91 -0
  47. package/dist/routers/school.d.ts +208 -0
  48. package/dist/routers/school.d.ts.map +1 -0
  49. package/dist/routers/school.js +481 -0
  50. package/dist/routers/section.d.ts +2 -1
  51. package/dist/routers/section.d.ts.map +1 -1
  52. package/dist/routers/section.js +30 -33
  53. package/dist/routers/user.d.ts +3 -2
  54. package/dist/routers/user.d.ts.map +1 -1
  55. package/dist/routers/user.js +21 -24
  56. package/dist/seedDatabase.d.ts +22 -0
  57. package/dist/seedDatabase.d.ts.map +1 -0
  58. package/dist/seedDatabase.js +75 -0
  59. package/dist/socket/handlers.js +26 -30
  60. package/dist/trpc.d.ts +5 -0
  61. package/dist/trpc.d.ts.map +1 -1
  62. package/dist/trpc.js +35 -26
  63. package/dist/types/trpc.d.ts +1 -1
  64. package/dist/types/trpc.d.ts.map +1 -1
  65. package/dist/types/trpc.js +1 -2
  66. package/dist/utils/email.js +2 -8
  67. package/dist/utils/generateInviteCode.js +1 -5
  68. package/dist/utils/logger.d.ts.map +1 -1
  69. package/dist/utils/logger.js +13 -9
  70. package/dist/utils/prismaErrorHandler.d.ts +9 -0
  71. package/dist/utils/prismaErrorHandler.d.ts.map +1 -0
  72. package/dist/utils/prismaErrorHandler.js +234 -0
  73. package/dist/utils/prismaWrapper.d.ts +14 -0
  74. package/dist/utils/prismaWrapper.d.ts.map +1 -0
  75. package/dist/utils/prismaWrapper.js +64 -0
  76. package/package.json +12 -4
  77. package/prisma/migrations/20250807062924_init/migration.sql +436 -0
  78. package/prisma/migrations/migration_lock.toml +3 -0
  79. package/prisma/schema.prisma +68 -1
  80. package/src/exportType.ts +3 -3
  81. package/src/index.ts +6 -6
  82. package/src/lib/fileUpload.ts +19 -10
  83. package/src/lib/thumbnailGenerator.ts +2 -2
  84. package/src/middleware/auth.ts +2 -4
  85. package/src/middleware/logging.ts +2 -2
  86. package/src/routers/_app.ts +17 -13
  87. package/src/routers/agenda.ts +2 -2
  88. package/src/routers/announcement.ts +2 -2
  89. package/src/routers/assignment.ts +86 -26
  90. package/src/routers/attendance.ts +2 -2
  91. package/src/routers/auth.ts +83 -57
  92. package/src/routers/class.ts +339 -39
  93. package/src/routers/event.ts +2 -2
  94. package/src/routers/file.ts +276 -21
  95. package/src/routers/folder.ts +755 -0
  96. package/src/routers/notifications.ts +93 -0
  97. package/src/routers/section.ts +2 -2
  98. package/src/routers/user.ts +3 -3
  99. package/src/seedDatabase.ts +88 -0
  100. package/src/socket/handlers.ts +5 -5
  101. package/src/trpc.ts +17 -4
  102. package/src/types/trpc.ts +1 -1
  103. package/src/utils/logger.ts +14 -4
  104. package/src/utils/prismaErrorHandler.ts +275 -0
  105. package/src/utils/prismaWrapper.ts +91 -0
  106. package/tests/auth.test.ts +25 -0
  107. package/tests/class.test.ts +281 -0
  108. package/tests/setup.ts +98 -0
  109. package/tests/startup.test.ts +5 -0
  110. package/tsconfig.json +2 -1
  111. package/vitest.config.ts +11 -0
  112. package/dist/logger.d.ts +0 -26
  113. package/dist/logger.d.ts.map +0 -1
  114. package/dist/logger.js +0 -135
  115. package/src/logger.ts +0 -163
@@ -0,0 +1,93 @@
1
+ import { createTRPCRouter, protectedProcedure } from "../trpc.js";
2
+ import { prisma } from "../lib/prisma.js";
3
+ import { z } from "zod";
4
+
5
+ export const notificationRouter = createTRPCRouter({
6
+ list: protectedProcedure.query(async ({ ctx }) => {
7
+ const notifications = await prisma.notification.findMany({
8
+ where: {
9
+ receiverId: ctx.user!.id,
10
+ },
11
+ orderBy: {
12
+ createdAt: "desc",
13
+ },
14
+ include: {
15
+ sender: {
16
+ select: {
17
+ username: true,
18
+ },
19
+ },
20
+ receiver: {
21
+ select: {
22
+ username: true,
23
+ },
24
+ },
25
+ },
26
+ });
27
+
28
+ return notifications;
29
+ }),
30
+ get: protectedProcedure.input(z.object({
31
+ id: z.string(),
32
+ })).query(async ({ ctx, input }) => {
33
+ const { id } = input;
34
+ const notification = await prisma.notification.findUnique({
35
+ where: {
36
+ id,
37
+ },
38
+ include: {
39
+ sender: {
40
+ select: {
41
+ username: true,
42
+ },
43
+ },
44
+ receiver: {
45
+ select: {
46
+ username: true,
47
+ },
48
+ },
49
+ },
50
+ });
51
+ return notification;
52
+ }),
53
+ sendTo: protectedProcedure.input(z.object({
54
+ receiverId: z.string(),
55
+ title: z.string(),
56
+ content: z.string(),
57
+ })).mutation(async ({ ctx, input }) => {
58
+ const { receiverId, title, content } = input;
59
+ const notification = await prisma.notification.create({
60
+ data: {
61
+ receiverId,
62
+ title,
63
+ content,
64
+ },
65
+ });
66
+ return notification;
67
+ }),
68
+ sendToMultiple: protectedProcedure.input(z.object({
69
+ receiverIds: z.array(z.string()),
70
+ title: z.string(),
71
+ content: z.string(),
72
+ })).mutation(async ({ ctx, input }) => {
73
+ const { receiverIds, title, content } = input;
74
+ const notifications = await prisma.notification.createMany({
75
+ data: receiverIds.map(receiverId => ({
76
+ receiverId,
77
+ title,
78
+ content,
79
+ })),
80
+ });
81
+ return notifications;
82
+ }),
83
+ markAsRead: protectedProcedure.input(z.object({
84
+ id: z.string(),
85
+ })).mutation(async ({ ctx, input }) => {
86
+ const { id } = input;
87
+ const notification = await prisma.notification.update({
88
+ where: { id },
89
+ data: { read: true },
90
+ });
91
+ return notification;
92
+ }),
93
+ })
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
- import { createTRPCRouter, protectedProcedure } from "../trpc";
2
+ import { createTRPCRouter, protectedProcedure } from "../trpc.js";
3
3
  import { TRPCError } from "@trpc/server";
4
- import { prisma } from "../lib/prisma";
4
+ import { prisma } from "../lib/prisma.js";
5
5
 
6
6
  const createSectionSchema = z.object({
7
7
  classId: z.string(),
@@ -1,8 +1,8 @@
1
1
  import { z } from "zod";
2
- import { createTRPCRouter, protectedProcedure } from "../trpc";
2
+ import { createTRPCRouter, protectedProcedure } from "../trpc.js";
3
3
  import { TRPCError } from "@trpc/server";
4
- import { prisma } from "../lib/prisma";
5
- import { uploadFiles, type UploadedFile } from "../lib/fileUpload";
4
+ import { prisma } from "../lib/prisma.js";
5
+ import { uploadFiles, type UploadedFile } from "../lib/fileUpload.js";
6
6
 
7
7
  const fileSchema = z.object({
8
8
  name: z.string(),
@@ -0,0 +1,88 @@
1
+ import { prisma } from "./lib/prisma.js";
2
+ import { hash } from "bcryptjs";
3
+ import { logger } from "./utils/logger.js";
4
+
5
+ export async function clearDatabase() {
6
+ // Delete in order to respect foreign key constraints
7
+ // Delete notifications first (they reference users)
8
+ await prisma.notification.deleteMany();
9
+
10
+ // Delete other records that reference users
11
+ await prisma.submission.deleteMany();
12
+ await prisma.assignment.deleteMany();
13
+ await prisma.announcement.deleteMany();
14
+ await prisma.event.deleteMany();
15
+ await prisma.attendance.deleteMany();
16
+ await prisma.file.deleteMany();
17
+
18
+ // Delete class-related records
19
+ await prisma.section.deleteMany();
20
+ await prisma.markScheme.deleteMany();
21
+ await prisma.gradingBoundary.deleteMany();
22
+ await prisma.folder.deleteMany();
23
+ await prisma.class.deleteMany();
24
+
25
+ // Delete user-related records
26
+ await prisma.session.deleteMany();
27
+ await prisma.userProfile.deleteMany();
28
+
29
+ // Finally delete users and schools
30
+ await prisma.user.deleteMany();
31
+ await prisma.school.deleteMany();
32
+ }
33
+
34
+ export async function createUser(email: string, password: string, username: string) {
35
+ logger.debug("Creating user", { email, username, password });
36
+
37
+ const hashedPassword = await hash(password, 10);
38
+ return await prisma.user.create({
39
+ data: { email, password: hashedPassword, username, verified: true },
40
+ });
41
+ }
42
+
43
+ export async function addNotification(userId: string, title: string, content: string) {
44
+ return await prisma.notification.create({
45
+ data: {
46
+ receiverId: userId,
47
+ title,
48
+ content,
49
+ },
50
+ });
51
+ }
52
+
53
+ export const seedDatabase = async () => {
54
+ await clearDatabase();
55
+ logger.info('Cleared database');
56
+
57
+ // create two test users
58
+ const teacher1 = await createUser('teacher1@studious.sh', '123456', 'teacher1');
59
+ const student1 = await createUser('student1@studious.sh', '123456', 'student1');
60
+
61
+ // create a class
62
+ const class1 = await prisma.class.create({
63
+ data: {
64
+ name: 'Class 1',
65
+ subject: 'Math',
66
+ section: 'A',
67
+ teachers: {
68
+ connect: {
69
+ id: teacher1.id,
70
+ }
71
+ },
72
+ students: {
73
+ connect: {
74
+ id: student1.id,
75
+ }
76
+ }
77
+ },
78
+ });
79
+
80
+ await addNotification(teacher1.id, 'Welcome to Studious', 'Welcome to Studious');
81
+ await addNotification(student1.id, 'Welcome to Studious', 'Welcome to Studious');
82
+ };
83
+
84
+ (async () => {
85
+ logger.info('Seeding database');
86
+ await seedDatabase();
87
+ logger.info('Database seeded');
88
+ })();
@@ -1,15 +1,15 @@
1
1
  import { Socket, Server } from 'socket.io';
2
- import { logger } from '../utils/logger';
2
+ import { logger } from '../utils/logger.js';
3
3
 
4
4
  export const setupSocketHandlers = (io: Server) => {
5
5
  io.on('connection', (socket: Socket) => {
6
6
  logger.info('Client connected', { socketId: socket.id });
7
7
 
8
- socket.on('disconnect', (reason) => {
8
+ socket.on('disconnect', (reason: string) => {
9
9
  logger.info('Client disconnected', { socketId: socket.id, reason });
10
10
  });
11
11
 
12
- socket.on('error', (error) => {
12
+ socket.on('error', (error: Error) => {
13
13
  logger.error('Socket error', { socketId: socket.id, error });
14
14
  });
15
15
 
@@ -29,7 +29,7 @@ export const setupSocketHandlers = (io: Server) => {
29
29
  logger.info('Class updated', { class: data.class });
30
30
  });
31
31
 
32
- socket.on('join-class', (classId: string, callback) => {
32
+ socket.on('join-class', (classId: string, callback: (classId: string) => void) => {
33
33
  try {
34
34
  socket.join(`class-${classId}`);
35
35
  logger.info('Client joined class room', { socketId: socket.id, classId });
@@ -42,7 +42,7 @@ export const setupSocketHandlers = (io: Server) => {
42
42
  } catch (error) {
43
43
  logger.error('Error joining class room', { socketId: socket.id, classId, error });
44
44
  if (callback) {
45
- callback(null);
45
+ callback(classId);
46
46
  }
47
47
  }
48
48
  });
package/src/trpc.ts CHANGED
@@ -1,11 +1,12 @@
1
1
  import { initTRPC, TRPCError } from '@trpc/server';
2
2
  import { ZodError } from 'zod';
3
- import { logger } from './utils/logger';
4
- import { prisma } from './lib/prisma';
5
- import { createLoggingMiddleware } from './middleware/logging';
6
- import { createAuthMiddleware } from './middleware/auth';
3
+ import { logger } from './utils/logger.js';
4
+ import { prisma } from './lib/prisma.js';
5
+ import { createLoggingMiddleware } from './middleware/logging.js';
6
+ import { createAuthMiddleware } from './middleware/auth.js';
7
7
  import { Request, Response } from 'express';
8
8
  import { z } from 'zod';
9
+ import { handlePrismaError, PrismaErrorInfo } from './utils/prismaErrorHandler.js';
9
10
 
10
11
  interface CreateContextOptions {
11
12
  req: Request;
@@ -50,11 +51,22 @@ export const createTRPCContext = async (opts: CreateContextOptions): Promise<Con
50
51
 
51
52
  export const t = initTRPC.context<Context>().create({
52
53
  errorFormatter({ shape, error }) {
54
+ // Handle Prisma errors specifically
55
+ let prismaErrorInfo: PrismaErrorInfo | null = null;
56
+ if (error.cause) {
57
+ try {
58
+ prismaErrorInfo = handlePrismaError(error.cause);
59
+ } catch (e) {
60
+ // If Prisma error handling fails, continue with normal error handling
61
+ }
62
+ }
63
+
53
64
  logger.error('tRPC Error', {
54
65
  code: shape.code,
55
66
  message: error.message,
56
67
  cause: error.cause,
57
68
  stack: error.stack,
69
+ prismaError: prismaErrorInfo,
58
70
  });
59
71
 
60
72
  return {
@@ -63,6 +75,7 @@ export const t = initTRPC.context<Context>().create({
63
75
  ...shape.data,
64
76
  zodError:
65
77
  error.cause instanceof ZodError ? error.cause.flatten() : null,
78
+ prismaError: prismaErrorInfo,
66
79
  },
67
80
  };
68
81
  },
package/src/types/trpc.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { inferAsyncReturnType } from '@trpc/server';
2
- import { createTRPCContext } from '../trpc';
2
+ import { createTRPCContext } from '../trpc.js';
3
3
 
4
4
  export type Context = inferAsyncReturnType<typeof createTRPCContext> & {
5
5
  isTeacher?: boolean;
@@ -37,10 +37,13 @@ class Logger {
37
37
  private levelEmojis: Record<LogLevel, string>;
38
38
 
39
39
  private constructor() {
40
- this.isDevelopment = process.env.NODE_ENV === 'development';
40
+
41
+ // this.isDevelopment = process.env.NODE_ENV === 'development';
42
+ this.isDevelopment = true;
41
43
 
42
44
  this.mode = (process.env.LOG_MODE as LogMode) || 'normal';
43
45
 
46
+
44
47
  this.levelColors = {
45
48
  [LogLevel.INFO]: colors.blue,
46
49
  [LogLevel.WARN]: colors.yellow,
@@ -107,9 +110,16 @@ class Logger {
107
110
  }
108
111
 
109
112
  private log(level: LogLevel, message: string, context?: Record<string, any>) {
110
- // if (this.shouldLog(level))
111
- // return;
112
-
113
+ if (!this.shouldLog(level))
114
+ return;
115
+
116
+ if (level == LogLevel.WARN || level == LogLevel.ERROR) {
117
+ if (level == LogLevel.ERROR) {
118
+ // alert me
119
+ }
120
+ // store in database
121
+ }
122
+
113
123
  const logMessage: LogMessage = {
114
124
  level,
115
125
  message,
@@ -0,0 +1,275 @@
1
+ import {
2
+ PrismaClientKnownRequestError,
3
+ PrismaClientUnknownRequestError,
4
+ PrismaClientValidationError
5
+ } from '@prisma/client/runtime/library';
6
+
7
+ export interface PrismaErrorInfo {
8
+ message: string;
9
+ code?: string;
10
+ meta?: any;
11
+ details?: string;
12
+ }
13
+
14
+ export function handlePrismaError(error: unknown): PrismaErrorInfo {
15
+ // PrismaClientKnownRequestError - Database constraint violations, etc.
16
+ if (error instanceof PrismaClientKnownRequestError) {
17
+ return handleKnownRequestError(error);
18
+ }
19
+
20
+ // PrismaClientValidationError - Invalid data format, missing required fields, etc.
21
+ if (error instanceof PrismaClientValidationError) {
22
+ return handleValidationError(error);
23
+ }
24
+
25
+ // PrismaClientUnknownRequestError - Unknown database errors
26
+ if (error instanceof PrismaClientUnknownRequestError) {
27
+ return handleUnknownRequestError(error);
28
+ }
29
+
30
+ // Generic error fallback
31
+ if (error instanceof Error) {
32
+ return {
33
+ message: error.message,
34
+ details: error.stack
35
+ };
36
+ }
37
+
38
+ return {
39
+ message: 'An unknown database error occurred',
40
+ details: String(error)
41
+ };
42
+ }
43
+
44
+ function handleKnownRequestError(error: PrismaClientKnownRequestError): PrismaErrorInfo {
45
+ const { code, meta, message } = error;
46
+
47
+ switch (code) {
48
+ case 'P2002':
49
+ const target = Array.isArray(meta?.target) ? meta.target.join(', ') : meta?.target || 'field';
50
+ return {
51
+ message: `A record with this ${target} already exists`,
52
+ code,
53
+ meta,
54
+ details: `Unique constraint violation on ${target}`
55
+ };
56
+
57
+ case 'P2003':
58
+ const fieldName = meta?.field_name || 'related field';
59
+ return {
60
+ message: `Cannot delete this record because it's referenced by other records`,
61
+ code,
62
+ meta,
63
+ details: `Foreign key constraint violation on ${fieldName}`
64
+ };
65
+
66
+ case 'P2025':
67
+ return {
68
+ message: 'The record you are trying to update or delete does not exist',
69
+ code,
70
+ meta,
71
+ details: 'Record not found in database'
72
+ };
73
+
74
+ case 'P2014':
75
+ return {
76
+ message: 'The change you are trying to make would violate the required relationship',
77
+ code,
78
+ meta,
79
+ details: 'Required relation violation'
80
+ };
81
+
82
+ case 'P2011':
83
+ return {
84
+ message: 'A required field is missing or empty',
85
+ code,
86
+ meta,
87
+ details: 'Null constraint violation'
88
+ };
89
+
90
+ case 'P2012':
91
+ return {
92
+ message: 'The data you provided is not in the correct format',
93
+ code,
94
+ meta,
95
+ details: 'Data validation error'
96
+ };
97
+
98
+ case 'P2013':
99
+ return {
100
+ message: 'The data you provided is too long for this field',
101
+ code,
102
+ meta,
103
+ details: 'String length constraint violation'
104
+ };
105
+
106
+ case 'P2015':
107
+ return {
108
+ message: 'The record you are looking for could not be found',
109
+ code,
110
+ meta,
111
+ details: 'Record not found'
112
+ };
113
+
114
+ case 'P2016':
115
+ return {
116
+ message: 'The query you are trying to execute is not valid',
117
+ code,
118
+ meta,
119
+ details: 'Query interpretation error'
120
+ };
121
+
122
+ case 'P2017':
123
+ return {
124
+ message: 'The relationship between records is not properly connected',
125
+ code,
126
+ meta,
127
+ details: 'Relation connection error'
128
+ };
129
+
130
+ case 'P2018':
131
+ return {
132
+ message: 'The connected record you are looking for does not exist',
133
+ code,
134
+ meta,
135
+ details: 'Connected record not found'
136
+ };
137
+
138
+ case 'P2019':
139
+ return {
140
+ message: 'The input you provided is not valid',
141
+ code,
142
+ meta,
143
+ details: 'Input error'
144
+ };
145
+
146
+ case 'P2020':
147
+ return {
148
+ message: 'The value you provided is outside the allowed range',
149
+ code,
150
+ meta,
151
+ details: 'Value out of range'
152
+ };
153
+
154
+ case 'P2021':
155
+ return {
156
+ message: 'The table you are trying to access does not exist',
157
+ code,
158
+ meta,
159
+ details: 'Table does not exist'
160
+ };
161
+
162
+ case 'P2022':
163
+ return {
164
+ message: 'The column you are trying to access does not exist',
165
+ code,
166
+ meta,
167
+ details: 'Column does not exist'
168
+ };
169
+
170
+ case 'P2023':
171
+ return {
172
+ message: 'The column data is not valid',
173
+ code,
174
+ meta,
175
+ details: 'Column data validation error'
176
+ };
177
+
178
+ case 'P2024':
179
+ return {
180
+ message: 'The database connection pool is exhausted',
181
+ code,
182
+ meta,
183
+ details: 'Connection pool timeout'
184
+ };
185
+
186
+ case 'P2026':
187
+ return {
188
+ message: 'The current database provider does not support this feature',
189
+ code,
190
+ meta,
191
+ details: 'Feature not supported by database provider'
192
+ };
193
+
194
+ case 'P2027':
195
+ return {
196
+ message: 'Multiple errors occurred during the database operation',
197
+ code,
198
+ meta,
199
+ details: 'Multiple errors in query execution'
200
+ };
201
+
202
+ default:
203
+ return {
204
+ message: 'A database constraint was violated',
205
+ code,
206
+ meta,
207
+ details: message
208
+ };
209
+ }
210
+ }
211
+
212
+ function handleValidationError(error: PrismaClientValidationError): PrismaErrorInfo {
213
+ return {
214
+ message: 'The data you provided is not valid',
215
+ details: error.message,
216
+ meta: {
217
+ type: 'validation_error',
218
+ originalMessage: error.message
219
+ }
220
+ };
221
+ }
222
+
223
+ function handleUnknownRequestError(error: PrismaClientUnknownRequestError): PrismaErrorInfo {
224
+ return {
225
+ message: 'An unexpected database error occurred',
226
+ details: error.message,
227
+ meta: {
228
+ type: 'unknown_request_error',
229
+ originalMessage: error.message
230
+ }
231
+ };
232
+ }
233
+
234
+ // Helper function to get user-friendly field names
235
+ export function getFieldDisplayName(fieldName: string): string {
236
+ const fieldMap: Record<string, string> = {
237
+ 'username': 'username',
238
+ 'email': 'email address',
239
+ 'password': 'password',
240
+ 'name': 'name',
241
+ 'title': 'title',
242
+ 'content': 'content',
243
+ 'description': 'description',
244
+ 'subject': 'subject',
245
+ 'section': 'section',
246
+ 'color': 'color',
247
+ 'location': 'location',
248
+ 'startTime': 'start time',
249
+ 'endTime': 'end time',
250
+ 'dueDate': 'due date',
251
+ 'maxGrade': 'maximum grade',
252
+ 'grade': 'grade',
253
+ 'feedback': 'feedback',
254
+ 'remarks': 'remarks',
255
+ 'syllabus': 'syllabus',
256
+ 'path': 'file path',
257
+ 'size': 'file size',
258
+ 'type': 'file type',
259
+ 'uploadedAt': 'upload date',
260
+ 'verified': 'verification status',
261
+ 'profileId': 'profile',
262
+ 'schoolId': 'school',
263
+ 'classId': 'class',
264
+ 'assignmentId': 'assignment',
265
+ 'submissionId': 'submission',
266
+ 'userId': 'user',
267
+ 'eventId': 'event',
268
+ 'sessionId': 'session',
269
+ 'thumbnailId': 'thumbnail',
270
+ 'annotationId': 'annotation',
271
+ 'logoId': 'logo'
272
+ };
273
+
274
+ return fieldMap[fieldName] || fieldName;
275
+ }
@@ -0,0 +1,91 @@
1
+ import { TRPCError } from '@trpc/server';
2
+ import { handlePrismaError } from './prismaErrorHandler.js';
3
+
4
+ export async function withPrismaErrorHandling<T>(
5
+ operation: () => Promise<T>,
6
+ context: string = 'database operation'
7
+ ): Promise<T> {
8
+ try {
9
+ return await operation();
10
+ } catch (error) {
11
+ const prismaErrorInfo = handlePrismaError(error);
12
+
13
+ // Log the detailed error for debugging
14
+ console.error(`Prisma error in ${context}:`, {
15
+ userMessage: prismaErrorInfo.message,
16
+ details: prismaErrorInfo.details,
17
+ code: prismaErrorInfo.code,
18
+ meta: prismaErrorInfo.meta,
19
+ originalError: error
20
+ });
21
+
22
+ // Throw a tRPC error with the user-friendly message
23
+ throw new TRPCError({
24
+ code: getTRPCCode(prismaErrorInfo.code),
25
+ message: prismaErrorInfo.message,
26
+ cause: error
27
+ });
28
+ }
29
+ }
30
+
31
+ function getTRPCCode(prismaCode?: string): TRPCError['code'] {
32
+ if (!prismaCode) return 'INTERNAL_SERVER_ERROR';
33
+
34
+ // Map Prisma error codes to tRPC error codes
35
+ const codeMap: Record<string, TRPCError['code']> = {
36
+ 'P2002': 'CONFLICT', // Unique constraint violation
37
+ 'P2003': 'BAD_REQUEST', // Foreign key constraint violation
38
+ 'P2025': 'NOT_FOUND', // Record not found
39
+ 'P2014': 'BAD_REQUEST', // Required relation violation
40
+ 'P2011': 'BAD_REQUEST', // Null constraint violation
41
+ 'P2012': 'BAD_REQUEST', // Data validation error
42
+ 'P2013': 'BAD_REQUEST', // String length constraint violation
43
+ 'P2015': 'NOT_FOUND', // Record not found
44
+ 'P2016': 'BAD_REQUEST', // Query interpretation error
45
+ 'P2017': 'BAD_REQUEST', // Relation connection error
46
+ 'P2018': 'NOT_FOUND', // Connected record not found
47
+ 'P2019': 'BAD_REQUEST', // Input error
48
+ 'P2020': 'BAD_REQUEST', // Value out of range
49
+ 'P2021': 'INTERNAL_SERVER_ERROR', // Table does not exist
50
+ 'P2022': 'INTERNAL_SERVER_ERROR', // Column does not exist
51
+ 'P2023': 'BAD_REQUEST', // Column data validation error
52
+ 'P2024': 'INTERNAL_SERVER_ERROR', // Connection pool timeout
53
+ 'P2026': 'INTERNAL_SERVER_ERROR', // Feature not supported
54
+ 'P2027': 'INTERNAL_SERVER_ERROR', // Multiple errors
55
+ };
56
+
57
+ return codeMap[prismaCode] || 'INTERNAL_SERVER_ERROR';
58
+ }
59
+
60
+ // Convenience functions for common operations
61
+ export const prismaWrapper = {
62
+ findUnique: <T>(operation: () => Promise<T | null>, context?: string) =>
63
+ withPrismaErrorHandling(operation, context),
64
+
65
+ findFirst: <T>(operation: () => Promise<T | null>, context?: string) =>
66
+ withPrismaErrorHandling(operation, context),
67
+
68
+ findMany: <T>(operation: () => Promise<T[]>, context?: string) =>
69
+ withPrismaErrorHandling(operation, context),
70
+
71
+ create: <T>(operation: () => Promise<T>, context?: string) =>
72
+ withPrismaErrorHandling(operation, context),
73
+
74
+ update: <T>(operation: () => Promise<T>, context?: string) =>
75
+ withPrismaErrorHandling(operation, context),
76
+
77
+ delete: <T>(operation: () => Promise<T>, context?: string) =>
78
+ withPrismaErrorHandling(operation, context),
79
+
80
+ deleteMany: <T>(operation: () => Promise<T>, context?: string) =>
81
+ withPrismaErrorHandling(operation, context),
82
+
83
+ upsert: <T>(operation: () => Promise<T>, context?: string) =>
84
+ withPrismaErrorHandling(operation, context),
85
+
86
+ count: <T>(operation: () => Promise<T>, context?: string) =>
87
+ withPrismaErrorHandling(operation, context),
88
+
89
+ aggregate: <T>(operation: () => Promise<T>, context?: string) =>
90
+ withPrismaErrorHandling(operation, context),
91
+ };