checkpoint

This commit is contained in:
fred 2025-08-14 13:05:57 -07:00
parent 24281a6322
commit 5dc89497c6
16 changed files with 1427 additions and 15 deletions

File diff suppressed because it is too large Load diff

View file

@ -3,19 +3,26 @@
"version": "1.0.0", "version": "1.0.0",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"dev": "node backendServer.js", "dev": "nodemon ./src/index.js",
"demo": "node backendServer.js", "production": "node ./src/index.js"
"production": "node backendServer.js"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"description": "", "description": "",
"dependencies": { "dependencies": {
"@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",
"ts-node": "^10.9.2",
"typescript": "^5.9.2"
},
"devDependencies": {
"nodemon": "^3.1.10"
} }
} }

View file

@ -0,0 +1,93 @@
-- CreateSchema
CREATE SCHEMA IF NOT EXISTS "public";
-- CreateTable
CREATE TABLE "public"."ingredients" (
"id" SERIAL NOT NULL,
"name" VARCHAR(255) NOT NULL,
"type" VARCHAR(255),
"notes" VARCHAR(255),
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ingredients_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "public"."knex_migrations" (
"id" SERIAL NOT NULL,
"name" VARCHAR(255),
"batch" INTEGER,
"migration_time" TIMESTAMPTZ(6),
CONSTRAINT "knex_migrations_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "public"."knex_migrations_lock" (
"index" SERIAL NOT NULL,
"is_locked" INTEGER,
CONSTRAINT "knex_migrations_lock_pkey" PRIMARY KEY ("index")
);
-- CreateTable
CREATE TABLE "public"."recipe_ingredients" (
"id" SERIAL NOT NULL,
"recipe_id" INTEGER NOT NULL,
"ingredient_id" INTEGER,
"quantity" VARCHAR(255),
"unit" VARCHAR(255),
"notes" VARCHAR(255),
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"raw" VARCHAR(255) DEFAULT '',
CONSTRAINT "recipe_ingredients_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "public"."recipe_steps" (
"id" SERIAL NOT NULL,
"recipe_id" INTEGER,
"step_number" INTEGER,
"instruction" VARCHAR(510),
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "recipe_steps_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "public"."recipes" (
"id" SERIAL NOT NULL,
"name" VARCHAR(255) NOT NULL,
"cuisine" VARCHAR(255) NOT NULL,
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"author" VARCHAR(255),
"stars" INTEGER,
"prep_minutes" INTEGER,
"cook_minutes" INTEGER,
CONSTRAINT "recipes_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "ingredients_name_unique" ON "public"."ingredients"("name");
-- CreateIndex
CREATE INDEX "recipe_ingredients_ingredient_id_index" ON "public"."recipe_ingredients"("ingredient_id");
-- CreateIndex
CREATE INDEX "recipe_ingredients_recipe_id_index" ON "public"."recipe_ingredients"("recipe_id");
-- CreateIndex
CREATE INDEX "recipe_steps_recipe_id_index" ON "public"."recipe_steps"("recipe_id");
-- CreateIndex
CREATE UNIQUE INDEX "recipe_steps_recipe_id_step_number_unique" ON "public"."recipe_steps"("recipe_id", "step_number");
-- CreateIndex
CREATE UNIQUE INDEX "recipes_name_unique" ON "public"."recipes"("name");

View file

@ -0,0 +1,12 @@
/*
Warnings:
- You are about to drop the `knex_migrations` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `knex_migrations_lock` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropTable
DROP TABLE "public"."knex_migrations";
-- DropTable
DROP TABLE "public"."knex_migrations_lock";

View file

@ -0,0 +1,20 @@
/*
Warnings:
- You are about to drop the `ingredients` table. If the table is not empty, all the data it contains will be lost.
*/
-- AlterTable
ALTER TABLE "public"."recipe_ingredients" ADD COLUMN "recipesId" INTEGER;
-- AlterTable
ALTER TABLE "public"."recipe_steps" ADD COLUMN "recipesId" INTEGER;
-- DropTable
DROP TABLE "public"."ingredients";
-- AddForeignKey
ALTER TABLE "public"."recipe_ingredients" ADD CONSTRAINT "recipe_ingredients_recipesId_fkey" FOREIGN KEY ("recipesId") REFERENCES "public"."recipes"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "public"."recipe_steps" ADD CONSTRAINT "recipe_steps_recipesId_fkey" FOREIGN KEY ("recipesId") REFERENCES "public"."recipes"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View file

@ -0,0 +1,11 @@
-- DropForeignKey
ALTER TABLE "public"."recipe_ingredients" DROP CONSTRAINT "recipe_ingredients_recipesId_fkey";
-- DropForeignKey
ALTER TABLE "public"."recipe_steps" DROP CONSTRAINT "recipe_steps_recipesId_fkey";
-- AddForeignKey
ALTER TABLE "public"."recipe_ingredients" ADD CONSTRAINT "recipe_ingredients_recipe_id_fkey" FOREIGN KEY ("recipe_id") REFERENCES "public"."recipes"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "public"."recipe_steps" ADD CONSTRAINT "recipe_steps_recipe_id_fkey" FOREIGN KEY ("recipe_id") REFERENCES "public"."recipes"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View file

@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"

View file

@ -0,0 +1,53 @@
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model recipes {
id Int @id @default(autoincrement())
name String @unique(map: "recipes_name_unique") @db.VarChar(255)
cuisine String @db.VarChar(255)
created_at DateTime @default(now()) @db.Timestamptz(6)
updated_at DateTime @default(now()) @db.Timestamptz(6)
author String? @db.VarChar(255)
stars Int?
prep_minutes Int?
cook_minutes Int?
recipeIngredients recipe_ingredients[]
recipeSteps recipe_steps[]
}
model recipe_ingredients {
id Int @id @default(autoincrement())
recipe_id Int
ingredient_id Int?
quantity String? @db.VarChar(255)
unit String? @db.VarChar(255)
notes String? @db.VarChar(255)
created_at DateTime @default(now()) @db.Timestamptz(6)
updated_at DateTime @default(now()) @db.Timestamptz(6)
raw String? @default("") @db.VarChar(255)
recipes recipes? @relation(fields: [recipe_id], references: [id])
recipesId Int?
@@index([ingredient_id], map: "recipe_ingredients_ingredient_id_index")
@@index([recipe_id], map: "recipe_ingredients_recipe_id_index")
}
model recipe_steps {
id Int @id @default(autoincrement())
recipe_id Int?
step_number Int?
instruction String? @db.VarChar(510)
created_at DateTime @default(now()) @db.Timestamptz(6)
updated_at DateTime @default(now()) @db.Timestamptz(6)
recipes recipes? @relation(fields: [recipe_id], references: [id])
recipesId Int?
@@unique([recipe_id, step_number], map: "recipe_steps_recipe_id_step_number_unique")
@@index([recipe_id], map: "recipe_steps_recipe_id_index")
}

View file

@ -0,0 +1,89 @@
const RecipeModel = require("../models/recipeModel");
const model = new RecipeModel();
exports.test = async (req, res) => {
console.log("test");
res.json({ env: process.env.NODE_ENV });
};
exports.getAllRecipes = async (req, res) => {
try {
const recipes = await model.getAllRecipes();
res.json(recipes);
} catch (error) {
res.status(500).json({
msg: "Failed to fetch all recipes",
source: "recipeController",
error: error.message,
});
}
};
exports.getRecipeById = async (req, res) => {
const id = parseInt(req.params.id, 10);
try {
const recipe = await model.findById(id);
if (recipe) {
res.json(recipe);
} else {
res.status(404).json({ msg: "Recipe not found" });
}
} catch (error) {
res.status(500).json({
msg: "Failed to fetch recipe",
source: "recipeController",
error: error.message,
});
}
};
exports.addRecipe = async (req, res) => {
if (process.env.NODE_ENV === "demo") {
return;
}
try {
const createdRecipe = await model.addRecipe(req.body);
res.status(201).json(createdRecipe);
} catch (error) {
res.status(500).json({
msg: "Failed to add recipe",
source: "recipeController",
error: error.message,
});
}
};
exports.setStars = async (req, res) => {
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);
} catch (error) {
res.status(500).json({
msg: "Failed to set stars",
source: "recipeController",
error: error.message,
});
}
};
exports.deleteRecipe = async (req, res) => {
if (process.env.NODE_ENV === "demo") {
return;
}
const id = parseInt(req.body.id, 10);
try {
await model.deleteRecipe(id);
res.status(204).send();
} catch (error) {
res.status(500).json({
msg: "Failed to delete recipe",
source: "recipeController",
error: error.message,
});
}
};

