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
1 change: 1 addition & 0 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ app.use(
'http://localhost:4173',
'https://www.dayframe.ru',
'https://dayframe.ru',
'https://dayframe.na4u.ru',
],
methods: 'GET,POST,PATCH,DELETE',
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'Accept', 'Origin'],
Expand Down
19 changes: 18 additions & 1 deletion src/entities/bot/bot.controller.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
const botService = require("./bot.service");
const botService = require('./bot.service');

class BotController {
/**
* Get tasks for a specific date by chat_id
* Route: GET /bot/tasks/:chat_id?date=YYYY-MM-DD
* If date is not provided, returns tasks for today
*/
getTasksForDateByChatId(req, res) {
const chat_id = req.params.chat_id;
const date = req.query.date || null; // Optional date parameter

botService.getTasksForDateByChatId(chat_id, date).then((result) => {
res.status(result.status).json(result.data);
});
}

/**
* @deprecated Use getTasksForDateByChatId instead. This method is kept for backward compatibility.
*/
getTasksForTodayByChatId(req, res) {
const chat_id = req.params.chat_id;
botService.getTasksForTodayByChatId(chat_id).then((result) => {
Expand Down
8 changes: 7 additions & 1 deletion src/entities/bot/bot.routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ const express = require('express');
const router = express.Router();
const botController = require('./bot.controller');

// Получить задачи на сегодня по chat_id
// Получить задачи на конкретную дату по chat_id
// Route: GET /bot/tasks/:chat_id?date=YYYY-MM-DD
// If date is not provided, returns tasks for today
//TODO:: authenticate придумать для бота
router.get('/tasks/:chat_id', (req, res) => botController.getTasksForDateByChatId(req, res));

// Deprecated: старый роут для обратной совместимости
//TODO:: authenticate придумать для бота
router.get('/today/:chat_id', (req, res) => botController.getTasksForTodayByChatId(req, res));

Expand Down
54 changes: 50 additions & 4 deletions src/entities/bot/bot.service.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,68 @@
const userService = require('../user/user.service');
const taskService = require('../task/task.service');
const templateModel = require('../template.task/models/template.task.model');
const { getDateStringInTZ } = require('../../utils/date');

/**
* Bot service class with business logic for bot operations
*/
class BotService {
async getTasksForTodayByChatId(chat_id) {
/**
* Get tasks for a specific date by chat_id (optimized with subtasks)
* Returns format compatible with old API: { tasks, templates, dateString, dayOfWeek }
* @param {string} chat_id - Telegram chat ID
* @param {string} date - Optional date in YYYY-MM-DD format. If not provided, uses today in user's timezone
* @returns {Promise<{status: number, data: any}>}
*/
async getTasksForDateByChatId(chat_id, date = null) {
try {
const user = await userService.getUserByChatId(chat_id);
if (!user) {
return { status: 404, data: { error: 'Пользователь не найден' } };
}
const result = await taskService.getTasksForToday(user.id, user.timezone);
return { status: result.status, data: result.data };

const timezone = user.timezone || 'Europe/Moscow';

// If date is not provided, use today in user's timezone
const taskDate = date || getDateStringInTZ(timezone);
const dateString = taskDate;

// Get tasks with subtasks (optimized - 2 queries)
const tasksResult = await taskService.getTasksForDateWithSubTasks(user.id, taskDate);
if (tasksResult.status !== 200) {
return tasksResult;
}

// Calculate day of week for templates
// Parse the date to get day of week
const dateObj = new Date(taskDate + 'T12:00:00'); // Use noon to avoid timezone issues
const jsDay = dateObj.getDay();
const dayOfWeek = jsDay === 0 ? 7 : jsDay; // Convert Sunday (0) to 7

// Get templates for this day of week
const [templates] = await templateModel.getTemplatesForDay(user.id, dayOfWeek);

return {
status: 200,
data: {
tasks: tasksResult.data,
templates: templates || [],
dateString,
dayOfWeek,
},
};
} catch (err) {
console.error("❌ Ошибка при получении задач на сегодня:", err);
console.error('❌ Ошибка при получении задач на дату:', err);
return { status: 500, data: { error: err.message } };
}
}

/**
* @deprecated Use getTasksForDateByChatId instead. This method is kept for backward compatibility.
*/
async getTasksForTodayByChatId(chat_id) {
return this.getTasksForDateByChatId(chat_id);
}
}

// Create a singleton instance
Expand Down
37 changes: 15 additions & 22 deletions src/entities/task/models/subtask.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,22 @@ function getSubtasksAllByUserId(user_id) {
return db.query(query, [user_id]);
}

function getAllSubtasksByUserId(user_id) {
return db
.query('SELECT * FROM subtasks WHERE user_id = ?', [user_id])
.then(([subtasks]) => [subtasks])
.catch((error) => {
console.error('Ошибка при получении подзадач:', error);
throw error;
});
}

function getAllSubtasksByParentTaskId(parent_task_id) {
return db.query('SELECT * FROM subtasks WHERE parent_task_id = ?', [
parent_task_id,
]);
return db.query('SELECT * FROM subtasks WHERE parent_task_id = ?', [parent_task_id]);
}

function addSubtask(user_id, subtask) {
const {
parent_task_id,
title,
is_done = false,
position = 0,
special_id,
} = subtask;
const { parent_task_id, title, is_done = false, position = 0, special_id } = subtask;

if (!special_id) {
throw new Error('special_id is required for subtasks');
Expand All @@ -28,14 +30,7 @@ function addSubtask(user_id, subtask) {
INSERT INTO subtasks (parent_task_id, title, is_done, position, special_id, user_id, created_at)
VALUES (?, ?, ?, ?, ?, ?, NOW())
`;
return db.query(query, [
parent_task_id,
title,
is_done,
position,
special_id,
user_id,
]);
return db.query(query, [parent_task_id, title, is_done, position, special_id, user_id]);
}

function getSubtaskById(id) {
Expand All @@ -47,10 +42,7 @@ function deleteSubtaskById(id) {
}

function updateSubtaskStatus(id, is_done) {
return db.query('UPDATE subtasks SET is_done = ? WHERE id = ?', [
is_done,
id,
]);
return db.query('UPDATE subtasks SET is_done = ? WHERE id = ?', [is_done, id]);
}

function updateSubtasksStatusByParentTaskId(parent_task_id, is_done) {
Expand Down Expand Up @@ -88,6 +80,7 @@ function countCompletedSubtaskByParentTaskId(parent_task_id) {

module.exports = {
getSubtasksAllByUserId,
getAllSubtasksByUserId,
getAllSubtasksByParentTaskId,
addSubtask,
getSubtaskById,
Expand Down
33 changes: 33 additions & 0 deletions src/entities/task/task.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const taskService = require('./task.service');
class TaskController {
/**
* Get all tasks with their subtasks for a user
* @deprecated Use getAllTasksOptimized instead. This method uses inefficient N+1 queries.
*/
getTasksWithSubTasks(req, res) {
const userId = Number(req.user.id);
Expand All @@ -18,6 +19,38 @@ class TaskController {
});
}

/**
* Optimized version: Get all tasks with subtasks using optimized query
* Includes timeout handling to prevent 502 errors
*/
getAllTasksOptimized(req, res) {
const userId = Number(req.user.id);
if (!userId) {
return res.status(400).json({ error: 'Не передан user-id в заголовке' });
}

// Add timeout of 25 seconds (less than 30 seconds at Express level)
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Request timeout')), 25000)
);

Promise.race([taskService.getAllTasksWithSubTasksOptimized(userId), timeoutPromise])
.then((result) => {
if (res.headersSent) return; // Check if response already sent
res.status(result.status).json(result.data);
})
.catch((err) => {
if (res.headersSent) return; // Check if response already sent
console.error('❌ Ошибка при получении задач:', err);
res.status(500).json({
error:
err.message === 'Request timeout'
? 'Превышено время ожидания ответа от сервера'
: 'Ошибка при получении задач',
});
});
}

/**
* Create a new task
*/
Expand Down
4 changes: 2 additions & 2 deletions src/entities/task/task.routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ const authorizeTask = require('../../middleware/authorizeTask');
const authorizeSubTask = require('../../middleware/authorizeSubTask');
const authenticate = require('../../middleware/authenticate');

// Получить все задачи по id пользователя
router.get('/', authenticate, (req, res) => taskController.getTasksWithSubTasks(req, res));
// Получить все задачи по id пользователя (оптимизированная версия)
router.get('/', authenticate, (req, res) => taskController.getAllTasksOptimized(req, res));

// Получить задачи за период
router.get('/period', authenticate, (req, res) => taskController.getTasksForPeriod(req, res));
Expand Down
89 changes: 89 additions & 0 deletions src/entities/task/task.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class TaskService {

/**
* Get all tasks with subtasks for a user
* @deprecated Use getAllTasksWithSubTasksOptimized instead. This method uses inefficient N+1 queries (1 + 2N queries).
*/
async getTasksWithSubTasks(userId) {
try {
Expand Down Expand Up @@ -65,6 +66,47 @@ class TaskService {
}
}

/**
* Optimized version: Get all tasks with subtasks using only 2 SQL queries
* Instead of 1 + 2N queries (where N is number of tasks)
*/
async getAllTasksWithSubTasksOptimized(userId) {
try {
// Get all tasks and all subtasks in parallel (2 queries total)
const [[tasks], [subtasks]] = await Promise.all([
taskModel.getAllTasksByUser(userId),
subtaskModel.getAllSubtasksByUserId(userId),
]);

if (!tasks.length) {
return { status: 404, data: { error: 'Задачи не найдены' } };
}

// Group subtasks by parent_task_id for O(1) lookup
const subtasksByTaskId = new Map();
if (subtasks && subtasks.length > 0) {
for (const subtask of subtasks) {
const taskId = subtask.parent_task_id;
if (!subtasksByTaskId.has(taskId)) {
subtasksByTaskId.set(taskId, []);
}
subtasksByTaskId.get(taskId).push(subtask);
}
}

// Combine tasks with their subtasks
const fullTasks = tasks.map((task) => ({
...task,
subtasks: subtasksByTaskId.get(task.id) || [],
}));

return { status: 200, data: fullTasks };
} catch (err) {
console.error('❌ Ошибка при получении задач с подзадачами (оптимизированная версия):', err);
return { status: 500, data: { error: err.message } };
}
}

/**
* Create a new task
*/
Expand Down Expand Up @@ -380,6 +422,53 @@ class TaskService {
return { status: 500, data: { error: err.message } };
}
}

/**
* Optimized version: Get tasks for a specific date with subtasks using only 2 SQL queries
* @param {number} userId - User ID
* @param {string} taskDate - Date in YYYY-MM-DD format
* @returns {Promise<{status: number, data: any}>}
*/
async getTasksForDateWithSubTasks(userId, taskDate) {
try {
// Get all tasks for the date and all subtasks for the user in parallel (2 queries total)
const [[tasks], [subtasks]] = await Promise.all([
taskModel.getTasksForDate(userId, taskDate),
subtaskModel.getAllSubtasksByUserId(userId),
]);

if (!tasks.length) {
return { status: 404, data: { error: 'Задачи не найдены' } };
}

// Filter subtasks to only include those for tasks on this date
const taskIds = new Set(tasks.map((task) => task.id));
const relevantSubtasks = (subtasks || []).filter((subtask) =>
taskIds.has(subtask.parent_task_id)
);

// Group subtasks by parent_task_id for O(1) lookup
const subtasksByTaskId = new Map();
for (const subtask of relevantSubtasks) {
const taskId = subtask.parent_task_id;
if (!subtasksByTaskId.has(taskId)) {
subtasksByTaskId.set(taskId, []);
}
subtasksByTaskId.get(taskId).push(subtask);
}

// Combine tasks with their subtasks
const fullTasks = tasks.map((task) => ({
...task,
subtasks: subtasksByTaskId.get(task.id) || [],
}));

return { status: 200, data: fullTasks };
} catch (err) {
console.error('❌ Ошибка при получении задач на дату с подзадачами:', err);
return { status: 500, data: { error: err.message } };
}
}
}

// Create a singleton instance
Expand Down
Loading