@studious-lms/server 1.0.1 → 1.0.3

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 (66) hide show
  1. package/dist/index.js +4 -4
  2. package/dist/middleware/auth.js +1 -1
  3. package/dist/middleware/logging.js +1 -1
  4. package/dist/routers/_app.js +1 -1
  5. package/dist/routers/agenda.js +1 -1
  6. package/dist/routers/announcement.js +1 -1
  7. package/dist/routers/assignment.js +3 -3
  8. package/dist/routers/attendance.js +1 -1
  9. package/dist/routers/auth.js +2 -2
  10. package/dist/routers/class.js +2 -2
  11. package/dist/routers/event.js +1 -1
  12. package/dist/routers/file.js +2 -2
  13. package/dist/routers/section.js +1 -1
  14. package/dist/routers/user.js +2 -2
  15. package/dist/trpc.js +2 -2
  16. package/package.json +1 -6
  17. package/prisma/schema.prisma +228 -0
  18. package/src/exportType.ts +9 -0
  19. package/src/index.ts +94 -0
  20. package/src/lib/fileUpload.ts +163 -0
  21. package/src/lib/googleCloudStorage.ts +94 -0
  22. package/src/lib/prisma.ts +16 -0
  23. package/src/lib/thumbnailGenerator.ts +185 -0
  24. package/src/logger.ts +163 -0
  25. package/src/middleware/auth.ts +191 -0
  26. package/src/middleware/logging.ts +54 -0
  27. package/src/routers/_app.ts +34 -0
  28. package/src/routers/agenda.ts +79 -0
  29. package/src/routers/announcement.ts +134 -0
  30. package/src/routers/assignment.ts +1614 -0
  31. package/src/routers/attendance.ts +284 -0
  32. package/src/routers/auth.ts +286 -0
  33. package/src/routers/class.ts +753 -0
  34. package/src/routers/event.ts +509 -0
  35. package/src/routers/file.ts +96 -0
  36. package/src/routers/section.ts +138 -0
  37. package/src/routers/user.ts +82 -0
  38. package/src/socket/handlers.ts +143 -0
  39. package/src/trpc.ts +90 -0
  40. package/src/types/trpc.ts +15 -0
  41. package/src/utils/email.ts +11 -0
  42. package/src/utils/generateInviteCode.ts +8 -0
  43. package/src/utils/logger.ts +156 -0
  44. package/tsconfig.json +17 -0
  45. package/generated/prisma/client.d.ts +0 -1
  46. package/generated/prisma/client.js +0 -4
  47. package/generated/prisma/default.d.ts +0 -1
  48. package/generated/prisma/default.js +0 -4
  49. package/generated/prisma/edge.d.ts +0 -1
  50. package/generated/prisma/edge.js +0 -389
  51. package/generated/prisma/index-browser.js +0 -375
  52. package/generated/prisma/index.d.ts +0 -34865
  53. package/generated/prisma/index.js +0 -410
  54. package/generated/prisma/libquery_engine-darwin-arm64.dylib.node +0 -0
  55. package/generated/prisma/package.json +0 -140
  56. package/generated/prisma/runtime/edge-esm.js +0 -34
  57. package/generated/prisma/runtime/edge.js +0 -34
  58. package/generated/prisma/runtime/index-browser.d.ts +0 -370
  59. package/generated/prisma/runtime/index-browser.js +0 -16
  60. package/generated/prisma/runtime/library.d.ts +0 -3647
  61. package/generated/prisma/runtime/library.js +0 -146
  62. package/generated/prisma/runtime/react-native.js +0 -83
  63. package/generated/prisma/runtime/wasm.js +0 -35
  64. package/generated/prisma/schema.prisma +0 -304
  65. package/generated/prisma/wasm.d.ts +0 -1
  66. package/generated/prisma/wasm.js +0 -375
