diff --git a/src/config/redis.config.ts b/src/config/redis.config.ts index 4751458..c2175fe 100644 --- a/src/config/redis.config.ts +++ b/src/config/redis.config.ts @@ -13,7 +13,6 @@ class RedisConnectionManager { private setupClient() { if (this.isConnecting && this.client && this.client.status === 'connecting') { - console.log('ioredis client already connecting.'); return; } @@ -44,12 +43,10 @@ class RedisConnectionManager { }); this.client.on('error', (err: Error) => { - console.error('unable to connect to redis:', err.message); this.isConnecting = false; // Reset connection status on error }); this.client.on('connect', () => { - console.log('ioredis Client Connected'); this.isConnecting = false; // Connection successful if (this.reconnectTimeout) { clearTimeout(this.reconnectTimeout); @@ -58,12 +55,10 @@ class RedisConnectionManager { }); this.client.on('reconnecting', (delay: number) => { - console.log(`ioredis Client Reconnecting... next attempt in ${delay}ms`); this.isConnecting = true; // Indicate reconnecting }); this.client.on('end', () => { - console.log('ioredis Client Connection Closed'); this.isConnecting = false; // Connection ended }); diff --git a/src/controllers/file.controller.ts b/src/controllers/file.controller.ts index efe03ca..61cc2f9 100644 --- a/src/controllers/file.controller.ts +++ b/src/controllers/file.controller.ts @@ -315,10 +315,25 @@ const updateFileAccessLevel = async (c: Context) => { const value = c.get('validated'); const user = c.get('user') as IUserAttributes; const fileId = c.req.param('id'); - const updatedFile = await fileRepository.updateFileAccessLevel(fileId, user.id, value.access_level); - return res.SuccessResponse(c, 200, { message: "File access level updated successfully", data: updatedFile }); + + const updatedCount = await fileRepository.updateFileAccessLevel(fileId, user.id, value.access_level); + + return res.SuccessResponse(c, 200, { + message: "File access level updated successfully", + data: { + updatedCount, + message: updatedCount > 1 + ? `Updated ${updatedCount} items (folder and its contents)` + : 'Updated 1 item' + } + }); - } catch (error) { + } catch (error: any) { + if (error.message === 'File not found or you do not have permission to update it') { + return res.FailureResponse(c, 404, { + message: error.message + }); + } if (error instanceof ForeignKeyConstraintError && error.index === "files_parent_id_fkey") { return res.FailureResponse(c, 422, { message: "Invalid parent folder ID. The specified folder does not exist." @@ -329,6 +344,7 @@ const updateFileAccessLevel = async (c: Context) => { message: "A folder/file with this name already exists in the same location." }); } + console.error('Error updating file access level:', error); return res.FailureResponse(c, 500, { message: "Internal server error" }); } } diff --git a/src/controllers/notification.controller.ts b/src/controllers/notification.controller.ts index 13a349d..4c9a66c 100644 --- a/src/controllers/notification.controller.ts +++ b/src/controllers/notification.controller.ts @@ -13,16 +13,16 @@ const getUserNotifications = async (c: Context) => { const validatedQuery = c.get('validatedQuery') as { limit?: number - offset?: number + cursor?: string unreadOnly?: boolean } - const { limit = 20, offset = 0, unreadOnly = false } = validatedQuery + const { limit = 20, cursor, unreadOnly = false } = validatedQuery const result = await notificationRepository.getUserNotifications( user.id, limit, - offset, + cursor, unreadOnly ) @@ -30,8 +30,8 @@ const getUserNotifications = async (c: Context) => { message: "Notifications retrieved successfully", data: { notifications: result.notifications, - totalCount: result.totalCount, - hasMore: (offset + limit) < result.totalCount + nextCursor: result.nextCursor, + hasMore: result.hasMore }, }) } catch (error: any) { diff --git a/src/repository/file.repository.ts b/src/repository/file.repository.ts index 33fbb24..273a239 100644 --- a/src/repository/file.repository.ts +++ b/src/repository/file.repository.ts @@ -548,10 +548,52 @@ const getRecents = async (userId: string, page: number = 1, limit: number = 20) }; }; -// Update the file's access level, you can only update the access level of the file if you are the owner of the file. +/** + * Update the file's access level recursively + * When a folder's access level is changed, all its children (files and subfolders) + * will also have their access level updated to match the parent's new access level. + */ const updateFileAccessLevel = async (fileId: string, userId: string, accessLevel: AccessLevel) => { - const file = await db.File.update({ access_level: accessLevel }, { where: { id: fileId, owner_id: userId } }); - return file; + // First, verify the file exists and user is the owner + const targetFile = await db.File.findOne({ + where: { id: fileId, owner_id: userId }, + attributes: ['id', 'is_folder', 'access_level'] + }); + + if (!targetFile) { + throw new Error('File not found or you do not have permission to update it'); + } + + // Use a recursive query to update the target file and all its descendants + const query = ` + WITH RECURSIVE file_tree AS ( + -- Base case: the target file/folder + SELECT id, parent_id, is_folder + FROM files + WHERE id = :fileId AND owner_id = :userId AND deleted_at IS NULL + + UNION ALL + + -- Recursive case: all descendants + SELECT f.id, f.parent_id, f.is_folder + FROM files f + INNER JOIN file_tree ft ON f.parent_id = ft.id + WHERE f.owner_id = :userId AND f.deleted_at IS NULL + ) + UPDATE files + SET access_level = :accessLevel, updated_at = NOW() + FROM file_tree + WHERE files.id = file_tree.id + RETURNING files.id; + `; + + const result = await db.connection.query(query, { + type: QueryTypes.UPDATE, + replacements: { fileId, userId, accessLevel } + }); + + // Return the count of updated files + return result[1] || 0; }; export default { diff --git a/src/repository/notification.repository.ts b/src/repository/notification.repository.ts index ba4f665..37b7cec 100644 --- a/src/repository/notification.repository.ts +++ b/src/repository/notification.repository.ts @@ -1,13 +1,14 @@ import db from "@/config/database" import type { NotificationAttributes } from "@/models/Notification.model" +import { Op } from "sequelize" /** - * Get user notifications with pagination and filtering + * Get user notifications with cursor-based pagination and filtering */ async function getUserNotifications( userId: string, limit: number = 20, - offset: number = 0, + cursor?: string, unreadOnly: boolean = false ) { const whereClause: any = { @@ -18,11 +19,37 @@ async function getUserNotifications( whereClause.is_read = false } - const { count, rows } = await db.Notification.findAndCountAll({ + // If cursor is provided, parse it and add to where clause + if (cursor) { + try { + const decodedCursor = JSON.parse(Buffer.from(cursor, 'base64').toString('utf-8')) + const { created_at, id } = decodedCursor + + // For DESC ordering, we want records BEFORE the cursor + // Using composite comparison: (created_at, id) < (cursor_created_at, cursor_id) + whereClause[Op.or] = [ + { created_at: { [Op.lt]: created_at } }, + { + [Op.and]: [ + { created_at: created_at }, + { id: { [Op.lt]: id } } + ] + } + ] + } catch (error) { + // Invalid cursor, ignore it and start from beginning + console.error('Invalid cursor format:', error) + } + } + + // Fetch one extra record to determine if there are more pages + const rows = await db.Notification.findAll({ where: whereClause, - order: [['created_at', 'DESC']], - limit, - offset, + order: [ + ['created_at', 'DESC'], + ['id', 'DESC'] + ], + limit: limit + 1, attributes: [ 'id', 'user_id', @@ -37,9 +64,29 @@ async function getUserNotifications( ] }) + // Check if there are more records + const hasMore = rows.length > limit + + // Remove the extra record if it exists + const notifications = hasMore ? rows.slice(0, limit) : rows + + // Generate next cursor from the last record + let nextCursor: string | null = null + if (hasMore && notifications.length > 0) { + const lastRecord = notifications[notifications.length - 1] + if (lastRecord) { + const cursorData = { + created_at: lastRecord.created_at, + id: lastRecord.id + } + nextCursor = Buffer.from(JSON.stringify(cursorData)).toString('base64') + } + } + return { - notifications: rows, - totalCount: count + notifications, + nextCursor, + hasMore } } diff --git a/src/server.ts b/src/server.ts index ab2bcf0..29cdbfa 100644 --- a/src/server.ts +++ b/src/server.ts @@ -60,8 +60,6 @@ export class App { private registerErrorHandler() { this.app.onError((err, c) => { - console.log(err); - console.error("❗ Unhandled error:", err.message); return c.text("Internal Server Error", 500); }); } diff --git a/src/validation/notification.validation.ts b/src/validation/notification.validation.ts index e0c1cba..39b4199 100644 --- a/src/validation/notification.validation.ts +++ b/src/validation/notification.validation.ts @@ -10,13 +10,10 @@ const getNotificationsValidation = Joi.object({ 'number.min': 'Limit should be at least 1', 'number.max': 'Limit cannot exceed 100', }), - offset: Joi.number().integer().min(0) + cursor: Joi.string() .optional() - .default(0) .messages({ - 'number.base': 'Offset should be a number', - 'number.integer': 'Offset should be an integer', - 'number.min': 'Offset should be zero or greater', + 'string.base': 'Cursor should be a string', }), unreadOnly: Joi.boolean() .optional()