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 };
  });
}