diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fbe74e..82f4d48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ what it changes for someone who uses the API. CI checks that it did. ## Unreleased +- `DELETE /tasks/done` removes every task that is done, and says how many it removed. (#6) - `PATCH /tasks/:id` renames a task, body `{"title": "..."}`, and answers with the task. (#5) (#7) - `GET /tasks/count` says how many tasks there are, and how many are done. - CI checks that a pull request adds a line here that ends with its number. (#4) diff --git a/README.md b/README.md index 8345f85..5cf67e9 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ and in an adopter's first run. It is small on purpose, so a job is cheap and fas | GET | `/tasks` | Lists every task | | POST | `/tasks` | Adds a task, body `{"title": "..."}` | | GET | `/tasks/count` | Says how many tasks there are, and how many are done | +| DELETE | `/tasks/done` | Removes every task that is done, and says how many | | GET | `/tasks/:id` | Returns one task | | PATCH | `/tasks/:id` | Changes a task's title, body `{"title": "..."}` | | POST | `/tasks/:id/complete` | Marks a task done | diff --git a/src/server.ts b/src/server.ts index a4a30f2..2a46507 100644 --- a/src/server.ts +++ b/src/server.ts @@ -8,6 +8,7 @@ import { TaskList, ValidationError } from './tasks.ts'; * GET /tasks every task * POST /tasks add a task, body {"title": "..."} * GET /tasks/count how many tasks, and how many are done + * DELETE /tasks/done remove every task that is done, and say how many * GET /tasks/:id one task * PATCH /tasks/:id change a task's title, body {"title": "..."} * POST /tasks/:id/complete mark a task done @@ -38,6 +39,7 @@ async function handle(tasks: TaskList, request: IncomingMessage, response: Serve } if (parts.length === 2 && parts[1] === 'count' && method === 'GET') return send(response, 200, tasks.count()); + if (parts.length === 2 && parts[1] === 'done' && method === 'DELETE') return send(response, 200, { removed: tasks.removeDone() }); const id = Number(parts[1]); if (!Number.isInteger(id) || id < 1) return send(response, 404, { error: 'not found' }); diff --git a/src/tasks.ts b/src/tasks.ts index 32d5f4f..08b1fff 100644 --- a/src/tasks.ts +++ b/src/tasks.ts @@ -49,6 +49,15 @@ export class TaskList { return this.tasks.delete(id); } + /** Remove every task that is done. Returns how many it removed. */ + removeDone(): number { + let removed = 0; + for (const task of this.tasks.values()) { + if (task.done && this.tasks.delete(task.id)) removed++; + } + return removed; + } + /** How many tasks there are, and how many of them are done. */ count(): { total: number; done: number } { let done = 0; diff --git a/test/server.test.ts b/test/server.test.ts index 0a143c8..0e2a2fe 100644 --- a/test/server.test.ts +++ b/test/server.test.ts @@ -57,6 +57,32 @@ describe('the HTTP API', () => { assert.deepEqual(await call('GET', '/tasks/count'), { status: 200, json: { total, done } }); }); + it('removes every task that is done, and keeps the rest', async () => { + await call('DELETE', '/tasks/done'); + const first = (await call('POST', '/tasks', { title: 'Done one' })).json!.id as number; + const open = (await call('POST', '/tasks', { title: 'Still open' })).json!.id as number; + const third = (await call('POST', '/tasks', { title: 'Done two' })).json!.id as number; + await call('POST', `/tasks/${first}/complete`); + await call('POST', `/tasks/${third}/complete`); + + assert.deepEqual(await call('DELETE', '/tasks/done'), { status: 200, json: { removed: 2 } }); + assert.equal((await call('GET', `/tasks/${first}`)).status, 404); + assert.equal((await call('GET', `/tasks/${third}`)).status, 404); + assert.deepEqual(await call('GET', `/tasks/${open}`), { status: 200, json: { id: open, title: 'Still open', done: false } }); + + await call('DELETE', `/tasks/${open}`); + }); + + it('answers removed 0 when no task is done', async () => { + await call('DELETE', '/tasks/done'); + const id = (await call('POST', '/tasks', { title: 'Not done yet' })).json!.id as number; + + assert.deepEqual(await call('DELETE', '/tasks/done'), { status: 200, json: { removed: 0 } }); + assert.deepEqual((await call('GET', `/tasks/${id}`)).json, { id, title: 'Not done yet', done: false }); + + await call('DELETE', `/tasks/${id}`); + }); + it('refuses a task without a title, and a body that is not JSON', async () => { assert.deepEqual(await call('POST', '/tasks', { title: '' }), { status: 400, json: { error: 'title must be text' } }); assert.deepEqual(await call('POST', '/tasks', 'not json'), { status: 400, json: { error: 'the body must be JSON' } }); diff --git a/test/tasks.test.ts b/test/tasks.test.ts index e36c1c7..1650aa1 100644 --- a/test/tasks.test.ts +++ b/test/tasks.test.ts @@ -63,6 +63,33 @@ describe('the task list', () => { assert.equal(tasks.get(id), undefined); }); + it('removes the tasks that are done, says how many, and keeps the rest', () => { + const tasks = new TaskList(); + const first = tasks.add('Write the spec'); + const second = tasks.add('Review it'); + const third = tasks.add('Ship it'); + tasks.complete(first.id); + tasks.complete(third.id); + + assert.equal(tasks.removeDone(), 2); + assert.deepEqual(tasks.all(), [{ id: second.id, title: 'Review it', done: false }]); + assert.deepEqual(tasks.count(), { total: 1, done: 0 }); + assert.equal(tasks.add('Celebrate').id, third.id + 1); + }); + + it('removes nothing when no task is done', () => { + const tasks = new TaskList(); + assert.equal(tasks.removeDone(), 0); + + const { id } = tasks.add('Write the spec'); + assert.equal(tasks.removeDone(), 0); + assert.deepEqual(tasks.all(), [{ id, title: 'Write the spec', done: false }]); + + tasks.complete(id); + assert.equal(tasks.removeDone(), 1); + assert.equal(tasks.removeDone(), 0); + }); + it('counts the tasks, and the ones that are done', () => { const tasks = new TaskList(); assert.deepEqual(tasks.count(), { total: 0, done: 0 });