33
backend/src/index.js Normal file
View file

@ -0,0 +1,33 @@
const express = require("express");
const { PrismaClient } = require("@prisma/client");
const appRoutes = require("./routes/appRoutes");
const app = express();
const cors = require("cors");
const port = 3000;
// Middleware
app.use(cors());
app.use(express.json());
// Routes
app.use("/api", appRoutes);
// Prisma client initialization
const prisma = new PrismaClient();
module.exports = { app, prisma };
app.listen(port, () => {
console.log(`Server is running on http://localhost:${port}`);
});
process.on("SIGINT", async () => {
try {
await prisma.$disconnect();
console.log("Prisma client disconnected");
process.exit(0);
} catch (error) {
console.error("Error disconnecting Prisma client:", error);
process.exit(1);
}
});

View file

@ -0,0 +1 @@
// todo

View file

@ -0,0 +1,119 @@
const { PrismaClient } = require("@prisma/client");
class recipeModel {
constructor() {
this.prisma = new PrismaClient();
}
async getAllRecipes() {
try {
return await this.prisma.recipes.findMany();
} catch (err) {
console.error("Error fetching all recipies:", err);
throw new Error(err.message);
}
}
async findById(id) {
try {
const recipe = await this.prisma.recipes.findUnique({
where: { id },
include: { recipeSteps: true, recipeIngredients: true },
});
if (!recipe) {
return null;
}
const data = {
details: {
id: recipe.id,
name: recipe.name,
author: recipe.author,
cuisine: recipe.cuisine,
stars: recipe.stars,
prep_minutes: recipe.prep_minutes,
cook_minutes: recipe.cook_minutes,
},
ingredients: recipe.recipeIngredients.map((ing) => ing.raw),
steps: recipe.recipeSteps.map((step, idx) => ({
step_idx: step.step_number,
instruction: step.instruction,
})),
};
return data;
} catch (err) {
console.error("Error finding recipe:", err);
throw new Error(err.message);
}
}
async addRecipe(recipeData) {
const {
name,
author,
cuisine,
stars,
ingredients,
steps,
prep_minutes,
cook_minutes,
} = recipeData;
try {
const createdRecipe = await this.prisma.recipes.create({
data: {
name,
author,
cuisine,
prep_minutes,
cook_minutes,
stars,
recipeIngredients: {
create: ingredients.map((ing) => ({ raw: ing })),
},
recipeSteps: {
create: Object.keys(steps).map((stepNumber) => ({
step_number: parseInt(stepNumber),
instruction: steps[stepNumber],
})),
},
},
});
return createdRecipe;
} catch (error) {
console.log(error);
throw new Error("Failed to add recipe");
}
}
async setStars(id, stars) {
try {
await this.prisma.recipes.update({
where: { id },
data: { stars },
});
return { message: "stars updated" };
} catch (err) {
console.error("Error updating stars:", err);
throw new Error(err.message);
}
}
async deleteRecipe(id) {
try {
await this.prisma.recipe_ingredients.deleteMany({
where: { recipe_id: id }, // Ensure you have the right foreign key relation
});
await this.prisma.recipe_steps.deleteMany({
where: { recipe_id: id }, // Ensure you have the right foreign key relation
});
const deletedRecipe = await this.prisma.recipes.delete({
where: { id },
});
return { message: "Recipe deleted successfully" };
} catch (err) {
console.error("Error deleting recipe:", err);
throw new Error(err.message);
}
}
}
module.exports = recipeModel;

