diff --git a/src/index.js b/src/index.js index 60221e6..3eaece8 100644 --- a/src/index.js +++ b/src/index.js @@ -39,6 +39,56 @@ const server = http.createServer((req, res) => { return; } + // Subtask routes: /tasks/:id/subtasks + const subtaskMatch = url.pathname.match(/^\/tasks\/(\d+)\/subtasks$/); + if (subtaskMatch) { + const parentId = parseInt(subtaskMatch[1], 10); + + if (method === 'GET') { + const subtasks = store.listSubtasks(parentId); + if (subtasks === null) { + res.writeHead(404); + res.end(JSON.stringify({ error: 'parent task not found' })); + return; + } + res.writeHead(200); + res.end(JSON.stringify(subtasks)); + return; + } + + if (method === 'POST') { + let body = ''; + req.on('data', chunk => { body += chunk; }); + req.on('end', () => { + try { + const { title, description } = JSON.parse(body); + if (!title) { + res.writeHead(400); + res.end(JSON.stringify({ error: 'title is required' })); + return; + } + const result = store.createSubtask(parentId, title, description); + if (result.error === 'parent not found') { + res.writeHead(404); + res.end(JSON.stringify({ error: 'parent task not found' })); + return; + } + if (result.error === 'subtasks of subtasks are not allowed') { + res.writeHead(400); + res.end(JSON.stringify({ error: 'subtasks of subtasks are not allowed' })); + return; + } + res.writeHead(201); + res.end(JSON.stringify(result)); + } catch (e) { + res.writeHead(400); + res.end(JSON.stringify({ error: 'invalid JSON' })); + } + }); + return; + } + } + const taskMatch = url.pathname.match(/^\/tasks\/(\d+)$/); if (taskMatch) { const id = parseInt(taskMatch[1], 10); diff --git a/src/store.js b/src/store.js index fe8b2b0..b6252de 100644 --- a/src/store.js +++ b/src/store.js @@ -10,6 +10,24 @@ class TaskStore { title, description, completed: false, + parentId: null, + createdAt: new Date().toISOString(), + }; + this.tasks.set(task.id, task); + return task; + } + + createSubtask(parentId, title, description = '') { + const parent = this.tasks.get(parentId); + if (!parent) return { error: 'parent not found' }; + if (parent.parentId !== null) return { error: 'subtasks of subtasks are not allowed' }; + + const task = { + id: this.nextId++, + title, + description, + completed: false, + parentId, createdAt: new Date().toISOString(), }; this.tasks.set(task.id, task); @@ -24,6 +42,12 @@ class TaskStore { return Array.from(this.tasks.values()); } + listSubtasks(parentId) { + const parent = this.tasks.get(parentId); + if (!parent) return null; + return Array.from(this.tasks.values()).filter(t => t.parentId === parentId); + } + update(id, updates) { const task = this.tasks.get(id); if (!task) return null; @@ -36,6 +60,15 @@ class TaskStore { } delete(id) { + if (!this.tasks.has(id)) return false; + + // Cascade delete: remove all subtasks of this task + for (const [taskId, task] of this.tasks) { + if (task.parentId === id) { + this.tasks.delete(taskId); + } + } + return this.tasks.delete(id); } } diff --git a/src/store.test.js b/src/store.test.js index d3c335c..5bc8443 100644 --- a/src/store.test.js +++ b/src/store.test.js @@ -46,4 +46,93 @@ describe('TaskStore', () => { assert.strictEqual(store.delete(1), true); assert.strictEqual(store.get(1), null); }); + + it('creates a task with parentId null by default', () => { + const store = new TaskStore(); + const task = store.create('Top-level task'); + assert.strictEqual(task.parentId, null); + }); +}); + +describe('TaskStore - Subtasks', () => { + it('creates a subtask under a valid parent', () => { + const store = new TaskStore(); + const parent = store.create('Parent task'); + const subtask = store.createSubtask(parent.id, 'Child task', 'Details'); + assert.strictEqual(subtask.parentId, parent.id); + assert.strictEqual(subtask.title, 'Child task'); + assert.strictEqual(subtask.description, 'Details'); + assert.strictEqual(subtask.completed, false); + assert.ok(subtask.id > parent.id); + }); + + it('returns error when creating subtask under non-existent parent', () => { + const store = new TaskStore(); + const result = store.createSubtask(999, 'Orphan task'); + assert.deepStrictEqual(result, { error: 'parent not found' }); + }); + + it('rejects creating a subtask of a subtask (single-level nesting)', () => { + const store = new TaskStore(); + const parent = store.create('Parent'); + const subtask = store.createSubtask(parent.id, 'Child'); + const result = store.createSubtask(subtask.id, 'Grandchild'); + assert.deepStrictEqual(result, { error: 'subtasks of subtasks are not allowed' }); + }); + + it('lists subtasks of a parent', () => { + const store = new TaskStore(); + const parent = store.create('Parent'); + store.createSubtask(parent.id, 'Child 1'); + store.createSubtask(parent.id, 'Child 2'); + const subtasks = store.listSubtasks(parent.id); + assert.strictEqual(subtasks.length, 2); + assert.strictEqual(subtasks[0].title, 'Child 1'); + assert.strictEqual(subtasks[1].title, 'Child 2'); + }); + + it('returns null when listing subtasks of non-existent parent', () => { + const store = new TaskStore(); + assert.strictEqual(store.listSubtasks(999), null); + }); + + it('returns empty array when parent has no subtasks', () => { + const store = new TaskStore(); + const parent = store.create('Parent'); + const subtasks = store.listSubtasks(parent.id); + assert.deepStrictEqual(subtasks, []); + }); + + it('cascade deletes subtasks when parent is deleted', () => { + const store = new TaskStore(); + const parent = store.create('Parent'); + const child1 = store.createSubtask(parent.id, 'Child 1'); + const child2 = store.createSubtask(parent.id, 'Child 2'); + store.delete(parent.id); + assert.strictEqual(store.get(parent.id), null); + assert.strictEqual(store.get(child1.id), null); + assert.strictEqual(store.get(child2.id), null); + }); + + it('GET /tasks returns all tasks including subtasks (backward compat)', () => { + const store = new TaskStore(); + store.create('Parent'); + store.createSubtask(1, 'Child'); + const all = store.list(); + assert.strictEqual(all.length, 2); + }); + + it('only returns subtasks belonging to the specified parent', () => { + const store = new TaskStore(); + const parent1 = store.create('Parent 1'); + const parent2 = store.create('Parent 2'); + store.createSubtask(parent1.id, 'Child of P1'); + store.createSubtask(parent2.id, 'Child of P2'); + const subtasks1 = store.listSubtasks(parent1.id); + assert.strictEqual(subtasks1.length, 1); + assert.strictEqual(subtasks1[0].title, 'Child of P1'); + const subtasks2 = store.listSubtasks(parent2.id); + assert.strictEqual(subtasks2.length, 1); + assert.strictEqual(subtasks2[0].title, 'Child of P2'); + }); });