Build layered dinosaur database API
This commit is contained in:
parent
8e5912fcf9
commit
3739142e6b
@ -1,5 +0,0 @@
|
||||
{
|
||||
"endOfLine": "lf",
|
||||
"useTabs": true,
|
||||
"printWidth": 120
|
||||
}
|
||||
6
lecture_5/dinosaurs/.env.example
Normal file
6
lecture_5/dinosaurs/.env.example
Normal file
@ -0,0 +1,6 @@
|
||||
PGHOST="localhost"
|
||||
PGPORT="5432"
|
||||
PGDATABASE="rg_academy_dev"
|
||||
PGUSER="rg_academy"
|
||||
PGPASSWORD="rg_academy"
|
||||
PORT="3000"
|
||||
5
lecture_5/dinosaurs/.prettierrc.json
Normal file
5
lecture_5/dinosaurs/.prettierrc.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"endOfLine": "lf",
|
||||
"useTabs": true,
|
||||
"printWidth": 120
|
||||
}
|
||||
11
lecture_5/dinosaurs/dinosaurs.http
Normal file
11
lecture_5/dinosaurs/dinosaurs.http
Normal file
@ -0,0 +1,11 @@
|
||||
### 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
|
||||
@ -15,6 +15,7 @@ export default defineConfig([
|
||||
rules: {
|
||||
indent: ["error", "tab"],
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
|
||||
quotes: ["error", "double"],
|
||||
semi: ["error", "always"],
|
||||
},
|
||||
39
lecture_5/dinosaurs/old-index.js
Normal file
39
lecture_5/dinosaurs/old-index.js
Normal file
@ -0,0 +1,39 @@
|
||||
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);
|
||||
});
|
||||
1796
lecture_5/dinosaurs/package-lock.json
generated
Normal file
1796
lecture_5/dinosaurs/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
lecture_5/dinosaurs/package.json
Normal file
23
lecture_5/dinosaurs/package.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"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": {
|
||||
"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
|
||||
CREATE TABLE dinosaur (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(256) NOT NULL,
|
||||
name VARCHAR(256) NOT NULL UNIQUE,
|
||||
description VARCHAR(4096) NOT NULL,
|
||||
period VARCHAR(32) NOT NULL,
|
||||
wikipedia_address VARCHAR(4096) NOT NULL
|
||||
);
|
||||
|
||||
-- Insert sample data into dinosaur table
|
||||
-- Seed records provide predictable data for the API exercises.
|
||||
INSERT INTO dinosaur (name, description, period, wikipedia_address)
|
||||
VALUES (
|
||||
'Tyrannosaurus',
|
||||
9
lecture_5/dinosaurs/src/app.js
Normal file
9
lecture_5/dinosaurs/src/app.js
Normal file
@ -0,0 +1,9 @@
|
||||
import express from "express";
|
||||
import { dinosaurRouter } from "./routes/dinosaurRoutes.js";
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(express.json());
|
||||
app.use(dinosaurRouter);
|
||||
|
||||
export { app };
|
||||
5
lecture_5/dinosaurs/src/db/client.js
Normal file
5
lecture_5/dinosaurs/src/db/client.js
Normal file
@ -0,0 +1,5 @@
|
||||
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();
|
||||
23
lecture_5/dinosaurs/src/index.js
Normal file
23
lecture_5/dinosaurs/src/index.js
Normal file
@ -0,0 +1,23 @@
|
||||
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);
|
||||
});
|
||||
58
lecture_5/dinosaurs/src/repositories/dinosaurRepository.js
Normal file
58
lecture_5/dinosaurs/src/repositories/dinosaurRepository.js
Normal file
@ -0,0 +1,58 @@
|
||||
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]);
|
||||
},
|
||||
};
|
||||
31
lecture_5/dinosaurs/src/routes/dinosaurRoutes.js
Normal file
31
lecture_5/dinosaurs/src/routes/dinosaurRoutes.js
Normal file
@ -0,0 +1,31 @@
|
||||
import express from "express";
|
||||
import { dinosaurRepository } from "../repositories/dinosaurRepository.js";
|
||||
|
||||
export const dinosaurRouter = express.Router();
|
||||
|
||||
// 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 id = Number(request.params.id);
|
||||
|
||||
if (!Number.isInteger(id) || id <= 0) {
|
||||
return response.status(400).json({
|
||||
message: "Dinosaur ID must be a positive integer",
|
||||
});
|
||||
}
|
||||
|
||||
const dinosaur = await dinosaurRepository.getDinosaurById(id);
|
||||
|
||||
if (dinosaur === null) {
|
||||
return response.status(404).json({
|
||||
message: "Dinosaur not found",
|
||||
});
|
||||
}
|
||||
|
||||
response.json(dinosaur);
|
||||
});
|
||||
1796
lecture_5/package-lock.json
generated
1796
lecture_5/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,20 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user