View file

@ -0,0 +1,18 @@
const express = require("express");
const recipeController = require("../controllers/recipeController");
const router = express.Router();
router.get("/test", recipeController.test);
router.get("/recipes", recipeController.getAllRecipes);
router.get("/recipe/:id", recipeController.getRecipeById);
router.post("/add-recipe", recipeController.addRecipe);
router.post("/set-stars", recipeController.setStars);
router.delete("/delete-recipe", recipeController.deleteRecipe);
module.exports = router;

View file

@ -0,0 +1 @@
// todo

16
backend/tsconfig.json Normal file
View file

@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"moduleResolution": "node",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"outDir": "dist"
},
"include": [
"src",
"scripts"
]
}

View file

@ -1,27 +1,27 @@
const baseUrl = process.env.NODE_ENV !== 'dev' const baseUrl =
? '/' process.env.NODE_ENV !== "dev" ? "/api" : "http://localhost:3000/api";
: 'http://localhost:3000/';
export const getRecipes = async () => { export const getRecipes = async () => {
const response = await fetch(`${baseUrl}backend/recipes`); console.log(`${baseUrl}/recipes`);
const response = await fetch(`${baseUrl}/recipes`);
const data = await response.json(); const data = await response.json();
return data; return data;
}; };
export const getRecipeSteps = async () => { export const getRecipeSteps = async () => {
const response = await fetch(`${baseUrl}backend/recipe-steps`); const response = await fetch(`${baseUrl}/recipe-steps`);
const data = await response.json(); const data = await response.json();
return data; return data;
}; };
export const getRecipeIngredients = async () => { export const getRecipeIngredients = async () => {
const response = await fetch(`${baseUrl}backend/recipe-ingredients`); const response = await fetch(`${baseUrl}/recipe-ingredients`);
const data = await response.json(); const data = await response.json();
return data; return data;
}; };
export const getRecipeById = async (id) => { export const getRecipeById = async (id) => {
const response = await fetch(`${baseUrl}backend/recipe/${id}`); const response = await fetch(`${baseUrl}/recipe/${id}`);
const data = await response.json(); const data = await response.json();
if (!data || !data.details) { if (!data || !data.details) {
return { details: null }; return { details: null };
@ -32,7 +32,7 @@ export const getRecipeById = async (id) => {
export const addRecipe = async (recipeData) => { export const addRecipe = async (recipeData) => {
console.log(JSON.stringify(recipeData)); console.log(JSON.stringify(recipeData));
// return // return
const response = await fetch(`${baseUrl}backend/add-recipe`, { const response = await fetch(`${baseUrl}/add-recipe`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(recipeData), body: JSON.stringify(recipeData),
@ -45,7 +45,7 @@ export const addRecipe = async (recipeData) => {
export const setDBStars = async (id, stars) => { export const setDBStars = async (id, stars) => {
console.log(JSON.stringify({ id: id, stars: stars })); console.log(JSON.stringify({ id: id, stars: stars }));
// return // return
const response = await fetch(`${baseUrl}backend/set-stars`, { const response = await fetch(`${baseUrl}/set-stars`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id: id, stars: stars }), body: JSON.stringify({ id: id, stars: stars }),
@ -58,7 +58,7 @@ export const setDBStars = async (id, stars) => {
export const deleteRecipe = async (id) => { export const deleteRecipe = async (id) => {
console.log(id); console.log(id);
// return // return
const response = await fetch(`${baseUrl}backend/delete-recipe`, { const response = await fetch(`${baseUrl}/delete-recipe`, {
method: "DELETE", method: "DELETE",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id }), body: JSON.stringify({ id }),