Add dinosaur CRUD validation
This commit is contained in:
parent
3739142e6b
commit
f432aa7d87
@ -9,3 +9,29 @@ GET http://localhost:3000/dinosaurs/not-a-number
|
|||||||
|
|
||||||
### Missing dinosaur
|
### Missing dinosaur
|
||||||
GET http://localhost:3000/dinosaurs/999999
|
GET http://localhost:3000/dinosaurs/999999
|
||||||
|
|
||||||
|
|
||||||
|
### Create a dinosaur
|
||||||
|
POST http://localhost:3000/dinosaurs
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Triceratops",
|
||||||
|
"description" : "Triceratops (neboli „třírohá tvář“) …",
|
||||||
|
"period": "křída",
|
||||||
|
"wikipediaAddress": "https://cs.wikipedia.org/wiki/Triceratops"
|
||||||
|
}
|
||||||
|
|
||||||
|
### Replace a dinosaur
|
||||||
|
PUT http://localhost:3000/dinosaurs/16
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Tyrannosaurus",
|
||||||
|
"description": "Tyrannosaurus …",
|
||||||
|
"period": "křída",
|
||||||
|
"wikipediaAddress": "https://en.wikipedia.org/wiki/Tyrannosaurus"
|
||||||
|
}
|
||||||
|
|
||||||
|
### Delete a dinosaur
|
||||||
|
DELETE http://localhost:3000/dinosaurs/2
|
||||||
|
|||||||
@ -13,7 +13,7 @@ const client = new Client({
|
|||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
app.get("/dinosaurs", async (request, response) => {
|
app.get("/dinosaurs", async (_request, response) => {
|
||||||
const result = await client.query("SELECT * FROM dinosaur ORDER BY id");
|
const result = await client.query("SELECT * FROM dinosaur ORDER BY id");
|
||||||
const dinosaurs = result.rows.map((row) => ({
|
const dinosaurs = result.rows.map((row) => ({
|
||||||
id: row.id,
|
id: row.id,
|
||||||
|
|||||||
11
lecture_5/dinosaurs/package-lock.json
generated
11
lecture_5/dinosaurs/package-lock.json
generated
@ -10,7 +10,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"pg": "^8.11.3"
|
"pg": "^8.11.3",
|
||||||
|
"zod": "^4.4.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
@ -1791,6 +1792,14 @@
|
|||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zod": {
|
||||||
|
"version": "4.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
|
||||||
|
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,7 +11,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"pg": "^8.11.3"
|
"pg": "^8.11.3",
|
||||||
|
"zod": "^4.4.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
|
|||||||
@ -55,4 +55,54 @@ export const dinosaurRepository = {
|
|||||||
|
|
||||||
return mapRowToDinosaurDto(result.rows[0]);
|
return mapRowToDinosaurDto(result.rows[0]);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a dinosaur and returns its generated ID.
|
||||||
|
*
|
||||||
|
* @param {DinosaurWithoutId} dinosaur
|
||||||
|
* @returns {Promise<number>}
|
||||||
|
*/
|
||||||
|
async createDinosaur(dinosaur) {
|
||||||
|
const result = await client.query(
|
||||||
|
`INSERT INTO dinosaur (name, description, period, wikipedia_address)
|
||||||
|
VALUES ($1, $2, $3, $4)
|
||||||
|
RETURNING id`,
|
||||||
|
[dinosaur.name, dinosaur.description, dinosaur.period, dinosaur.wikipediaAddress],
|
||||||
|
);
|
||||||
|
|
||||||
|
return result.rows[0].id;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a dinosaur by its ID.
|
||||||
|
*
|
||||||
|
* @param {number} id
|
||||||
|
* @param {DinosaurWithoutId} dinosaur
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
async updateDinosaurById(id, dinosaur) {
|
||||||
|
const result = await client.query(
|
||||||
|
`UPDATE dinosaur
|
||||||
|
SET name = $2,
|
||||||
|
description = $3,
|
||||||
|
period = $4,
|
||||||
|
wikipedia_address = $5
|
||||||
|
WHERE id = $1`,
|
||||||
|
[id, dinosaur.name, dinosaur.description, dinosaur.period, dinosaur.wikipediaAddress],
|
||||||
|
);
|
||||||
|
|
||||||
|
return result.rowCount > 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a dinosaur by its ID.
|
||||||
|
*
|
||||||
|
* @param {number} id
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
async deleteDinosaurById(id) {
|
||||||
|
const result = await client.query("DELETE FROM dinosaur WHERE id = $1", [id]);
|
||||||
|
|
||||||
|
return result.rowCount > 0;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,8 +1,16 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
|
import { dinosaurIdParamSchema, dinosaurBodySchema } from "../validation/dinosaurSchemas.js";
|
||||||
import { dinosaurRepository } from "../repositories/dinosaurRepository.js";
|
import { dinosaurRepository } from "../repositories/dinosaurRepository.js";
|
||||||
|
|
||||||
export const dinosaurRouter = express.Router();
|
export const dinosaurRouter = express.Router();
|
||||||
|
|
||||||
|
const formatValidationErrors = (issues) => {
|
||||||
|
return issues.map((issue) => ({
|
||||||
|
field: issue.path.join("."),
|
||||||
|
message: issue.message,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
// Return all dinosaurs through the repository abstraction.
|
// Return all dinosaurs through the repository abstraction.
|
||||||
dinosaurRouter.get("/dinosaurs", async (_request, response) => {
|
dinosaurRouter.get("/dinosaurs", async (_request, response) => {
|
||||||
const dinosaurs = await dinosaurRepository.listDinosaurs();
|
const dinosaurs = await dinosaurRepository.listDinosaurs();
|
||||||
@ -11,14 +19,16 @@ dinosaurRouter.get("/dinosaurs", async (_request, response) => {
|
|||||||
|
|
||||||
// Return one dinosaur identified by a positive integer ID.
|
// Return one dinosaur identified by a positive integer ID.
|
||||||
dinosaurRouter.get("/dinosaurs/:id", async (request, response) => {
|
dinosaurRouter.get("/dinosaurs/:id", async (request, response) => {
|
||||||
const id = Number(request.params.id);
|
const parsedParams = dinosaurIdParamSchema.safeParse(request.params);
|
||||||
|
|
||||||
if (!Number.isInteger(id) || id <= 0) {
|
if (!parsedParams.success) {
|
||||||
return response.status(400).json({
|
return response.status(400).json({
|
||||||
message: "Dinosaur ID must be a positive integer",
|
message: "Dinosaur ID must be a positive integer",
|
||||||
|
errors: formatValidationErrors(parsedParams.error.issues),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { id } = parsedParams.data;
|
||||||
const dinosaur = await dinosaurRepository.getDinosaurById(id);
|
const dinosaur = await dinosaurRepository.getDinosaurById(id);
|
||||||
|
|
||||||
if (dinosaur === null) {
|
if (dinosaur === null) {
|
||||||
@ -29,3 +39,71 @@ dinosaurRouter.get("/dinosaurs/:id", async (request, response) => {
|
|||||||
|
|
||||||
response.json(dinosaur);
|
response.json(dinosaur);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Insert one dinosaur into the database.
|
||||||
|
dinosaurRouter.post("/dinosaurs", async (request, response) => {
|
||||||
|
const parsedBody = dinosaurBodySchema.safeParse(request.body);
|
||||||
|
|
||||||
|
if (!parsedBody.success) {
|
||||||
|
return response.status(400).json({
|
||||||
|
message: "Invalid dinosaur payload",
|
||||||
|
errors: formatValidationErrors(parsedBody.error.issues),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = await dinosaurRepository.createDinosaur(parsedBody.data);
|
||||||
|
response.status(201).json({ id });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Replace one dinosaur identified by a positive integer ID.
|
||||||
|
dinosaurRouter.put("/dinosaurs/:id", async (request, response) => {
|
||||||
|
const parsedParams = dinosaurIdParamSchema.safeParse(request.params);
|
||||||
|
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return response.status(400).json({
|
||||||
|
message: "Dinosaur ID must be a positive integer",
|
||||||
|
errors: formatValidationErrors(parsedParams.error.issues),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedBody = dinosaurBodySchema.safeParse(request.body);
|
||||||
|
|
||||||
|
if (!parsedBody.success) {
|
||||||
|
return response.status(400).json({
|
||||||
|
message: "Invalid dinosaur payload",
|
||||||
|
errors: formatValidationErrors(parsedBody.error.issues),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await dinosaurRepository.updateDinosaurById(parsedParams.data.id, parsedBody.data);
|
||||||
|
|
||||||
|
if (!updated) {
|
||||||
|
return response.status(404).json({
|
||||||
|
message: "Dinosaur not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
response.sendStatus(204);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete one dinosaur identified by a positive integer ID.
|
||||||
|
dinosaurRouter.delete("/dinosaurs/:id", async (request, response) => {
|
||||||
|
const parsedParams = dinosaurIdParamSchema.safeParse(request.params);
|
||||||
|
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return response.status(400).json({
|
||||||
|
message: "Dinosaur ID must be a positive integer",
|
||||||
|
errors: formatValidationErrors(parsedParams.error.issues),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleted = await dinosaurRepository.deleteDinosaurById(parsedParams.data.id);
|
||||||
|
|
||||||
|
if (!deleted) {
|
||||||
|
return response.status(404).json({
|
||||||
|
message: "Dinosaur not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
response.sendStatus(204);
|
||||||
|
});
|
||||||
|
|||||||
17
lecture_5/dinosaurs/src/validation/dinosaurSchemas.js
Normal file
17
lecture_5/dinosaurs/src/validation/dinosaurSchemas.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
// Route schemas keep HTTP input aligned with the database constraints in schema.sql.
|
||||||
|
export const dinosaurIdParamSchema = z
|
||||||
|
.object({
|
||||||
|
id: z.coerce.number().int("id must be an integer").min(1, { message: "id must be greater than or equal to 1" }),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
export const dinosaurBodySchema = z
|
||||||
|
.object({
|
||||||
|
name: z.string().min(1, { message: "name must not be an empty string" }).max(256),
|
||||||
|
description: z.string().min(1, { message: "description must not be an empty string" }).max(4096),
|
||||||
|
period: z.string().min(1, { message: "period must not be an empty string" }).max(32),
|
||||||
|
wikipediaAddress: z.url().max(4096),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
Loading…
Reference in New Issue
Block a user