Compare commits
No commits in common. "9c32f1ff4d40900f853eac47954b151cb3aa2eba" and "8e5912fcf9d26ce04f48550d71e30a0f0395b0c9" have entirely different histories.
9c32f1ff4d
...
8e5912fcf9
5
lecture_5/.prettierrc.json
Normal file
5
lecture_5/.prettierrc.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"useTabs": true,
|
||||||
|
"printWidth": 120
|
||||||
|
}
|
||||||
@ -1,6 +0,0 @@
|
|||||||
PGHOST="localhost"
|
|
||||||
PGPORT="5432"
|
|
||||||
PGDATABASE="rg_academy_dev"
|
|
||||||
PGUSER="rg_academy"
|
|
||||||
PGPASSWORD="rg_academy"
|
|
||||||
PORT="3000"
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"endOfLine": "lf",
|
|
||||||
"useTabs": true,
|
|
||||||
"printWidth": 120
|
|
||||||
}
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
### List all dinosaurs
|
|
||||||
GET http://localhost:3000/dinosaurs
|
|
||||||
|
|
||||||
### Get one dinosaur
|
|
||||||
GET http://localhost:3000/dinosaurs/1
|
|
||||||
|
|
||||||
### Invalid dinosaur ID
|
|
||||||
GET http://localhost:3000/dinosaurs/not-a-number
|
|
||||||
|
|
||||||
### Missing dinosaur
|
|
||||||
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
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
import "dotenv/config";
|
|
||||||
import express from "express";
|
|
||||||
import { Client } from "pg";
|
|
||||||
|
|
||||||
// Legacy single-file implementation kept for comparison with the layered src/ structure.
|
|
||||||
const client = new Client({
|
|
||||||
host: process.env.PGHOST,
|
|
||||||
port: Number(process.env.PGPORT),
|
|
||||||
database: process.env.PGDATABASE,
|
|
||||||
user: process.env.PGUSER,
|
|
||||||
password: process.env.PGPASSWORD,
|
|
||||||
});
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
|
|
||||||
app.get("/dinosaurs", async (_request, response) => {
|
|
||||||
const result = await client.query("SELECT * FROM dinosaur ORDER BY id");
|
|
||||||
const dinosaurs = result.rows.map((row) => ({
|
|
||||||
id: row.id,
|
|
||||||
name: row.name,
|
|
||||||
description: row.description,
|
|
||||||
period: row.period,
|
|
||||||
wikipediaAddress: row.wikipedia_address,
|
|
||||||
}));
|
|
||||||
response.json(dinosaurs);
|
|
||||||
});
|
|
||||||
|
|
||||||
await client.connect();
|
|
||||||
console.log("Connected to database");
|
|
||||||
|
|
||||||
app.listen(3000, () => {
|
|
||||||
console.log("Server running on port 3000");
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on("SIGINT", async () => {
|
|
||||||
console.log("Shutting down...");
|
|
||||||
await client.end();
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
1830
lecture_5/dinosaurs/package-lock.json
generated
1830
lecture_5/dinosaurs/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "dinosaurs",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"start": "node src/index.js",
|
|
||||||
"lint": "eslint .",
|
|
||||||
"format": "prettier --write .",
|
|
||||||
"check": "npm run lint && prettier --check ."
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"cors": "^2.8.6",
|
|
||||||
"dotenv": "^17.4.2",
|
|
||||||
"express": "^5.1.0",
|
|
||||||
"pg": "^8.11.3",
|
|
||||||
"zod": "^4.4.3"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@eslint/js": "^10.0.1",
|
|
||||||
"@eslint/json": "^1.2.0",
|
|
||||||
"eslint": "^10.0.0",
|
|
||||||
"globals": "^17.4.0",
|
|
||||||
"prettier": "^3.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
import express from "express";
|
|
||||||
import cors from "cors";
|
|
||||||
import { dinosaurRouter } from "./routes/dinosaurRoutes.js";
|
|
||||||
import { errorHandler } from "./validation/errorHandler.js";
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
|
|
||||||
app.use(cors());
|
|
||||||
app.use(express.json());
|
|
||||||
app.use(dinosaurRouter);
|
|
||||||
// Error middleware must be registered after routes so it can handle rejected route handlers.
|
|
||||||
app.use(errorHandler);
|
|
||||||
|
|
||||||
export { app };
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
import "dotenv/config";
|
|
||||||
import { Client } from "pg";
|
|
||||||
|
|
||||||
// The pg client reads the standard PGHOST, PGPORT, PGDATABASE, PGUSER, and PGPASSWORD variables.
|
|
||||||
export const client = new Client();
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
import { client } from "./db/client.js";
|
|
||||||
import { app } from "./app.js";
|
|
||||||
|
|
||||||
const port = Number(process.env.PORT ?? 3000);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await client.connect();
|
|
||||||
console.log("Connected to database");
|
|
||||||
|
|
||||||
app.listen(port, () => {
|
|
||||||
console.log(`Server running on http://localhost:${port}`);
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to start the server:", error.message);
|
|
||||||
process.exitCode = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close the database connection when the process is stopped from the terminal.
|
|
||||||
process.on("SIGINT", async () => {
|
|
||||||
console.log("Shutting down...");
|
|
||||||
await client.end();
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
@ -1,108 +0,0 @@
|
|||||||
import { client } from "../db/client.js";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {Object} DinosaurWithoutId
|
|
||||||
* @property {string} name
|
|
||||||
* @property {string} description
|
|
||||||
* @property {string} period
|
|
||||||
* @property {string} wikipediaAddress
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {DinosaurWithoutId} Dinosaur
|
|
||||||
* @property {number} id
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a database row representing a dinosaur into a `Dinosaur` object.
|
|
||||||
*
|
|
||||||
* @param {Object} row
|
|
||||||
* @returns {Dinosaur}
|
|
||||||
*/
|
|
||||||
const mapRowToDinosaurDto = (row) => {
|
|
||||||
return {
|
|
||||||
id: row.id,
|
|
||||||
name: row.name,
|
|
||||||
description: row.description,
|
|
||||||
period: row.period,
|
|
||||||
wikipediaAddress: row.wikipedia_address,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const dinosaurRepository = {
|
|
||||||
/**
|
|
||||||
* Obtains all dinosaurs, ordered by ID.
|
|
||||||
*
|
|
||||||
* @returns {Promise<Dinosaur[]>}
|
|
||||||
*/
|
|
||||||
async listDinosaurs() {
|
|
||||||
const result = await client.query("SELECT * FROM dinosaur ORDER BY id");
|
|
||||||
return result.rows.map(mapRowToDinosaurDto);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Obtains one dinosaur by its ID.
|
|
||||||
*
|
|
||||||
* @param {number} id
|
|
||||||
* @returns {Promise<Dinosaur | null>}
|
|
||||||
*/
|
|
||||||
async getDinosaurById(id) {
|
|
||||||
const result = await client.query("SELECT * FROM dinosaur WHERE id = $1", [id]);
|
|
||||||
|
|
||||||
if (result.rowCount === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
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,109 +0,0 @@
|
|||||||
import express from "express";
|
|
||||||
import { dinosaurIdParamSchema, dinosaurBodySchema } from "../validation/dinosaurSchemas.js";
|
|
||||||
import { dinosaurRepository } from "../repositories/dinosaurRepository.js";
|
|
||||||
|
|
||||||
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.
|
|
||||||
dinosaurRouter.get("/dinosaurs", async (_request, response) => {
|
|
||||||
const dinosaurs = await dinosaurRepository.listDinosaurs();
|
|
||||||
response.json(dinosaurs);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Return one dinosaur identified by a positive integer ID.
|
|
||||||
dinosaurRouter.get("/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 { id } = parsedParams.data;
|
|
||||||
const dinosaur = await dinosaurRepository.getDinosaurById(id);
|
|
||||||
|
|
||||||
if (dinosaur === null) {
|
|
||||||
return response.status(404).json({
|
|
||||||
message: "Dinosaur not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
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();
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
import { ZodError } from "zod";
|
|
||||||
|
|
||||||
// Centralized Express error middleware for validation and unexpected runtime errors.
|
|
||||||
export const errorHandler = (error, _request, response, _next) => {
|
|
||||||
console.error(error);
|
|
||||||
|
|
||||||
if (error instanceof ZodError) {
|
|
||||||
return response.status(400).json({
|
|
||||||
message: "Validation error",
|
|
||||||
errors: error.issues,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.status(500).json({
|
|
||||||
message: "Internal server error",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@ -15,7 +15,6 @@ export default defineConfig([
|
|||||||
rules: {
|
rules: {
|
||||||
indent: ["error", "tab"],
|
indent: ["error", "tab"],
|
||||||
"linebreak-style": ["error", "unix"],
|
"linebreak-style": ["error", "unix"],
|
||||||
"no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
|
|
||||||
quotes: ["error", "double"],
|
quotes: ["error", "double"],
|
||||||
semi: ["error", "always"],
|
semi: ["error", "always"],
|
||||||
},
|
},
|
||||||
1796
lecture_5/package-lock.json
generated
Normal file
1796
lecture_5/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
lecture_5/package.json
Normal file
20
lecture_5/package.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "dinosaurs",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node index.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"dotenv": "^17.4.2",
|
||||||
|
"express": "^5.1.0",
|
||||||
|
"pg": "^8.11.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^10.0.1",
|
||||||
|
"@eslint/json": "^1.2.0",
|
||||||
|
"eslint": "^10.0.0",
|
||||||
|
"globals": "^17.4.0",
|
||||||
|
"prettier": "^3.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,13 +1,13 @@
|
|||||||
-- Dinosaur table
|
-- Dinosaur table
|
||||||
CREATE TABLE dinosaur (
|
CREATE TABLE dinosaur (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
name VARCHAR(256) NOT NULL UNIQUE,
|
name VARCHAR(256) NOT NULL,
|
||||||
description VARCHAR(4096) NOT NULL,
|
description VARCHAR(4096) NOT NULL,
|
||||||
period VARCHAR(32) NOT NULL,
|
period VARCHAR(32) NOT NULL,
|
||||||
wikipedia_address VARCHAR(4096) NOT NULL
|
wikipedia_address VARCHAR(4096) NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Seed records provide predictable data for the API exercises.
|
-- Insert sample data into dinosaur table
|
||||||
INSERT INTO dinosaur (name, description, period, wikipedia_address)
|
INSERT INTO dinosaur (name, description, period, wikipedia_address)
|
||||||
VALUES (
|
VALUES (
|
||||||
'Tyrannosaurus',
|
'Tyrannosaurus',
|
||||||
Loading…
Reference in New Issue
Block a user