+1
server/books.json
+1
server/books.json
···
···
1
+
[{"id":"9780553212471","title":"Frankenstein","author":"Mary Shelley"},{"id":"9780060935467","title":"To Kill a Mockingbird","author":"Harper Lee"},{"id":"9780141439518","title":"Pride and Prejudice","author":"Jane Austen"}]
+288
server/books.ts
+288
server/books.ts
···
···
1
+
import express, {
2
+
type NextFunction,
3
+
type Request,
4
+
type Response,
5
+
} from "express";
6
+
import { writeFile, readFile, exists } from "fs/promises";
7
+
8
+
const ISBN13 =
9
+
/^(?:ISBN(?:-13)?:? )?(?=[0-9]{13}$|(?=(?:[0-9]+[- ]){4})[- 0-9]{17}$)[\d-]+$/;
10
+
11
+
interface Book {
12
+
id: string;
13
+
title: string;
14
+
author: string;
15
+
}
16
+
17
+
const initBooks: () => Promise<void> = async () => {
18
+
await writeFile(
19
+
"books.json",
20
+
JSON.stringify([
21
+
{
22
+
id: "9780553212471",
23
+
title: "Frankenstein",
24
+
author: "Mary Shelley",
25
+
},
26
+
{
27
+
id: "9780060935467",
28
+
title: "To Kill a Mockingbird",
29
+
author: "Harper Lee",
30
+
},
31
+
{
32
+
id: "9780141439518",
33
+
title: "Pride and Prejudice",
34
+
author: "Jane Austen",
35
+
},
36
+
]),
37
+
);
38
+
};
39
+
40
+
enum ErrorType {
41
+
NotFound,
42
+
InvalidId,
43
+
BadData,
44
+
AlreadyExists,
45
+
}
46
+
47
+
class BookError extends Error {
48
+
public readonly status: number;
49
+
constructor(err: ErrorType) {
50
+
let msg: string;
51
+
let st: number;
52
+
switch (err) {
53
+
case ErrorType.NotFound:
54
+
msg = "Book {{id}} not found";
55
+
st = 404;
56
+
break;
57
+
case ErrorType.InvalidId:
58
+
msg = "Invalid book id ({{id}}) [must be ISBN-13 formatted]";
59
+
st = 400;
60
+
break;
61
+
case ErrorType.BadData:
62
+
msg = "Invalid book data";
63
+
st = 400;
64
+
break;
65
+
case ErrorType.AlreadyExists:
66
+
msg = "Book with id {{id}} already exists";
67
+
st = 409;
68
+
break;
69
+
}
70
+
super(msg);
71
+
this.name = "BookError";
72
+
this.status = st;
73
+
}
74
+
}
75
+
76
+
const getBooks: () => Promise<Book[]> = async () => {
77
+
if (!(await exists("books.json"))) {
78
+
await initBooks();
79
+
}
80
+
const file = await readFile("books.json", "utf-8");
81
+
if (file.length < 4) {
82
+
await initBooks();
83
+
return await getBooks();
84
+
}
85
+
return JSON.parse(file);
86
+
};
87
+
88
+
const updateBook = async (task: Book): Promise<void> => {
89
+
const books = await getBooks();
90
+
const index = books.findIndex((b) => b.id === task.id);
91
+
if (index !== -1) {
92
+
books[index] = task;
93
+
} else {
94
+
books.push(task);
95
+
}
96
+
await writeFile("books.json", JSON.stringify(books));
97
+
};
98
+
99
+
const removeBook = async (id: string): Promise<void> => {
100
+
const books = await getBooks();
101
+
const index = books.findIndex((b) => b.id === id);
102
+
if (index !== -1) {
103
+
books.splice(index, 1);
104
+
await writeFile("books.json", JSON.stringify(books));
105
+
}
106
+
};
107
+
108
+
class BadDataIssues extends Error {
109
+
missingKeys: string[];
110
+
extraKeys: string[];
111
+
badValues: [string, string][];
112
+
113
+
constructor(
114
+
missingKeys: string[],
115
+
extraKeys: string[],
116
+
badValues: [string, string][],
117
+
) {
118
+
super("Bad data issues");
119
+
this.missingKeys = missingKeys;
120
+
this.extraKeys = extraKeys;
121
+
this.badValues = badValues;
122
+
}
123
+
}
124
+
125
+
const keyTypes = {
126
+
id: "ISBN13 code",
127
+
title: "string",
128
+
author: "string",
129
+
};
130
+
131
+
const validateBook = (task: { [key: string]: any }): Book => {
132
+
let missingKeys = ["id", "title", "author"].filter(
133
+
(key) => !Object.keys(task).includes(key),
134
+
);
135
+
let extraKeys = Object.keys(task).filter(
136
+
(key) => !["id", "title", "author"].includes(key),
137
+
);
138
+
let badValues = Object.entries(task)
139
+
.filter(([key, value]) => {
140
+
if (key === "id") return typeof value !== "string" || !ISBN13.test(value);
141
+
if (key === "title") return typeof value !== "string";
142
+
if (key === "author") return typeof value !== "string";
143
+
return false;
144
+
})
145
+
.map(
146
+
([key, _value]) =>
147
+
[key, keyTypes[key as keyof typeof keyTypes]] as [string, string],
148
+
);
149
+
if (missingKeys.length > 0 || extraKeys.length > 0 || badValues.length > 0) {
150
+
throw new BadDataIssues(missingKeys, extraKeys, badValues);
151
+
}
152
+
return task as Book;
153
+
};
154
+
155
+
const auth = async (req: Request, res: Response, next: NextFunction) => {
156
+
if (req.method === "GET") {
157
+
next();
158
+
return;
159
+
}
160
+
if (!req.headers.authorization) {
161
+
res.status(401).json({ error: "Unauthorized" });
162
+
return;
163
+
}
164
+
let token = req.headers.authorization.split(" ")[1];
165
+
if (token !== "password1!") {
166
+
res.status(401).json({ error: "Unauthorized" });
167
+
return;
168
+
}
169
+
next();
170
+
};
171
+
172
+
const errorHandler = (
173
+
err: Error,
174
+
_req: Request,
175
+
res: Response,
176
+
_next: NextFunction,
177
+
) => {
178
+
if (err instanceof BookError) {
179
+
let msg = err.message.replace("{{id}}", res.locals.id ?? "");
180
+
181
+
let obj: Map<string, any> = new Map<string, any>([
182
+
["error", `${err.name}: ${msg}`],
183
+
]);
184
+
185
+
if (res.locals.bdi) {
186
+
if (res.locals.bdi.missingKeys.length > 0) {
187
+
obj.set("missingKeys", res.locals.bdi.missingKeys);
188
+
}
189
+
if (res.locals.bdi.extraKeys.length > 0) {
190
+
obj.set("extraKeys", res.locals.bdi.extraKeys);
191
+
}
192
+
if (res.locals.bdi.badValues.length > 0) {
193
+
obj.set("badValues", res.locals.bdi.badValues);
194
+
}
195
+
}
196
+
197
+
res.status(err.status).json(Object.fromEntries(obj.entries()));
198
+
} else {
199
+
console.error(err.stack);
200
+
res.status(500).json({ error: "Internal Server Error" });
201
+
}
202
+
};
203
+
204
+
const router = express.Router();
205
+
206
+
router.use(express.json());
207
+
router.use((req, res, next) => {
208
+
console.log(`Recieved a ${req.method} request to ${req.url}`);
209
+
next();
210
+
});
211
+
router.use(auth);
212
+
213
+
router.get("/", async (_req, res) => {
214
+
res.json(await getBooks());
215
+
});
216
+
217
+
router.post("/", async (req, res) => {
218
+
const books = await getBooks();
219
+
try {
220
+
const bookData = validateBook(req.body);
221
+
res.locals.id = bookData.id;
222
+
if (books.filter((b) => b.id === bookData.id).length > 0) {
223
+
throw new BookError(ErrorType.AlreadyExists);
224
+
}
225
+
await updateBook(bookData);
226
+
res.status(201).json(bookData);
227
+
} catch (err) {
228
+
if (err instanceof BookError) {
229
+
throw err;
230
+
} else if (err instanceof BadDataIssues) {
231
+
res.locals.bdi = err;
232
+
throw new BookError(ErrorType.BadData);
233
+
} else {
234
+
res.status(500).json({ error: "Internal Server Error" });
235
+
}
236
+
}
237
+
});
238
+
239
+
router.get("/:id", async (req, res) => {
240
+
res.locals.id = req.params.id;
241
+
if (!ISBN13.test(req.params.id)) {
242
+
throw new BookError(ErrorType.InvalidId);
243
+
}
244
+
const books = await getBooks();
245
+
const book = books.find((b) => b.id == req.params.id);
246
+
if (!book) throw new BookError(ErrorType.NotFound);
247
+
res.json(book);
248
+
});
249
+
250
+
router.put("/:id", async (req, res) => {
251
+
res.locals.id = req.params.id;
252
+
if (!ISBN13.test(req.params.id)) {
253
+
throw new BookError(ErrorType.InvalidId);
254
+
}
255
+
const books = await getBooks();
256
+
const book = books.find((b) => b.id == req.params.id);
257
+
if (!book) throw new BookError(ErrorType.NotFound);
258
+
const bookData = validateBook(req.body);
259
+
await updateBook(bookData);
260
+
res.sendStatus(204);
261
+
});
262
+
263
+
router.delete("/reset", async (_req, res) => {
264
+
await initBooks();
265
+
res.sendStatus(204);
266
+
});
267
+
268
+
router.delete("/:id", async (req, res) => {
269
+
res.locals.id = req.params.id;
270
+
if (!ISBN13.test(req.params.id)) {
271
+
throw new BookError(ErrorType.InvalidId);
272
+
}
273
+
const books = await getBooks();
274
+
const book = books.find((b) => b.id == req.params.id);
275
+
if (!book) throw new BookError(ErrorType.NotFound);
276
+
await removeBook(book.id);
277
+
res.sendStatus(204);
278
+
});
279
+
280
+
router.all("/{*splat}", async (req, res) => {
281
+
res
282
+
.status(404)
283
+
.json({ error: `path: ${req.method} at /${req.params.splat} Not Found` });
284
+
});
285
+
286
+
router.use(errorHandler);
287
+
288
+
export default router;
+4
-140
server/index.ts
+4
-140
server/index.ts
···
1
import express from "express";
2
-
import { readFile, writeFile, exists } from "fs/promises";
3
4
const app = express();
5
const port = process.env["NODE_PORT"] ?? 5173;
6
7
-
interface Task {
8
-
id: string;
9
-
title: string;
10
-
completed: boolean;
11
-
}
12
-
13
-
const getTasks: () => Promise<Task[]> = async () => {
14
-
if (!(await exists("tasks.json"))) {
15
-
await writeFile("tasks.json", JSON.stringify([]));
16
-
}
17
-
const file = await readFile("tasks.json", "utf-8");
18
-
if (file.length === 0) {
19
-
return [];
20
-
}
21
-
return JSON.parse(file);
22
-
};
23
-
24
-
const updateTask = async (task: Task): Promise<void> => {
25
-
const tasks = await getTasks();
26
-
const index = tasks.findIndex((t) => t.id === task.id);
27
-
if (index !== -1) {
28
-
tasks[index] = task;
29
-
} else {
30
-
tasks.push(task);
31
-
}
32
-
await writeFile("tasks.json", JSON.stringify(tasks));
33
-
};
34
-
35
-
const removeTask = async (id: string): Promise<void> => {
36
-
const tasks = await getTasks();
37
-
const index = tasks.findIndex((t) => t.id === id);
38
-
if (index !== -1) {
39
-
tasks.splice(index, 1);
40
-
await writeFile("tasks.json", JSON.stringify(tasks));
41
-
}
42
-
};
43
-
44
-
app.use(express.json());
45
-
46
-
app.get("/tasks", async (_req, res) => {
47
-
res.json(await getTasks());
48
-
});
49
-
50
-
app.post("/tasks", async (req, res) => {
51
-
if (!(typeof req.body.title === "string")) {
52
-
res.status(400).send("Invalid title");
53
-
return;
54
-
}
55
-
const newTask = {
56
-
id: Math.random().toString(16).substring(2, 8),
57
-
title: req.body.title,
58
-
completed: false,
59
-
};
60
-
await updateTask(newTask);
61
-
res.json(newTask).status(201);
62
-
});
63
-
64
-
app.get("/tasks/:id", async (req, res) => {
65
-
const task = (await getTasks()).find((t) => t.id === req.params.id);
66
-
if (!task) {
67
-
res.status(404).send("Task not found");
68
-
} else {
69
-
res.json(task);
70
-
}
71
-
});
72
-
73
-
app.put("/tasks/:id", async (req, res) => {
74
-
const task = (await getTasks()).find((t) => t.id === req.params.id);
75
-
if (!task) {
76
-
res.status(404).send("Task not found");
77
-
} else {
78
-
const missing = [];
79
-
if (req.body.title === undefined) missing.push("title");
80
-
if (req.body.completed === undefined) missing.push("completed");
81
-
if (missing.length > 0) {
82
-
res
83
-
.status(400)
84
-
.send(
85
-
`Missing field${missing.length > 1 ? "s" : ""}: ${missing.join(", ")}`,
86
-
);
87
-
}
88
-
const badTypes = [];
89
-
if (!(typeof req.body.title === "string")) badTypes.push("title");
90
-
if (!(typeof req.body.completed === "boolean")) badTypes.push("completed");
91
-
if (badTypes.length > 0) {
92
-
res
93
-
.status(400)
94
-
.send(
95
-
`Invalid type${badTypes.length > 1 ? "s" : ""}: ${badTypes.join(", ")}`,
96
-
);
97
-
return;
98
-
}
99
-
task.title = req.body.title ?? task.title;
100
-
task.completed = req.body.completed ?? task.completed;
101
-
await updateTask(task);
102
-
res.json(task);
103
-
}
104
-
});
105
-
106
-
app.patch("/tasks/:id", async (req, res) => {
107
-
const task = (await getTasks()).find((t) => t.id === req.params.id);
108
-
if (!task) {
109
-
res.status(404).send("Task not found");
110
-
} else {
111
-
const badTypes = [];
112
-
if (
113
-
Object.keys(req.body).includes("title") &&
114
-
!(typeof req.body.title === "string")
115
-
)
116
-
badTypes.push("title");
117
-
if (
118
-
Object.keys(req.body).includes("completed") &&
119
-
!(typeof req.body.completed === "boolean")
120
-
)
121
-
badTypes.push("completed");
122
-
if (badTypes.length > 0) {
123
-
res
124
-
.status(400)
125
-
.send(
126
-
`Invalid type${badTypes.length > 1 ? "s" : ""}: ${badTypes.join(", ")}`,
127
-
);
128
-
return;
129
-
}
130
-
task.title = req.body.title ?? task.title;
131
-
task.completed = req.body.completed ?? task.completed;
132
-
await updateTask(task);
133
-
res.json(task);
134
-
}
135
-
});
136
-
137
-
app.delete("/tasks/:id", async (req, res) => {
138
-
const task = (await getTasks()).find((t) => t.id === req.params.id);
139
-
if (!task) {
140
-
res.status(404).send("Task not found");
141
-
} else {
142
-
await removeTask(task.id);
143
-
res.status(204).send();
144
-
}
145
-
});
146
147
app.listen(port, () => {
148
console.log(`Server listening on port ${port}`);
···
1
import express from "express";
2
+
import tasks from "./tasks.ts";
3
+
import books from "./books.ts";
4
5
const app = express();
6
const port = process.env["NODE_PORT"] ?? 5173;
7
8
+
app.use("/tasks", tasks);
9
+
app.use("/books", books);
10
11
app.listen(port, () => {
12
console.log(`Server listening on port ${port}`);
-1
server/tasks.json
-1
server/tasks.json
···
1
-
[{"id":"2d516f","title":"","completed":4},{"id":"a4ec84","title":"","completed":4},{"id":"a667ea","title":"","completed":4},{"id":"b4d420","title":"4e4tta475aj","completed":false}]
···
+146
server/tasks.ts
+146
server/tasks.ts
···
···
1
+
import express from "express";
2
+
import { readFile, writeFile, exists } from "fs/promises";
3
+
4
+
const router = express.Router();
5
+
6
+
interface Task {
7
+
id: string;
8
+
title: string;
9
+
completed: boolean;
10
+
}
11
+
12
+
const getTasks: () => Promise<Task[]> = async () => {
13
+
if (!(await exists("tasks.json"))) {
14
+
await writeFile("tasks.json", JSON.stringify([]));
15
+
}
16
+
const file = await readFile("tasks.json", "utf-8");
17
+
if (file.length === 0) {
18
+
return [];
19
+
}
20
+
return JSON.parse(file);
21
+
};
22
+
23
+
const updateTask = async (task: Task): Promise<void> => {
24
+
const tasks = await getTasks();
25
+
const index = tasks.findIndex((t) => t.id === task.id);
26
+
if (index !== -1) {
27
+
tasks[index] = task;
28
+
} else {
29
+
tasks.push(task);
30
+
}
31
+
await writeFile("tasks.json", JSON.stringify(tasks));
32
+
};
33
+
34
+
const removeTask = async (id: string): Promise<void> => {
35
+
const tasks = await getTasks();
36
+
const index = tasks.findIndex((t) => t.id === id);
37
+
if (index !== -1) {
38
+
tasks.splice(index, 1);
39
+
await writeFile("tasks.json", JSON.stringify(tasks));
40
+
}
41
+
};
42
+
43
+
router.use(express.json());
44
+
45
+
router.get("/", async (_req, res) => {
46
+
res.json(await getTasks());
47
+
});
48
+
49
+
router.post("/", async (req, res) => {
50
+
if (!(typeof req.body.title === "string")) {
51
+
res.status(400).send("Invalid title");
52
+
return;
53
+
}
54
+
const newTask = {
55
+
id: Math.random().toString(16).substring(2, 8),
56
+
title: req.body.title,
57
+
completed: false,
58
+
};
59
+
await updateTask(newTask);
60
+
res.json(newTask).status(201);
61
+
});
62
+
63
+
router.get("/:id", async (req, res) => {
64
+
const task = (await getTasks()).find((t) => t.id === req.params.id);
65
+
if (!task) {
66
+
res.status(404).send("Task not found");
67
+
} else {
68
+
res.json(task);
69
+
}
70
+
});
71
+
72
+
router.put("/:id", async (req, res) => {
73
+
const task = (await getTasks()).find((t) => t.id === req.params.id);
74
+
if (!task) {
75
+
res.status(404).send("Task not found");
76
+
} else {
77
+
const missing = [];
78
+
if (req.body.title === undefined) missing.push("title");
79
+
if (req.body.completed === undefined) missing.push("completed");
80
+
if (missing.length > 0) {
81
+
res
82
+
.status(400)
83
+
.send(
84
+
`Missing field${missing.length > 1 ? "s" : ""}: ${missing.join(", ")}`,
85
+
);
86
+
}
87
+
const badTypes = [];
88
+
if (!(typeof req.body.title === "string")) badTypes.push("title");
89
+
if (!(typeof req.body.completed === "boolean")) badTypes.push("completed");
90
+
if (badTypes.length > 0) {
91
+
res
92
+
.status(400)
93
+
.send(
94
+
`Invalid type${badTypes.length > 1 ? "s" : ""}: ${badTypes.join(", ")}`,
95
+
);
96
+
return;
97
+
}
98
+
task.title = req.body.title ?? task.title;
99
+
task.completed = req.body.completed ?? task.completed;
100
+
await updateTask(task);
101
+
res.json(task);
102
+
}
103
+
});
104
+
105
+
router.patch("/:id", async (req, res) => {
106
+
const task = (await getTasks()).find((t) => t.id === req.params.id);
107
+
if (!task) {
108
+
res.status(404).send("Task not found");
109
+
} else {
110
+
const badTypes = [];
111
+
if (
112
+
Object.keys(req.body).includes("title") &&
113
+
!(typeof req.body.title === "string")
114
+
)
115
+
badTypes.push("title");
116
+
if (
117
+
Object.keys(req.body).includes("completed") &&
118
+
!(typeof req.body.completed === "boolean")
119
+
)
120
+
badTypes.push("completed");
121
+
if (badTypes.length > 0) {
122
+
res
123
+
.status(400)
124
+
.send(
125
+
`Invalid type${badTypes.length > 1 ? "s" : ""}: ${badTypes.join(", ")}`,
126
+
);
127
+
return;
128
+
}
129
+
task.title = req.body.title ?? task.title;
130
+
task.completed = req.body.completed ?? task.completed;
131
+
await updateTask(task);
132
+
res.json(task);
133
+
}
134
+
});
135
+
136
+
router.delete("/:id", async (req, res) => {
137
+
const task = (await getTasks()).find((t) => t.id === req.params.id);
138
+
if (!task) {
139
+
res.status(404).send("Task not found");
140
+
} else {
141
+
await removeTask(task.id);
142
+
res.status(204).send();
143
+
}
144
+
});
145
+
146
+
export default router;