Spaces:
Paused
Paused
File size: 4,021 Bytes
c562e21 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | import crypto from 'node:crypto';
import { db } from '../core/db.js';
import { bus } from '../core/bus.js';
import { requireMember } from '../core/guards.js';
function sendError(reply, status, code, message) {
return reply.code(status).send({ error: { code, message } });
}
function hasField(obj, key) {
return Object.prototype.hasOwnProperty.call(obj, key);
}
function isMember(spaceId, userId) {
if (userId === null || userId === undefined) return true;
return Boolean(
db
.prepare('SELECT id FROM space_members WHERE space_id = ? AND user_id = ?')
.get(spaceId, userId)
);
}
function validateNullableTimestamp(value, name) {
if (value === null || value === undefined) return null;
if (!Number.isInteger(value)) return `${name} должен быть unix ms или null`;
return null;
}
export default async function tasksRoutes(fastify) {
fastify.get('/spaces/:id/tasks', async (request) => {
await requireMember(request, request.params.id);
const tasks = db
.prepare('SELECT * FROM tasks WHERE space_id = ? ORDER BY created_at DESC')
.all(request.params.id);
return { tasks };
});
fastify.post('/spaces/:id/tasks', async (request, reply) => {
await requireMember(request, request.params.id);
const spaceId = request.params.id;
const { title, assignee_id = null, due_at = null } = request.body || {};
if (!title || typeof title !== 'string') {
return sendError(reply, 400, 'VALIDATION', 'title обязателен');
}
if (!isMember(spaceId, assignee_id)) {
return sendError(reply, 400, 'VALIDATION', 'assignee_id должен быть участником пространства или null');
}
const dueError = validateNullableTimestamp(due_at, 'due_at');
if (dueError) {
return sendError(reply, 400, 'VALIDATION', dueError);
}
const task = {
id: crypto.randomUUID(),
space_id: spaceId,
title,
assignee_id,
done: 0,
due_at,
created_at: Date.now(),
};
db.prepare(
`INSERT INTO tasks (id, space_id, title, assignee_id, done, due_at, created_at)
VALUES (?,?,?,?,?,?,?)`
).run(task.id, task.space_id, task.title, task.assignee_id, task.done, task.due_at, task.created_at);
bus.emit(spaceId, { type: 'task.created', space_id: spaceId, payload: task });
return reply.code(201).send({ task });
});
fastify.patch('/spaces/:id/tasks/:taskId', async (request, reply) => {
await requireMember(request, request.params.id);
const spaceId = request.params.id;
const taskId = request.params.taskId;
const body = request.body || {};
const existing = db.prepare('SELECT * FROM tasks WHERE id = ? AND space_id = ?').get(taskId, spaceId);
if (!existing) {
return sendError(reply, 404, 'NOT_FOUND', 'Задача не найдена');
}
const task = { ...existing };
if (hasField(body, 'title')) {
if (!body.title || typeof body.title !== 'string') {
return sendError(reply, 400, 'VALIDATION', 'title должен быть непустой строкой');
}
task.title = body.title;
}
if (hasField(body, 'assignee_id')) {
if (!isMember(spaceId, body.assignee_id)) {
return sendError(reply, 400, 'VALIDATION', 'assignee_id должен быть участником пространства или null');
}
task.assignee_id = body.assignee_id;
}
if (hasField(body, 'done')) {
if (typeof body.done !== 'boolean' && body.done !== 0 && body.done !== 1) {
return sendError(reply, 400, 'VALIDATION', 'done должен быть boolean или 0|1');
}
task.done = body.done ? 1 : 0;
}
db.prepare('UPDATE tasks SET title = ?, assignee_id = ?, done = ? WHERE id = ? AND space_id = ?')
.run(task.title, task.assignee_id, task.done, task.id, task.space_id);
bus.emit(spaceId, { type: 'task.updated', space_id: spaceId, payload: task });
return { task };
});
}
|