index.ts
import express, { Express } from "express";
import bodyParser from 'body-parser';
import { listItems, addItem, completeItem } from './routes';
// Configure and start the HTTP server.
const port: number = 8088;
const app: Express = express();
app.use(bodyParser.json());
app.get("/api/list", listItems);
app.post("/api/add", addItem);
app.post("/api/complete", completeItem);
app.listen(port, () => console.log(`Server listening on ${port}`));
routes.ts
import { Request, Response } from "express";
import { ParamsDictionary } from "express-serve-static-core";
// Require type checking of request body.
type SafeRequest = Request<ParamsDictionary, {}, Record<string, unknown>>;
type SafeResponse = Response; // only writing, so no need to check
// Description of an individual item in the list.
type Item = {
name: string,
completedAt: number // < 0 if not completed
};
// Map from item name to item. (Note that item names must be unique.)
const items: Map<string, Item> = new Map();
/**
* Returns a list of all items that are uncompleted or were completed less than
* 5 seconds ago.
* @param _req the request
* @param res the response
*/
export const listItems = (_req: SafeRequest, res: SafeResponse): void => {
const now = Date.now();
const remaining: {name: string, completed: boolean}[] = [];
for (const item of items.values()) {
if (item.completedAt < 0) {
remaining.push({name: item.name, completed: false});
} else if (now - item.completedAt <= 5000) {
remaining.push({name: item.name, completed: true});
} else {
// skip this because it was completed too long ago
}
}
res.send({items: remaining});
}
/**
* Add the item to the list.
* @param req the request
* @param res the response
*/
export const addItem = (req: SafeRequest, res: SafeResponse):void => {
const name = req.body.name;
if (typeof name !== 'string') {
res.status(400).send(`name is not a string: ${name}`);
return;
}
// If the item does not already exist, then add it. Notify the client of
// whether we had to add the item.
const item = items.get(name);
if (item === undefined) {
items.set(name, {name, completedAt: -1});
res.send({name, added: true});
} else if (item.completedAt >= 0) {
item.completedAt = -1; // no longer removed
res.send({name, added: true});
} else {
res.send({name, added: false});
}
}
/**
* Marks the given item as completed.
* @param req the request
* @param res the response
*/
export const completeItem = (req: SafeRequest, res: SafeResponse): void => {
const name = req.body.name;
if (typeof name !== 'string') {
res.status(400).send("name is not a string: ${name}");
return;
}
const item = items.get(name);
if (item === undefined) {
res.status(400).send(`no item called "${name}"`);
return;
} else if (item.completedAt >= 0) {
res.status(400).send(`item called "${name}" is already completed`);
return;
}
item.completedAt = Date.now();
res.send({completed: true});
}
routes_test.ts
import * as assert from 'assert';
import * as httpMocks from 'node-mocks-http';
import { listItems, addItem, completeItem } from './routes';
describe('routes', function() {
it('end-to-end', function() {
const req1 = httpMocks.createRequest({method: 'GET', url: '/api/list'});
const res1 = httpMocks.createResponse();
listItems(req1, res1);
assert.strictEqual(res1._getStatusCode(), 200);
assert.deepStrictEqual(res1._getData(), {items: []});
const req2 = httpMocks.createRequest({method: 'POST', url: '/api/add',
body: {name: "laundry"}});
const res2 = httpMocks.createResponse();
addItem(req2, res2);
assert.strictEqual(res2._getStatusCode(), 200);
assert.deepStrictEqual(res2._getData(), {added: true, name: "laundry"});
// Try to create it again.
const req3 = httpMocks.createRequest({method: 'POST', url: '/api/add',
body: {name: "laundry"}});
const res3 = httpMocks.createResponse();
addItem(req3, res3);
assert.strictEqual(res3._getStatusCode(), 200);
assert.deepStrictEqual(res3._getData(), {added: false, name: "laundry"});
const req4 = httpMocks.createRequest({method: 'POST', url: '/api/add',
body: {name: "wash dog"}});
const res4 = httpMocks.createResponse();
addItem(req4, res4);
assert.strictEqual(res4._getStatusCode(), 200);
assert.deepStrictEqual(res4._getData(), {added: true, name: "wash dog"});
const req5 = httpMocks.createRequest({method: 'GET', url: '/api/list'});
const res5 = httpMocks.createResponse();
listItems(req5, res5);
assert.strictEqual(res5._getStatusCode(), 200);
assert.deepStrictEqual(res5._getData(), {items: [
{name: "laundry", completed: false},
{name: "wash dog", completed: false},
]});
const req6 = httpMocks.createRequest({method: 'POST', url: '/api/complete',
body: {name: "wash dog"}});
const res6 = httpMocks.createResponse();
completeItem(req6, res6);
assert.strictEqual(res6._getStatusCode(), 200);
assert.deepStrictEqual(res6._getData(), {completed: true});
const req7 = httpMocks.createRequest({method: 'GET', url: '/api/list'});
const res7 = httpMocks.createResponse();
listItems(req7, res7);
assert.strictEqual(res7._getStatusCode(), 200);
assert.deepStrictEqual(res7._getData(), {items: [
{name: "laundry", completed: false},
{name: "wash dog", completed: true},
]});
});
});
Full Code