diff options
Diffstat (limited to 'backend')
| -rw-r--r-- | backend/package-lock.json | 169 | ||||
| -rw-r--r-- | backend/package.json | 13 | ||||
| -rw-r--r-- | backend/src/db.ts (renamed from backend/src/db.js) | 31 | ||||
| -rw-r--r-- | backend/src/routes/api.js | 12 | ||||
| -rw-r--r-- | backend/src/routes/api.ts | 10 | ||||
| -rw-r--r-- | backend/src/routes/events.ts (renamed from backend/src/routes/events.js) | 62 | ||||
| -rw-r--r-- | backend/src/routes/users.ts (renamed from backend/src/routes/users.js) | 42 | ||||
| -rw-r--r-- | backend/src/server.js | 40 | ||||
| -rw-r--r-- | backend/src/server.ts | 37 | ||||
| -rw-r--r-- | backend/src/types.ts | 24 | ||||
| -rw-r--r-- | backend/tsconfig.json | 46 |
11 files changed, 367 insertions, 119 deletions
diff --git a/backend/package-lock.json b/backend/package-lock.json index 47e566a..e2ddc64 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -14,6 +14,154 @@ "express": "^4.21.2", "helmet": "^8.1.0", "morgan": "^1.10.1" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.13", + "@types/cookie-parser": "^1.4.10", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.6", + "@types/morgan": "^1.9.10", + "@types/node": "^25.0.9", + "typescript": "^5.9.3" + } + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookie-parser": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.10.tgz", + "integrity": "sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/morgan": { + "version": "1.9.10", + "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.10.tgz", + "integrity": "sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "25.0.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.9.tgz", + "integrity": "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" } }, "node_modules/accepts": { @@ -1324,6 +1472,27 @@ "node": ">= 0.6" } }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", diff --git a/backend/package.json b/backend/package.json index b6c45cd..77bb09b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -4,8 +4,8 @@ "private": true, "type": "module", "scripts": { - "start": "NODE_ENV=production node src/server.js", - "dev": "node --watch src/server.js" + "start": "NODE_ENV=production node dist/server.js", + "dev": "node --watch dist/server.js" }, "dependencies": { "better-sqlite3": "^11.10.0", @@ -14,5 +14,14 @@ "express": "^4.21.2", "helmet": "^8.1.0", "morgan": "^1.10.1" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.13", + "@types/cookie-parser": "^1.4.10", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.6", + "@types/morgan": "^1.9.10", + "@types/node": "^25.0.9", + "typescript": "^5.9.3" } } diff --git a/backend/src/db.js b/backend/src/db.ts index f20cac1..2b124f1 100644 --- a/backend/src/db.js +++ b/backend/src/db.ts @@ -1,16 +1,15 @@ -import Database from 'better-sqlite3'; -import EVENTS from './data/events.json' with {type: 'json'}; -import USERS from './data/users.json' with {type: 'json'}; -import RSVPS from './data/rsvps.json' with {type: 'json'}; +import DatabaseConstructor, { type Database } from "better-sqlite3"; +import EVENTS from "./data/events.json" with { type: "json" }; +import USERS from "./data/users.json" with { type: "json" }; +import RSVPS from "./data/rsvps.json" with { type: "json" }; - -const db = new Database('src/sqlite.db', { verbose: console.log }); +const db: Database = new DatabaseConstructor("src/sqlite.db", { + verbose: console.log, +}); console.log(`Initializing database: ${db.name} `); - -db.pragma('foreign_keys = ON'); - +db.pragma("foreign_keys = ON"); db.exec(` CREATE TABLE IF NOT EXISTS users ( @@ -32,24 +31,22 @@ db.exec(` CREATE INDEX IF NOT EXISTS eventhosts ON events(host_id); `); - const upsertUser = db.prepare(` INSERT INTO users VALUES (@id, @username, @name, @email) ON CONFLICT(id) DO NOTHING - `) + `); USERS.map((user) => upsertUser.run(user)); const upsertEvent = db.prepare(` INSERT INTO events VALUES (@id, @title, @description, @image_url, @date, @host_id) ON CONFLICT(id) DO NOTHING - `) + `); EVENTS.map((event) => { - upsertEvent.run(event); + upsertEvent.run(event); }); - db.exec(` CREATE TABLE IF NOT EXISTS rsvps ( event_id INTEGER REFERENCES events NOT NULL, @@ -64,9 +61,7 @@ const upsertRSVP = db.prepare(` INSERT INTO rsvps VALUES (@event_id, @name, @email) `); RSVPS.map((rsvp) => { - upsertRSVP.run(rsvp); + upsertRSVP.run(rsvp); }); - - -export default db;
\ No newline at end of file +export default db; diff --git a/backend/src/routes/api.js b/backend/src/routes/api.js deleted file mode 100644 index 2eb07d1..0000000 --- a/backend/src/routes/api.js +++ /dev/null @@ -1,12 +0,0 @@ -import { Router } from 'express'; -import usersRouter from './users.js'; -import eventsRouter from './events.js'; - - -const router = Router(); - -router.use('/users', usersRouter); -router.use('/events', eventsRouter); - - -export default router; diff --git a/backend/src/routes/api.ts b/backend/src/routes/api.ts new file mode 100644 index 0000000..92eae46 --- /dev/null +++ b/backend/src/routes/api.ts @@ -0,0 +1,10 @@ +import { Router } from "express"; +import usersRouter from "./users.js"; +import eventsRouter from "./events.js"; + +const router = Router(); + +router.use("/users", usersRouter); +router.use("/events", eventsRouter); + +export default router; diff --git a/backend/src/routes/events.js b/backend/src/routes/events.ts index b95c747..df65eb5 100644 --- a/backend/src/routes/events.js +++ b/backend/src/routes/events.ts @@ -1,52 +1,56 @@ -import db from '../db.js'; -import { Router } from 'express'; -import { getUser } from './users.js'; +import db from "../db.js"; +import { Router } from "express"; +import { getUser } from "./users.js"; +import { type Event, type Id, type Rsvps, type User } from "../types.js"; const router = Router(); -const joinHost = (event) => { - const host = getUser(event.host_id); +const joinHost = (event: Event) => { + const host = getUser(event.host_id) as User; return { ...event, host }; -} +}; -const joinRSVPs = (event) => { +const joinRSVPs = (event: Event) => { const { id } = event; - const getRSVPs = db.prepare('SELECT * FROM rsvps WHERE event_id = @id'); - const rsvps = getRSVPs.all({ id }); + const getRSVPs = db.prepare("SELECT * FROM rsvps WHERE event_id = @id"); + const rsvps = getRSVPs.all({ id }) as Rsvps[]; return { ...event, rsvps }; -} +}; -const getEvent = (eventId) => { - const byId = db.prepare('SELECT * FROM events WHERE id = @eventId'); - const event = byId.get({ eventId }); +const getEvent = (eventId: Id) => { + const byId = db.prepare("SELECT * FROM events WHERE id = @eventId"); + const event = byId.get({ eventId }) as Event; return joinHost(event); -} +}; -router.get('/', (_req, res) => { +router.get("/", (_req, res) => { const listEvents = db.prepare(`SELECT * FROM events`); - const events = listEvents.all(); + const events = listEvents.all() as Event[]; res.json(events.map(joinHost).map(joinRSVPs)); }); -const insertEvent = db.prepare(`INSERT INTO events VALUES (@id, @title, @description, @image_url, @date, @host_id)`); +const insertEvent = db.prepare( + `INSERT INTO events VALUES (@id, @title, @description, @image_url, @date, @host_id)`, +); -router.post('/new', (req, res) => { +router.post("/new", (req, res) => { const data = req.body; - const { lastInsertRowid: id } = insertEvent.run(data); + const { lastInsertRowid } = insertEvent.run(data); + const id = lastInsertRowid as number; const event = getEvent(id); res.status(201).json(event); }); -router.get('/:id', (req, res) => { +router.get("/:id", (req, res) => { const id = parseInt(req.params.id); const event = getEvent(id); if (!event) { - return res.status(404).json({ error: 'Event not found' }); + return res.status(404).json({ error: "Event not found" }); } res.json(event); }); -router.patch('/:id', (req, res) => { +router.patch("/:id", (req, res) => { const eventId = parseInt(req.params.id); const patch = req.body; @@ -64,23 +68,27 @@ router.patch('/:id', (req, res) => { res.json(updated); }); -router.delete('/:id', (req, res) => { +router.delete("/:id", (req, res) => { const deleteEvent = db.prepare(`DELETE FROM events WHERE id = @eventId`); const eventId = parseInt(req.params.id); const event = getEvent(eventId); if (!event) { - return res.status(404).json({ error: 'Event not found' }); + return res.status(404).json({ error: "Event not found" }); } deleteEvent.run({ eventId }); res.json(event); }); -router.post('/:id/rsvp', (req, res) => { +router.post("/:id/rsvp", (req, res) => { const eventId = parseInt(req.params.id); const { name, email } = req.body; - const getRSVP = db.prepare(`SELECT * FROM rsvps WHERE (event_id = ${eventId} AND email = '${email}')`); - const insertRSVP = db.prepare(`INSERT INTO rsvps VALUES (@eventId, @name, @email)`); + const getRSVP = db.prepare( + `SELECT * FROM rsvps WHERE (event_id = ${eventId} AND email = '${email}')`, + ); + const insertRSVP = db.prepare( + `INSERT INTO rsvps VALUES (@eventId, @name, @email)`, + ); let [rsvp] = getRSVP.all({ eventId, email }); if (rsvp) { diff --git a/backend/src/routes/users.js b/backend/src/routes/users.ts index 6d874e9..98ec361 100644 --- a/backend/src/routes/users.js +++ b/backend/src/routes/users.ts @@ -1,40 +1,42 @@ -import { Router } from 'express'; -import db from '../db.js'; +import { Router } from "express"; +import db from "../db.js"; +import type { User } from "../types.js"; const router = Router(); -export const getUser = (userId) => { - const byId = db.prepare('SELECT * FROM users WHERE id = @userId'); +export const getUser = (userId: User["id"]) => { + const byId = db.prepare("SELECT * FROM users WHERE id = @userId"); return byId.get({ userId }); -} +}; -router.get('/', (_req, res) => { - const listUsers = db.prepare(`SELECT * FROM users`) +router.get("/", (_req, res) => { + const listUsers = db.prepare(`SELECT * FROM users`); const users = listUsers.all(); res.json(users); }); -router.post('/new', (req, res) => { +router.post("/new", (req, res) => { const data = req.body; - const cols = Object.keys(data).join(' , '); - const vals = Object.values(data).join(' , '); + const cols = Object.keys(data).join(" , "); + const vals = Object.values(data).join(" , "); const insertUser = db.prepare(`INSERT INTO users(@cols) VALUES (@vals)`); - const { lastInsertRowid: id } = insertUser.run({ cols, vals }); + const { lastInsertRowid } = insertUser.run({ cols, vals }); + const id = lastInsertRowid as number; const user = getUser(id); res.json(user); }); -router.get('/:id', (req, res) => { - const id = req.params.id; +router.get("/:id", (req, res) => { + const id = Number(req.params.id); const user = getUser(id); if (!user) { - res.status(404).json({ error: 'User not found' }); + res.status(404).json({ error: "User not found" }); } res.json(user); }); -router.patch('/:id', (req, res) => { - const userId = req.params.id; +router.patch("/:id", (req, res) => { + const userId = Number(req.params.id); const patch = req.body; const updateCol = db.prepare(` @@ -43,7 +45,7 @@ router.patch('/:id', (req, res) => { const updateUser = db.transaction((patch) => { for (const [col, val] of Object.entries(patch)) { updateCol.run(col, val, userId); - }; + } }); updateUser(Object.entries(patch)); @@ -51,12 +53,12 @@ router.patch('/:id', (req, res) => { res.json(updated); }); -router.delete('/:id', (req, res) => { - const deleteUser = db.prepare(`DELETE FROM users WHERE id = @userId`) +router.delete("/:id", (req, res) => { + const deleteUser = db.prepare(`DELETE FROM users WHERE id = @userId`); const userId = parseInt(req.params.id); const user = getUser(userId); if (!user) { - res.status(404).json({ error: 'User not found' }); + res.status(404).json({ error: "User not found" }); } deleteUser.run({ userId }); res.json(user); diff --git a/backend/src/server.js b/backend/src/server.js deleted file mode 100644 index 8b579b3..0000000 --- a/backend/src/server.js +++ /dev/null @@ -1,40 +0,0 @@ -import express from 'express'; -import cookieParser from 'cookie-parser'; -import logger from 'morgan'; -import cors from 'cors'; -import helmet from 'helmet'; - -import apiRouter from './routes/api.js'; - - -const PORT = process.env.PORT || 3000; -const NODE_ENV = process.env.NODE_ENV || 'development'; - -const app = express(); - - -app.use(logger('dev')); -app.use(express.json()); -app.use(express.urlencoded({ extended: false })); -app.use(cookieParser()); -app.use(helmet()); -app.use(cors({ - origin: 'http://localhost:5173' -})); - -app.use('/api', apiRouter); - -app.use((_req, res) => { - res.status(404).json({ - error: 'Not Found', - endpoints: [ - '/api/events', - '/api/users' - ] - }); -}); - -app.listen(PORT, () => { - console.log(`\nServer listening at http://localhost:${PORT}`); - console.log(`Server running in ${NODE_ENV} mode\n`); -});
\ No newline at end of file diff --git a/backend/src/server.ts b/backend/src/server.ts new file mode 100644 index 0000000..8582347 --- /dev/null +++ b/backend/src/server.ts @@ -0,0 +1,37 @@ +import express from "express"; +import cookieParser from "cookie-parser"; +import logger from "morgan"; +import cors from "cors"; +import helmet from "helmet"; + +import apiRouter from "./routes/api.js"; + +const PORT = process.env.PORT || 3000; +const NODE_ENV = process.env.NODE_ENV || "development"; + +const app = express(); + +app.use(logger("dev")); +app.use(express.json()); +app.use(express.urlencoded({ extended: false })); +app.use(cookieParser()); +app.use(helmet()); +app.use( + cors({ + origin: "http://localhost:5173", + }), +); + +app.use("/api", apiRouter); + +app.use((_req, res) => { + res.status(404).json({ + error: "Not Found", + endpoints: ["/api/events", "/api/users"], + }); +}); + +app.listen(PORT, () => { + console.log(`\nServer listening at http://localhost:${PORT}`); + console.log(`Server running in ${NODE_ENV} mode\n`); +}); diff --git a/backend/src/types.ts b/backend/src/types.ts new file mode 100644 index 0000000..5c8112c --- /dev/null +++ b/backend/src/types.ts @@ -0,0 +1,24 @@ +export type Id = number; + +export interface Event { + id: Id; + title: string; + description?: string; + date: Date; + host_id: number; + image_url?: string; + host: User; + rsvps: Rsvps[]; +} + +export interface User { + id: number; + name: string; + email: string; +} + +export interface Rsvps { + id: number; + name: string; + email: string; +} diff --git a/backend/tsconfig.json b/backend/tsconfig.json new file mode 100644 index 0000000..0373555 --- /dev/null +++ b/backend/tsconfig.json @@ -0,0 +1,46 @@ +{ + "extends": "@tsconfig/node-lts/tsconfig.json", + // Visit https://aka.ms/tsconfig to read more about this file + "compilerOptions": { + // File Layout + "rootDir": "./src", + "outDir": "./dist", + + // Environment Settings + // See also https://aka.ms/tsconfig/module + "module": "nodenext", + "target": "esnext", + "types": [ + "node" + ], + // For nodejs: + // "lib": ["esnext"], + // and npm install -D @types/node + + // Other Outputs + "sourceMap": true, + "declaration": true, + "declarationMap": true, + + // Stricter Typechecking Options + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + + // Style Options + // "noImplicitReturns": true, + // "noImplicitOverride": true, + // "noUnusedLocals": true, + // "noUnusedParameters": true, + // "noFallthroughCasesInSwitch": true, + // "noPropertyAccessFromIndexSignature": true, + + // Recommended Options + "strict": true, + "jsx": "react-jsx", + "verbatimModuleSyntax": true, + "isolatedModules": true, + "noUncheckedSideEffectImports": true, + "moduleDetection": "force", + "skipLibCheck": true, + } +} |
