add typescript to backend

This commit is contained in:
fred 2025-08-15 15:02:11 -07:00
parent 0a41568a2e
commit 30b28056de
10 changed files with 221 additions and 67 deletions

View file

@ -4,11 +4,10 @@ WORKDIR /usr/src/app
COPY package*.json ./ COPY package*.json ./
RUN npm install RUN if [ "$NODE_ENV" = "dev" ]; then npm install; else npm install --only=production; fi
COPY . . COPY . .
EXPOSE 3000 EXPOSE 3000
# CMD ["npm", "run", "dev"]
CMD npm run $NODE_ENV CMD npm run $NODE_ENV

View file

@ -21,6 +21,8 @@
"typescript": "^5.9.2" "typescript": "^5.9.2"
}, },
"devDependencies": { "devDependencies": {
"@types/cors": "^2.8.19",
"@types/express": "^5.0.3",
"nodemon": "^3.1.10" "nodemon": "^3.1.10"
} }
}, },
@ -170,6 +172,76 @@
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
"license": "MIT" "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": { "node_modules/@types/node": {
"version": "24.2.1", "version": "24.2.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz",
@ -179,6 +251,43 @@
"undici-types": "~7.10.0" "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": { "node_modules/accepts": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",

View file

@ -4,7 +4,8 @@
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"dev": "nodemon ./src/index.js", "dev": "nodemon ./src/index.js",
"production": "node ./src/index.js" "production": "tsc && node ./dist/index.js",
"demo": "tsc && node ./dist/index.js"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
@ -12,17 +13,19 @@
"description": "", "description": "",
"dependencies": { "dependencies": {
"@prisma/client": "^6.14.0", "@prisma/client": "^6.14.0",
"@types/node": "^24.2.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^17.0.1", "dotenv": "^17.0.1",
"express": "^5.1.0", "express": "^5.1.0",
"knex": "^3.1.0", "knex": "^3.1.0",
"pg": "^8.16.3", "pg": "^8.16.3",
"prisma": "^6.14.0", "prisma": "^6.14.0"
"ts-node": "^10.9.2",
"typescript": "^5.9.2"
}, },
"devDependencies": { "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" "nodemon": "^3.1.10"
} }
} }

View file