@@ -0,0 +1,163 @@
1
+ import { TRPCError } from "@trpc/server";
2
+ import { v4 as uuidv4 } from "uuid";
3
+ import { uploadFile as uploadToGCS, getSignedUrl } from "./googleCloudStorage";
4
+ import { generateThumbnail, storeThumbnail, generateMediaThumbnail } from "./thumbnailGenerator";
5
+ import { prisma } from "./prisma";
6
+
7
+ export interface FileData {
8
+ name: string;
9
+ type: string;
10
+ size: number;
11
+ data: string; // base64 encoded file data
12
+ }
13
+
14
+ export interface UploadedFile {
15
+ id: string;
16
+ name: string;
17
+ type: string;
18
+ size: number;
19
+ path: string;
20
+ thumbnailId?: string;
21
+ }
22
+
23
+ /**
24
+ * Uploads a single file to Google Cloud Storage and creates a file record
25
+ * @param file The file data to upload
26
+ * @param userId The ID of the user uploading the file
27
+ * @param directory Optional directory to store the file in
28
+ * @param assignmentId Optional assignment ID to associate the file with
29
+ * @returns The uploaded file record
30
+ */
31
+ export async function uploadFile(
32
+ file: FileData,
33
+ userId: string,
34
+ directory?: string,
35
+ assignmentId?: string
36
+ ): Promise<UploadedFile> {
37
+ try {
38
+ // Create a unique filename
39
+ const fileExtension = file.name.split('.').pop();
40
+ const uniqueFilename = `${uuidv4()}.${fileExtension}`;
41
+
42
+ // Construct the full path
43
+ const filePath = directory
44
+ ? `${directory}/${uniqueFilename}`
45
+ : uniqueFilename;
46
+
47
+ // Upload to Google Cloud Storage
48
+ const uploadedPath = await uploadToGCS(file.data, filePath, file.type);
49
+
50
+ // Generate and store thumbnail if supported
51
+ let thumbnailId: string | undefined;
52
+ try {
53
+ // Convert base64 to buffer for thumbnail generation
54
+ const base64Data = file.data.split(',')[1];
55
+ const fileBuffer = Buffer.from(base64Data, 'base64');
56
+
57
+ // Generate thumbnail directly from buffer
58
+ const thumbnailBuffer = await generateMediaThumbnail(fileBuffer, file.type);
59
+ if (thumbnailBuffer) {
60
+ // Store thumbnail in a thumbnails directory
61
+ const thumbnailPath = `thumbnails/${filePath}`;
62
+ const thumbnailBase64 = `data:image/jpeg;base64,${thumbnailBuffer.toString('base64')}`;
63
+ await uploadToGCS(thumbnailBase64, thumbnailPath, 'image/jpeg');
64
+
65
+ // Create thumbnail file record
66
+ const thumbnailFile = await prisma.file.create({
67
+ data: {
68
+ name: `${file.name}_thumb.jpg`,
69
+ type: 'image/jpeg',
70
+ path: thumbnailPath,
71
+ user: {
72
+ connect: { id: userId }
73
+ }
74
+ }
75
+ });
76
+
77
+ thumbnailId = thumbnailFile.id;
78
+ }
79
+ } catch (error) {
80
+ console.warn('Failed to generate thumbnail:', error);
81
+ }
82
+
83
+ // Create file record in database
84
+ const fileRecord = await prisma.file.create({
85
+ data: {
86
+ name: file.name,
87
+ type: file.type,
88
+ size: file.size,
89
+ path: uploadedPath,
90
+ user: {
91
+ connect: { id: userId }
92
+ },
93
+ ...(thumbnailId && {
94
+ thumbnail: {
95
+ connect: { id: thumbnailId }
96
+ }
97
+ }),
98
+ ...(assignmentId && {
99
+ assignment: {
100
+ connect: { id: assignmentId }
101
+ }
102
+ })
103
+ },
104
+ });
105
+
106
+ // Return file information
107
+ return {
108
+ id: fileRecord.id,
109
+ name: file.name,
110
+ type: file.type,
111
+ size: file.size,
112
+ path: uploadedPath,
113
+ thumbnailId: thumbnailId
114
+ };
115
+ } catch (error) {
116
+ console.error('Error uploading file:', error);
117
+ throw new TRPCError({
118
+ code: 'INTERNAL_SERVER_ERROR',
119
+ message: 'Failed to upload file',
120
+ });
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Uploads multiple files
126
+ * @param files Array of files to upload
127
+ * @param userId The ID of the user uploading the files
128
+ * @param directory Optional subdirectory to store the files in
129
+ * @returns Array of uploaded file information
130
+ */
131
+ export async function uploadFiles(
132
+ files: FileData[],
133
+ userId: string,
134
+ directory?: string
135
+ ): Promise<UploadedFile[]> {
136
+ try {
137
+ const uploadPromises = files.map(file => uploadFile(file, userId, directory));
138
+ return await Promise.all(uploadPromises);
139
+ } catch (error) {
140
+ console.error('Error uploading files:', error);
141
+ throw new TRPCError({
142
+ code: 'INTERNAL_SERVER_ERROR',
143
+ message: 'Failed to upload files',
144
+ });
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Gets a signed URL for a file
150
+ * @param filePath The path of the file in Google Cloud Storage
151
+ * @returns The signed URL
152
+ */
153
+ export async function getFileUrl(filePath: string): Promise<string> {
154
+ try {
155
+ return await getSignedUrl(filePath);
156
+ } catch (error) {
157
+ console.error('Error getting signed URL:', error);
158
+ throw new TRPCError({
159
+ code: 'INTERNAL_SERVER_ERROR',
160
+ message: 'Failed to get file URL',
161
+ });
162
+ }
163
+ }
@@ -0,0 +1,94 @@
1
+ import { Storage } from '@google-cloud/storage';
2
+ import { TRPCError } from '@trpc/server';
3
+
4
+ const storage = new Storage({
5
+ projectId: process.env.GOOGLE_CLOUD_PROJECT_ID,
6
+ credentials: {
7
+ client_email: process.env.GOOGLE_CLOUD_CLIENT_EMAIL,
8
+ private_key: process.env.GOOGLE_CLOUD_PRIVATE_KEY?.replace(/\\n/g, '\n'),
9
+ },
10
+ });
11
+
12
+ const bucket = storage.bucket(process.env.GOOGLE_CLOUD_BUCKET_NAME || '');
13
+
14
+ // Short expiration time for signed URLs (5 minutes)
15
+ const SIGNED_URL_EXPIRATION = 5 * 60 * 1000;
16
+
17
+ /**
18
+ * Uploads a file to Google Cloud Storage
19
+ * @param base64Data Base64 encoded file data
20
+ * @param filePath The path where the file should be stored
21
+ * @param contentType The MIME type of the file
22
+ * @returns The path of the uploaded file
23
+ */
24
+ export async function uploadFile(
25
+ base64Data: string,
26
+ filePath: string,
27
+ contentType: string
28
+ ): Promise<string> {
29
+ try {
30
+ // Remove the data URL prefix if present
31
+ const base64Content = base64Data.includes('base64,')
32
+ ? base64Data.split('base64,')[1]
33
+ : base64Data;
34
+
35
+ // Convert base64 to buffer
36
+ const fileBuffer = Buffer.from(base64Content, 'base64');
37
+
38
+ // Create a new file in the bucket
39
+ const file = bucket.file(filePath);
40
+
41
+ // Upload the file
42
+ await file.save(fileBuffer, {
43
+ metadata: {
44
+ contentType,
45
+ },
46
+ });
47
+
48
+ return filePath;
49
+ } catch (error) {
50
+ console.error('Error uploading to Google Cloud Storage:', error);
51
+ throw new TRPCError({
52
+ code: 'INTERNAL_SERVER_ERROR',
53
+ message: 'Failed to upload file to storage',
54
+ });
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Gets a signed URL for a file
60
+ * @param filePath The path of the file in the bucket
61
+ * @returns The signed URL
62
+ */
63
+ export async function getSignedUrl(filePath: string): Promise<string> {
64
+ try {
65
+ const [url] = await bucket.file(filePath).getSignedUrl({
66
+ version: 'v4',
67
+ action: 'read',
68
+ expires: Date.now() + SIGNED_URL_EXPIRATION,
69
+ });
70
+ return url;
71
+ } catch (error) {
72
+ console.error('Error getting signed URL:', error);
73
+ throw new TRPCError({
74
+ code: 'INTERNAL_SERVER_ERROR',
75
+ message: 'Failed to get signed URL',
76
+ });
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Deletes a file from Google Cloud Storage
82
+ * @param filePath The path of the file to delete
83
+ */
84
+ export async function deleteFile(filePath: string): Promise<void> {
85
+ try {
86
+ await bucket.file(filePath).delete();
87
+ } catch (error) {
88
+ console.error('Error deleting file from Google Cloud Storage:', error);
89
+ throw new TRPCError({
90
+ code: 'INTERNAL_SERVER_ERROR',
91
+ message: 'Failed to delete file from storage',
92
+ });
93
+ }
94
+ }
@@ -0,0 +1,16 @@
1
+ import { PrismaClient } from '@prisma/client';
2
+
3
+ const prismaClientSingleton = () => {
4
+ return new PrismaClient();
5
+ };
6
+
7
+ // Prevent multiple instances of Prisma Client in development
8
+ declare global {
9
+ var prisma: undefined | ReturnType<typeof prismaClientSingleton>;
10
+ }
11
+
12
+ export const prisma = globalThis.prisma ?? prismaClientSingleton();
13
+
14
+ if (process.env.NODE_ENV !== 'production') {
15
+ globalThis.prisma = prisma;
16
+ }
@@ -0,0 +1,185 @@
1
+ import sharp from 'sharp';
2
+ import { prisma } from './prisma';
3
+ import { uploadFile, deleteFile, getSignedUrl } from './googleCloudStorage';
4
+
5
+ // Thumbnail size configuration
6
+ const THUMBNAIL_WIDTH = 200;
7
+ const THUMBNAIL_HEIGHT = 200;
8
+
9
+ // File type configurations
10
+ const SUPPORTED_IMAGE_TYPES = [
11
+ 'image/jpeg',
12
+ 'image/png',
13
+ 'image/gif',
14
+ 'image/webp',
15
+ 'image/tiff',
16
+ 'image/bmp',
17
+ 'image/avif'
18
+ ];
19
+
20
+ const DOCUMENT_TYPES = [
21
+ 'application/pdf',
22
+ 'application/msword',
23
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // .docx
24
+ 'application/vnd.ms-excel',
25
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx
26
+ 'application/vnd.ms-powerpoint',
27
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation', // .pptx
28
+ 'text/plain',
29
+ 'text/csv',
30
+ 'application/json',
31
+ 'text/html',
32
+ 'text/javascript',
33
+ 'text/css'
34
+ ];
35
+
36
+ const VIDEO_TYPES = [
37
+ 'video/mp4',
38
+ 'video/webm',
39
+ 'video/ogg',
40
+ 'video/quicktime'
41
+ ];
42
+
43
+ const AUDIO_TYPES = [
44
+ 'audio/mpeg',
45
+ 'audio/ogg',
46
+ 'audio/wav',
47
+ 'audio/webm'
48
+ ];
49
+
50
+ /**
51
+ * Generates a thumbnail for an image or PDF file
52
+ * @param fileBuffer The file buffer
53
+ * @param fileType The MIME type of the file
54
+ * @returns Thumbnail buffer
55
+ */
56
+ export async function generateMediaThumbnail(fileBuffer: Buffer, fileType: string): Promise<Buffer> {
57
+ if (fileType === 'application/pdf') {
58
+ // For PDFs, we need to use a different approach
59
+ try {
60
+ return await sharp(fileBuffer, {
61
+ density: 300, // Higher density for better quality
62
+ page: 0 // First page only
63
+ })
64
+ .resize(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT, {
65
+ fit: 'inside',
66
+ withoutEnlargement: true,
67
+ })
68
+ .jpeg({ quality: 80 })
69
+ .toBuffer();
70
+ } catch (error) {
71
+ console.warn('Failed to generate PDF thumbnail:', error);
72
+ return generateGenericThumbnail(fileType);
73
+ }
74
+ }
75
+
76
+ // For regular images
77
+ return sharp(fileBuffer)
78
+ .resize(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT, {
79
+ fit: 'inside',
80
+ withoutEnlargement: true,
81
+ })
82
+ .jpeg({ quality: 80 })
83
+ .toBuffer();
84
+ }
85
+
86
+ /**
87
+ * Generates a generic icon-based thumbnail for a file type
88
+ * @param fileType The MIME type of the file
89
+ * @returns Thumbnail buffer
90
+ */
91
+ async function generateGenericThumbnail(fileType: string): Promise<Buffer> {
92
+ // Create a blank canvas with a colored background based on file type
93
+ const canvas = sharp({
94
+ create: {
95
+ width: THUMBNAIL_WIDTH,
96
+ height: THUMBNAIL_HEIGHT,
97
+ channels: 4,
98
+ background: { r: 245, g: 245, b: 245, alpha: 1 }
99
+ }
100
+ });
101
+
102
+ // Add a colored overlay based on file type
103
+ let color = { r: 200, g: 200, b: 200, alpha: 0.5 }; // Default gray
104
+
105
+ if (DOCUMENT_TYPES.includes(fileType)) {
106
+ color = { r: 52, g: 152, b: 219, alpha: 0.5 }; // Blue for documents
107
+ } else if (VIDEO_TYPES.includes(fileType)) {
108
+ color = { r: 231, g: 76, b: 60, alpha: 0.5 }; // Red for videos
109
+ } else if (AUDIO_TYPES.includes(fileType)) {
110
+ color = { r: 46, g: 204, b: 113, alpha: 0.5 }; // Green for audio
111
+ }
112
+
113
+ return canvas
114
+ .composite([{
115
+ input: Buffer.from([color.r, color.g, color.b, Math.floor(color.alpha * 255)]),
116
+ raw: {
117
+ width: 1,
118
+ height: 1,
119
+ channels: 4
120
+ },
121
+ tile: true,
122
+ blend: 'overlay'
123
+ }])
124
+ .jpeg({ quality: 80 })
125
+ .toBuffer();
126
+ }
127
+
128
+ /**
129
+ * Generates a thumbnail for a file
130
+ * @param fileName The name of the file in Google Cloud Storage
131
+ * @param fileType The MIME type of the file
132
+ * @returns The thumbnail buffer or null if thumbnail generation is not supported
133
+ */
134
+ export async function generateThumbnail(fileName: string, fileType: string): Promise<Buffer | null> {
135
+ try {
136
+ const signedUrl = await getSignedUrl(fileName);
137
+ const response = await fetch(signedUrl);
138
+
139
+ if (!response.ok) {
140
+ throw new Error(`Failed to download file from storage: ${response.status} ${response.statusText}`);
141
+ }
142
+
143
+ const fileBuffer = await response.arrayBuffer();
144
+
145
+ if (SUPPORTED_IMAGE_TYPES.includes(fileType) || fileType === 'application/pdf') {
146
+ try {
147
+ const thumbnail = await generateMediaThumbnail(Buffer.from(fileBuffer), fileType);
148
+ return thumbnail;
149
+ } catch (error) {
150
+ return generateGenericThumbnail(fileType);
151
+ }
152
+ } else if ([...DOCUMENT_TYPES, ...VIDEO_TYPES, ...AUDIO_TYPES].includes(fileType)) {
153
+ return generateGenericThumbnail(fileType);
154
+ }
155
+
156
+ return null; // Unsupported file type
157
+ } catch (error) {
158
+ return null;
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Stores a thumbnail in Google Cloud Storage and creates a File entry
164
+ * @param thumbnailBuffer The thumbnail buffer to store
165
+ * @param originalFileName The original file name
166
+ * @param userId The user ID who owns the file
167
+ * @returns The ID of the created thumbnail File
168
+ */
169
+ export async function storeThumbnail(thumbnailBuffer: Buffer, originalFileName: string, userId: string): Promise<string> {
170
+ // Convert buffer to base64 for uploadFile function
171
+ const base64Data = `data:image/jpeg;base64,${thumbnailBuffer.toString('base64')}`;
172
+ const thumbnailFileName = await uploadFile(base64Data, `thumbnails/${originalFileName}_thumb`, 'image/jpeg');
173
+
174
+ // Create a new File entry for the thumbnail
175
+ const newThumbnail = await prisma.file.create({
176
+ data: {
177
+ name: `${originalFileName}_thumb.jpg`,
178
+ path: thumbnailFileName,
179
+ type: 'image/jpeg',
180
+ userId: userId,
181
+ },
182
+ });
183
+
184
+ return newThumbnail.id;
185
+ }
package/src/logger.ts ADDED
@@ -0,0 +1,163 @@
1
+ export enum LogLevel {
2
+ INFO = 'info',
3
+ WARN = 'warn',
4
+ ERROR = 'error',
5
+ DEBUG = 'debug'
6
+ }
7
+
8
+ type LogMode = 'silent' | 'minimal' | 'normal' | 'verbose';
9
+
10
+ interface LogMessage {
11
+ level: LogLevel;
12
+ message: string;
13
+ timestamp: string;
14
+ context?: Record<string, any>;
15
+ }
16
+
17
+ // Environment detection
18
+ const isNode = typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
19
+ const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined';
20
+
21
+ // ANSI color codes for Node.js
22
+ const ansiColors = {
23
+ reset: '\x1b[0m',
24
+ bright: '\x1b[1m',
25
+ dim: '\x1b[2m',
26
+ red: '\x1b[31m',
27
+ green: '\x1b[32m',
28
+ yellow: '\x1b[33m',
29
+ blue: '\x1b[34m',
30
+ magenta: '\x1b[35m',
31
+ cyan: '\x1b[36m',
32
+ white: '\x1b[37m',
33
+ gray: '\x1b[90m'
34
+ };
35
+
36
+ // CSS color codes for browser console
37
+ const cssColors = {
38
+ reset: '%c',
39
+ info: 'color: #2196F3; font-weight: bold;',
40
+ warn: 'color: #FFC107; font-weight: bold;',
41
+ error: 'color: #F44336; font-weight: bold;',
42
+ debug: 'color: #9C27B0; font-weight: bold;',
43
+ gray: 'color: #9E9E9E;',
44
+ context: 'color: #757575; font-style: italic;'
45
+ };
46
+
47
+ class Logger {
48
+ private static instance: Logger;
49
+ private isDevelopment: boolean;
50
+ private mode: LogMode;
51
+ private levelEmojis: Record<LogLevel, string>;
52
+
53
+ private constructor() {
54
+ this.isDevelopment = process.env.NODE_ENV === 'development';
55
+ this.mode = (process.env.LOG_MODE as LogMode) || 'normal';
56
+
57
+ this.levelEmojis = {
58
+ [LogLevel.INFO]: 'ℹ️',
59
+ [LogLevel.WARN]: '⚠️',
60
+ [LogLevel.ERROR]: '❌',
61
+ [LogLevel.DEBUG]: '🔍'
62
+ };
63
+ }
64
+
65
+ public static getInstance(): Logger {
66
+ if (!Logger.instance) {
67
+ Logger.instance = new Logger();
68
+ }
69
+ return Logger.instance;
70
+ }
71
+
72
+ public setMode(mode: LogMode) {
73
+ this.mode = mode;
74
+ }
75
+
76
+ private shouldLog(level: LogLevel): boolean {
77
+ const silent = [LogLevel.ERROR];
78
+ const minimal = [LogLevel.ERROR, LogLevel.WARN];
79
+ const normal = [LogLevel.ERROR, LogLevel.WARN, LogLevel.INFO];
80
+
81
+ if (this.mode === 'silent') return silent.includes(level);
82
+ if (this.mode === 'minimal') return minimal.includes(level);
83
+ if (this.mode === 'normal') return normal.includes(level);
84
+ return true; // verbose mode
85
+ }
86
+
87
+ private formatMessage(logMessage: LogMessage): string | [string, ...string[]] {
88
+ const { level, message, timestamp, context } = logMessage;
89
+ const emoji = this.levelEmojis[level];
90
+
91
+ const timestampStr = `[${timestamp}]`;
92
+ const levelStr = `[${level.toUpperCase()}]`;
93
+ const messageStr = `${emoji} ${message}`;
94
+
95
+ const contextStr = context
96
+ ? `\nContext: ${JSON.stringify(context, null, 2)}`
97
+ : '';
98
+
99
+ if (isNode) {
100
+ const color = ansiColors[level === LogLevel.INFO ? 'blue' :
101
+ level === LogLevel.WARN ? 'yellow' :
102
+ level === LogLevel.ERROR ? 'red' : 'magenta'];
103
+
104
+ return `${ansiColors.gray}${timestampStr}${ansiColors.reset} ${color}${levelStr}${ansiColors.reset} ${messageStr}${contextStr}`;
105
+ } else {
106
+ const color = cssColors[level];
107
+ return [
108
+ `${timestampStr} ${levelStr} ${messageStr}${contextStr}`,
109
+ cssColors.reset,
110
+ color,
111
+ cssColors.gray,
112
+ cssColors.context
113
+ ];
114
+ }
115
+ }
116
+
117
+ private log(level: LogLevel, message: string, context?: Record<string, any>) {
118
+ if (!this.shouldLog(level)) return;
119
+
120
+ const logMessage: LogMessage = {
121
+ level,
122
+ message,
123
+ timestamp: new Date().toISOString(),
124
+ context
125
+ };
126
+
127
+ const formattedMessage = this.formatMessage(logMessage);
128
+
129
+ switch (level) {
130
+ case LogLevel.ERROR:
131
+ console.error(...(Array.isArray(formattedMessage) ? formattedMessage : [formattedMessage]));
132
+ break;
133
+ case LogLevel.WARN:
134
+ console.warn(...(Array.isArray(formattedMessage) ? formattedMessage : [formattedMessage]));
135
+ break;
136
+ case LogLevel.DEBUG:
137
+ if (this.isDevelopment) {
138
+ console.debug(...(Array.isArray(formattedMessage) ? formattedMessage : [formattedMessage]));
139
+ }
140
+ break;
141
+ default:
142
+ console.log(...(Array.isArray(formattedMessage) ? formattedMessage : [formattedMessage]));
143
+ }
144
+ }
145
+
146
+ public info(message: string, context?: Record<string, any>) {
147
+ this.log(LogLevel.INFO, message, context);
148
+ }
149
+
150
+ public warn(message: string, context?: Record<string, any>) {
151
+ this.log(LogLevel.WARN, message, context);
152
+ }
153
+
154
+ public error(message: string, context?: Record<string, any>) {
155
+ this.log(LogLevel.ERROR, message, context);
156
+ }
157
+
158
+ public debug(message: string, context?: Record<string, any>) {
159
+ this.log(LogLevel.DEBUG, message, context);
160
+ }
161
+ }
162
+
163
+ export const logger = Logger.getInstance();