From 30b28056dec53ef04a4f4490c3137472dd28b7a2 Mon Sep 17 00:00:00 2001 From: fred Date: Fri, 15 Aug 2025 15:02:11 -0700 Subject: [PATCH] add typescript to backend --- backend/Dockerfile | 3 +- backend/package-lock.json | 109 ++++++++++++++++++ backend/package.json | 13 ++- ...ecipeController.js => recipeController.ts} | 48 +++++--- backend/src/{index.js => index.ts} | 13 +-- .../models/{recipeModel.js => recipeModel.ts} | 72 ++++++++---- .../src/routes/{appRoutes.js => appRoutes.ts} | 6 +- backend/src/utils/{logger.js => logger.ts} | 18 +-- frontend/Dockerfile | 2 +- frontend/package.json | 4 +- 10 files changed, 221 insertions(+), 67 deletions(-) rename backend/src/controllers/{recipeController.js => recipeController.ts} (57%) rename backend/src/{index.js => index.ts} (75%) rename backend/src/models/{recipeModel.js => recipeModel.ts} (58%) rename backend/src/routes/{appRoutes.js => appRoutes.ts} (74%) rename backend/src/utils/{logger.js => logger.ts} (53%) diff --git a/backend/Dockerfile b/backend/Dockerfile index bfa4239..1a9e971 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -4,11 +4,10 @@ WORKDIR /usr/src/app COPY package*.json ./ -RUN npm install +RUN if [ "$NODE_ENV" = "dev" ]; then npm install; else npm install --only=production; fi COPY . . EXPOSE 3000 -# CMD ["npm", "run", "dev"] CMD npm run $NODE_ENV diff --git a/backend/package-lock.json b/backend/package-lock.json index a1e6346..907a4dc 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -21,6 +21,8 @@ "typescript": "^5.9.2" }, "devDependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^5.0.3", "nodemon": "^3.1.10" } }, @@ -170,6 +172,76 @@ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "license": "MIT" }, + "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/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.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", + "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz", + "integrity": "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==", + "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/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "24.2.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz", @@ -179,6 +251,43 @@ "undici-types": "~7.10.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": "0.17.5", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", + "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", + "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", diff --git a/backend/package.json b/backend/package.json index ae27440..10f8b02 100644 --- a/backend/package.json +++ b/backend/package.json @@ -4,7 +4,8 @@ "main": "index.js", "scripts": { "dev": "nodemon ./src/index.js", - "production": "node ./src/index.js" + "production": "tsc && node ./dist/index.js", + "demo": "tsc && node ./dist/index.js" }, "keywords": [], "author": "", @@ -12,17 +13,19 @@ "description": "", "dependencies": { "@prisma/client": "^6.14.0", - "@types/node": "^24.2.1", "cors": "^2.8.5", "dotenv": "^17.0.1", "express": "^5.1.0", "knex": "^3.1.0", "pg": "^8.16.3", - "prisma": "^6.14.0", - "ts-node": "^10.9.2", - "typescript": "^5.9.2" + "prisma": "^6.14.0" }, "devDependencies": { + "typescript": "^5.9.2", + "ts-node": "^10.9.2", + "@types/node": "^24.2.1", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.3", "nodemon": "^3.1.10" } } diff --git a/backend/src/controllers/recipeController.js b/backend/src/controllers/recipeController.ts similarity index 57% rename from backend/src/controllers/recipeController.js rename to backend/src/controllers/recipeController.ts index be9284e..f0873a0 100644 --- a/backend/src/controllers/recipeController.js +++ b/backend/src/controllers/recipeController.ts @@ -1,12 +1,17 @@ -const RecipeModel = require("../models/recipeModel"); +import { Request, Response } from "express"; +import RecipeModel from "../models/recipeModel"; + const model = new RecipeModel(); -exports.test = async (req, res) => { +export const test = async (req: Request, res: Response): Promise => { console.log("test"); res.json({ env: process.env.NODE_ENV }); }; -exports.getAllRecipes = async (req, res) => { +export const getAllRecipes = async ( + req: Request, + res: Response, +): Promise => { try { const recipes = await model.getAllRecipes(); res.json(recipes); @@ -14,12 +19,15 @@ exports.getAllRecipes = async (req, res) => { res.status(500).json({ msg: "Failed to fetch all recipes", source: "recipeController", - error: error.message, + error: error instanceof Error ? error.message : "Unknown error", }); } }; -exports.getRecipeById = async (req, res) => { +export const getRecipeById = async ( + req: Request, + res: Response, +): Promise => { const id = parseInt(req.params.id, 10); try { const recipe = await model.findById(id); @@ -32,12 +40,12 @@ exports.getRecipeById = async (req, res) => { res.status(500).json({ msg: "Failed to fetch recipe", source: "recipeController", - error: error.message, + error: error instanceof Error ? error.message : "Unknown error", }); } }; -exports.addRecipe = async (req, res) => { +export const addRecipe = async (req: Request, res: Response): Promise => { if (process.env.NODE_ENV === "demo") { return; } @@ -48,30 +56,33 @@ exports.addRecipe = async (req, res) => { res.status(500).json({ msg: "Failed to add recipe", source: "recipeController", - error: error.message, + error: error instanceof Error ? error.message : "Unknown error", }); } }; -exports.setStars = async (req, res) => { +export const setStars = async (req: Request, res: Response): Promise => { if (process.env.NODE_ENV === "demo") { return; } const id = parseInt(req.body.id, 10); const stars = parseInt(req.body.stars, 10); try { - const createdRecipe = await model.setStars(id, stars); - res.status(202).json(createdRecipe); + const updatedRecipe = await model.setStars(id, stars); + res.status(202).json(updatedRecipe); } catch (error) { res.status(500).json({ msg: "Failed to set stars", source: "recipeController", - error: error.message, + error: error instanceof Error ? error.message : "Unknown error", }); } }; -exports.deleteRecipe = async (req, res) => { +export const deleteRecipe = async ( + req: Request, + res: Response, +): Promise => { if (process.env.NODE_ENV === "demo") { return; } @@ -83,7 +94,16 @@ exports.deleteRecipe = async (req, res) => { res.status(500).json({ msg: "Failed to delete recipe", source: "recipeController", - error: error.message, + error: error instanceof Error ? error.message : "Unknown error", }); } }; + +export default { + test, + getAllRecipes, + getRecipeById, + addRecipe, + setStars, + deleteRecipe, +}; diff --git a/backend/src/index.js b/backend/src/index.ts similarity index 75% rename from backend/src/index.js rename to backend/src/index.ts index 1b914be..b75a2ea 100644 --- a/backend/src/index.js +++ b/backend/src/index.ts @@ -1,18 +1,17 @@ -const express = require("express"); -const cors = require("cors"); -const { PrismaClient } = require("@prisma/client"); -const appRoutes = require("./routes/appRoutes"); +import express, { Express } from "express"; +import cors from "cors"; +import { PrismaClient } from "@prisma/client"; +import appRoutes from "./routes/appRoutes"; -const app = express(); +const app: Express = express(); const port = process.env.PORT || 3000; const prisma = new PrismaClient(); -function setupMiddleware(app) { +function setupMiddleware(app: Express) { app.use(cors()); app.use(express.json()); app.use("/api", appRoutes); } - setupMiddleware(app); // Start server diff --git a/backend/src/models/recipeModel.js b/backend/src/models/recipeModel.ts similarity index 58% rename from backend/src/models/recipeModel.js rename to backend/src/models/recipeModel.ts index 82a3159..f6256fa 100644 --- a/backend/src/models/recipeModel.js +++ b/backend/src/models/recipeModel.ts @@ -1,29 +1,32 @@ -const { PrismaClient } = require("@prisma/client"); -const Logger = require("../utils/logger.js"); +import { PrismaClient } from "@prisma/client"; +import Logger from "../utils/logger"; + const logger = new Logger(); -class recipeModel { +class RecipeModel { + private prisma: PrismaClient; + constructor() { this.prisma = new PrismaClient(); } - async getAllRecipes() { + async getAllRecipes(): Promise { try { return await this.prisma.recipes.findMany(); } catch (err) { - console.error("Error fetching all recipies:", err); - throw new Error(err.message); + console.error("Error fetching all recipes:", err); + throw new Error(err instanceof Error ? err.message : "Unknown error"); } } - async findById(id) { + async findById(id: number): Promise { try { const recipe = await this.prisma.recipes.findUnique({ where: { id }, include: { recipeSteps: true, recipeIngredients: true }, }); if (!recipe) { - logger.warn(`recipe with id ${id} cannot be found`); + logger.warn(`Recipe with id ${id} cannot be found`); return null; } const data = { @@ -37,7 +40,7 @@ class recipeModel { cook_minutes: recipe.cook_minutes, }, ingredients: recipe.recipeIngredients.map((ing) => ing.raw), - steps: recipe.recipeSteps.map((step, idx) => ({ + steps: recipe.recipeSteps.map((step) => ({ step_number: step.step_number, instruction: step.instruction, })), @@ -45,11 +48,23 @@ class recipeModel { return data; } catch (err) { console.log("Error finding recipe:", err); - logger.error("error finding recipe", err); - throw new Error(err.message); + logger.error("Error finding recipe", { + message: err instanceof Error ? err.message : "Unknown error", + }); + throw new Error(err instanceof Error ? err.message : "Unknown error"); } } - async addRecipe(recipeData) { + + async addRecipe(recipeData: { + name: string; + author: string; + cuisine: string; + stars: number; + ingredients: string[]; + steps: { [key: string]: string }; + prep_minutes: number; + cook_minutes: number; + }): Promise { const { name, author, @@ -81,33 +96,37 @@ class recipeModel { }, }); - logger.info("new recipe created", { + logger.info("New recipe created", { id: createdRecipe.id, name: createdRecipe.name, }); return createdRecipe; - } catch (error) { - console.log(error); - logger.error("error creating recipe", err); + } catch (err) { + console.log("Error creating recipe:", err); + logger.error("Error creating recipe", { + message: err instanceof Error ? err.message : "Unknown error", + }); throw new Error("Failed to add recipe"); } } - async setStars(id, stars) { + async setStars(id: number, stars: number): Promise<{ message: string }> { try { await this.prisma.recipes.update({ where: { id }, data: { stars }, }); - return { message: "stars updated" }; + return { message: "Stars updated" }; } catch (err) { console.error("Error updating stars:", err); - logger.error("error setting stars", err); - throw new Error(err.message); + logger.error("Error setting stars", { + message: err instanceof Error ? err.message : "Unknown error", + }); + throw new Error(err instanceof Error ? err.message : "Unknown error"); } } - async deleteRecipe(id) { + async deleteRecipe(id: number): Promise<{ message: string }> { try { await this.prisma.recipe_ingredients.deleteMany({ where: { recipe_id: id }, @@ -119,16 +138,19 @@ class recipeModel { const deletedRecipe = await this.prisma.recipes.delete({ where: { id }, }); - logger.info("recipe deleted", { + logger.info("Recipe deleted", { id: deletedRecipe.id, name: deletedRecipe.name, }); return { message: "Recipe deleted successfully" }; } catch (err) { console.error("Error deleting recipe:", err); - logger.error("error deleting recipe", err); - throw new Error(err.message); + logger.error("Error deleting recipe", { + message: err instanceof Error ? err.message : "Unknown error", + }); + throw new Error(err instanceof Error ? err.message : "Unknown error"); } } } -module.exports = recipeModel; + +export default RecipeModel; diff --git a/backend/src/routes/appRoutes.js b/backend/src/routes/appRoutes.ts similarity index 74% rename from backend/src/routes/appRoutes.js rename to backend/src/routes/appRoutes.ts index cbac0a0..5888abe 100644 --- a/backend/src/routes/appRoutes.js +++ b/backend/src/routes/appRoutes.ts @@ -1,5 +1,5 @@ -const express = require("express"); -const recipeController = require("../controllers/recipeController"); +import express, { Router } from "express"; +import recipeController from "../controllers/recipeController"; const router = express.Router(); @@ -15,4 +15,4 @@ router.post("/set-stars", recipeController.setStars); router.delete("/delete-recipe", recipeController.deleteRecipe); -module.exports = router; +export default router; diff --git a/backend/src/utils/logger.js b/backend/src/utils/logger.ts similarity index 53% rename from backend/src/utils/logger.js rename to backend/src/utils/logger.ts index 6c2b66a..425986d 100644 --- a/backend/src/utils/logger.js +++ b/backend/src/utils/logger.ts @@ -1,11 +1,13 @@ -const fs = require("fs"); +import fs from "fs"; class Logger { - constructor(filePath) { - this.filePath = "/logs/app.log"; + private filePath: string; + + constructor(filePath: string = "/logs/app.log") { + this.filePath = filePath; } - log(level, message, params) { + private log(level: string, message: string, params?: object) { const logEntry = { timestamp: new Date().toISOString(), level: level, @@ -17,17 +19,17 @@ class Logger { }); } - info(message, params = {}) { + info(message: string, params: object = {}) { this.log("info", message, params); } - warn(message, params = {}) { + warn(message: string, params: object = {}) { this.log("warn", message, params); } - error(message, params = {}) { + error(message: string, params: object = {}) { this.log("error", message, params); } } -module.exports = Logger; +export default Logger; diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 571a3b1..1fe85ae 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -4,7 +4,7 @@ WORKDIR /usr/src/app COPY package*.json ./ -RUN npm install +RUN if [ "$NODE_ENV" = "dev" ]; then npm install; else npm install --only=production; fi COPY . . diff --git a/frontend/package.json b/frontend/package.json index 72999ad..b459c0d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,17 +12,17 @@ "preview": "vite preview" }, "dependencies": { - "@types/node": "^24.2.0", - "autoprefixer": "^10.4.21", "react": "^19.1.0", "react-dom": "^19.1.0", "react-router-dom": "^7.6.3" }, "devDependencies": { + "@types/node": "^24.2.0", "@eslint/js": "^9.29.0", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@vitejs/plugin-react": "^4.5.2", + "autoprefixer": "^10.4.21", "eslint": "^9.29.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20",