@ -1,12 +1,17 @@
const RecipeModel = require("../models/recipeModel"); import { Request, Response } from "express";
import RecipeModel from "../models/recipeModel";
const model = new RecipeModel(); const model = new RecipeModel();
exports.test = async (req, res) => { export const test = async (req: Request, res: Response): Promise<void> => {
console.log("test"); console.log("test");
res.json({ env: process.env.NODE_ENV }); res.json({ env: process.env.NODE_ENV });
}; };
exports.getAllRecipes = async (req, res) => { export const getAllRecipes = async (
req: Request,
res: Response,
): Promise<void> => {
try { try {
const recipes = await model.getAllRecipes(); const recipes = await model.getAllRecipes();
res.json(recipes); res.json(recipes);
@ -14,12 +19,15 @@ exports.getAllRecipes = async (req, res) => {
res.status(500).json({ res.status(500).json({
msg: "Failed to fetch all recipes", msg: "Failed to fetch all recipes",
source: "recipeController", 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<void> => {
const id = parseInt(req.params.id, 10); const id = parseInt(req.params.id, 10);
try { try {
const recipe = await model.findById(id); const recipe = await model.findById(id);
@ -32,12 +40,12 @@ exports.getRecipeById = async (req, res) => {
res.status(500).json({ res.status(500).json({
msg: "Failed to fetch recipe", msg: "Failed to fetch recipe",
source: "recipeController", 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<void> => {
if (process.env.NODE_ENV === "demo") { if (process.env.NODE_ENV === "demo") {
return; return;
} }
@ -48,30 +56,33 @@ exports.addRecipe = async (req, res) => {
res.status(500).json({ res.status(500).json({
msg: "Failed to add recipe", msg: "Failed to add recipe",
source: "recipeController", 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<void> => {
if (process.env.NODE_ENV === "demo") { if (process.env.NODE_ENV === "demo") {
return; return;
} }
const id = parseInt(req.body.id, 10); const id = parseInt(req.body.id, 10);
const stars = parseInt(req.body.stars, 10); const stars = parseInt(req.body.stars, 10);
try { try {
const createdRecipe = await model.setStars(id, stars); const updatedRecipe = await model.setStars(id, stars);
res.status(202).json(createdRecipe); res.status(202).json(updatedRecipe);
} catch (error) { } catch (error) {
res.status(500).json({ res.status(500).json({
msg: "Failed to set stars", msg: "Failed to set stars",
source: "recipeController", 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<void> => {
if (process.env.NODE_ENV === "demo") { if (process.env.NODE_ENV === "demo") {
return; return;
} }
@ -83,7 +94,16 @@ exports.deleteRecipe = async (req, res) => {
res.status(500).json({ res.status(500).json({
msg: "Failed to delete recipe", msg: "Failed to delete recipe",
source: "recipeController", source: "recipeController",
error: error.message, error: error instanceof Error ? error.message : "Unknown error",
}); });
} }
}; };
export default {
test,
getAllRecipes,
getRecipeById,
addRecipe,
setStars,
deleteRecipe,
};

View file

@ -1,18 +1,17 @@
const express = require("express"); import express, { Express } from "express";
const cors = require("cors"); import cors from "cors";
const { PrismaClient } = require("@prisma/client"); import { PrismaClient } from "@prisma/client";
const appRoutes = require("./routes/appRoutes"); import appRoutes from "./routes/appRoutes";
const app = express(); const app: Express = express();
const port = process.env.PORT || 3000; const port = process.env.PORT || 3000;
const prisma = new PrismaClient(); const prisma = new PrismaClient();
function setupMiddleware(app) { function setupMiddleware(app: Express) {
app.use(cors()); app.use(cors());
app.use(express.json()); app.use(express.json());
app.use("/api", appRoutes); app.use("/api", appRoutes);
} }
setupMiddleware(app); setupMiddleware(app);
// Start server // Start server

View file

@ -1,29 +1,32 @@
const { PrismaClient } = require("@prisma/client"); import { PrismaClient } from "@prisma/client";
const Logger = require("../utils/logger.js"); import Logger from "../utils/logger";
const logger = new Logger(); const logger = new Logger();
class recipeModel { class RecipeModel {
private prisma: PrismaClient;
constructor() { constructor() {
this.prisma = new PrismaClient(); this.prisma = new PrismaClient();
} }
async getAllRecipes() { async getAllRecipes(): Promise<any[]> {
try { try {
return await this.prisma.recipes.findMany(); return await this.prisma.recipes.findMany();
} catch (err) { } catch (err) {
console.error("Error fetching all recipies:", err); console.error("Error fetching all recipes:", err);
throw new Error(err.message); throw new Error(err instanceof Error ? err.message : "Unknown error");
} }
} }
async findById(id) { async findById(id: number): Promise<any | null> {
try { try {
const recipe = await this.prisma.recipes.findUnique({ const recipe = await this.prisma.recipes.findUnique({
where: { id }, where: { id },
include: { recipeSteps: true, recipeIngredients: true }, include: { recipeSteps: true, recipeIngredients: true },
}); });
if (!recipe) { if (!recipe) {
logger.warn(`recipe with id ${id} cannot be found`); logger.warn(`Recipe with id ${id} cannot be found`);
return null; return null;
} }
const data = { const data = {
@ -37,7 +40,7 @@ class recipeModel {
cook_minutes: recipe.cook_minutes, cook_minutes: recipe.cook_minutes,
}, },
ingredients: recipe.recipeIngredients.map((ing) => ing.raw), ingredients: recipe.recipeIngredients.map((ing) => ing.raw),
steps: recipe.recipeSteps.map((step, idx) => ({ steps: recipe.recipeSteps.map((step) => ({
step_number: step.step_number, step_number: step.step_number,
instruction: step.instruction, instruction: step.instruction,
})), })),
@ -45,11 +48,23 @@ class recipeModel {
return data; return data;
} catch (err) { } catch (err) {
console.log("Error finding recipe:", err); console.log("Error finding recipe:", err);
logger.error("error finding recipe", err); logger.error("Error finding recipe", {
throw new Error(err.message); 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<any> {
const { const {
name, name,
author, author,
@ -81,33 +96,37 @@ class recipeModel {
}, },
}); });
logger.info("new recipe created", { logger.info("New recipe created", {
id: createdRecipe.id, id: createdRecipe.id,
name: createdRecipe.name, name: createdRecipe.name,
}); });
return createdRecipe; return createdRecipe;
} catch (error) { } catch (err) {
console.log(error); console.log("Error creating recipe:", err);
logger.error("error creating recipe", err); logger.error("Error creating recipe", {
message: err instanceof Error ? err.message : "Unknown error",
});
throw new Error("Failed to add recipe"); throw new Error("Failed to add recipe");
} }
} }
async setStars(id, stars) { async setStars(id: number, stars: number): Promise<{ message: string }> {
try { try {
await this.prisma.recipes.update({ await this.prisma.recipes.update({
where: { id }, where: { id },
data: { stars }, data: { stars },
}); });
return { message: "stars updated" }; return { message: "Stars updated" };
} catch (err) { } catch (err) {
console.error("Error updating stars:", err); console.error("Error updating stars:", err);
logger.error("error setting stars", err); logger.error("Error setting stars", {
throw new Error(err.message); 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 { try {
await this.prisma.recipe_ingredients.deleteMany({ await this.prisma.recipe_ingredients.deleteMany({
where: { recipe_id: id }, where: { recipe_id: id },
@ -119,16 +138,19 @@ class recipeModel {
const deletedRecipe = await this.prisma.recipes.delete({ const deletedRecipe = await this.prisma.recipes.delete({
where: { id }, where: { id },
}); });
logger.info("recipe deleted", { logger.info("Recipe deleted", {
id: deletedRecipe.id, id: deletedRecipe.id,
name: deletedRecipe.name, name: deletedRecipe.name,
}); });
return { message: "Recipe deleted successfully" }; return { message: "Recipe deleted successfully" };
} catch (err) { } catch (err) {
console.error("Error deleting recipe:", err); console.error("Error deleting recipe:", err);
logger.error("error deleting recipe", err); logger.error("Error deleting recipe", {
throw new Error(err.message); message: err instanceof Error ? err.message : "Unknown error",
});
throw new Error(err instanceof Error ? err.message : "Unknown error");
} }
} }
} }
module.exports = recipeModel;
export default RecipeModel;

View file

@ -1,5 +1,5 @@
const express = require("express"); import express, { Router } from "express";
const recipeController = require("../controllers/recipeController"); import recipeController from "../controllers/recipeController";
const router = express.Router(); const router = express.Router();
@ -15,4 +15,4 @@ router.post("/set-stars", recipeController.setStars);
router.delete("/delete-recipe", recipeController.deleteRecipe); router.delete("/delete-recipe", recipeController.deleteRecipe);
module.exports = router; export default router;

View file

@ -1,11 +1,13 @@
const fs = require("fs"); import fs from "fs";
class Logger { class Logger {
constructor(filePath) { private filePath: string;
this.filePath = "/logs/app.log";
constructor(filePath: string = "/logs/app.log") {
this.filePath = filePath;
} }
log(level, message, params) { private log(level: string, message: string, params?: object) {
const logEntry = { const logEntry = {
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
level: level, level: level,
@ -17,17 +19,17 @@ class Logger {
}); });
} }
info(message, params = {}) { info(message: string, params: object = {}) {
this.log("info", message, params); this.log("info", message, params);
} }
warn(message, params = {}) { warn(message: string, params: object = {}) {
this.log("warn", message, params); this.log("warn", message, params);
} }
error(message, params = {}) { error(message: string, params: object = {}) {
this.log("error", message, params); this.log("error", message, params);
} }
} }
module.exports = Logger; export default Logger;

View file

@ -4,7 +4,7 @@ WORKDIR /usr/src/app
COPY package*.json ./ COPY package*.json ./
RUN npm install RUN if [ "$NODE_ENV" = "dev" ]; then npm install; else npm install --only=production; fi
COPY . . COPY . .

View file

@ -12,17 +12,17 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@types/node": "^24.2.0",
"autoprefixer": "^10.4.21",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-router-dom": "^7.6.3" "react-router-dom": "^7.6.3"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^24.2.0",
"@eslint/js": "^9.29.0", "@eslint/js": "^9.29.0",
"@types/react": "^19.1.8", "@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6", "@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.5.2", "@vitejs/plugin-react": "^4.5.2",
"autoprefixer": "^10.4.21",
"eslint": "^9.29.0", "eslint": "^9.29.0",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20", "eslint-plugin-react-refresh": "^0.4.20",