feat: Dec10 #1

merged
opened by thecoded.prof targeting main from private/coded/push-vpwzwsyztzpm
Changed files
+439 -141
server
+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
··· 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
··· 1 1 import express from "express"; 2 - import { readFile, writeFile, exists } from "fs/promises"; 2 + import tasks from "./tasks.ts"; 3 + import books from "./books.ts"; 3 4 4 5 const app = express(); 5 6 const port = process.env["NODE_PORT"] ?? 5173; 6 7 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 - }); 8 + app.use("/tasks", tasks); 9 + app.use("/books", books); 146 10 147 11 app.listen(port, () => { 148 12 console.log(`Server listening on port ${port}`);
-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
··· 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;