Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions src/config/redis.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ class RedisConnectionManager {

private setupClient() {
if (this.isConnecting && this.client && this.client.status === 'connecting') {
console.log('ioredis client already connecting.');
return;
}

Expand Down Expand Up @@ -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);
Expand All @@ -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
});

Expand Down
22 changes: 19 additions & 3 deletions src/controllers/file.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,10 +315,25 @@ const updateFileAccessLevel = async (c: Context) => {
const value = c.get<UpdateFileAccessLevelBody>('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."
Expand All @@ -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" });
}
}
Expand Down
10 changes: 5 additions & 5 deletions src/controllers/notification.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,25 @@ 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
)

return res.SuccessResponse(c, 200, {
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) {
Expand Down
48 changes: 45 additions & 3 deletions src/repository/file.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
63 changes: 55 additions & 8 deletions src/repository/notification.repository.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -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',
Expand All @@ -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
}
}

Expand Down
2 changes: 0 additions & 2 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
}
Expand Down
7 changes: 2 additions & 5 deletions src/validation/notification.validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down