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