Skip to content
Closed
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
50 changes: 50 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
33 changes: 33 additions & 0 deletions src/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
Expand All @@ -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);
}
}
Expand Down
89 changes: 89 additions & 0 deletions src/store.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
Loading