prisma (#1)
migrate from knex to prisma on backend Co-authored-by: fred <> Reviewed-on: #1
This commit is contained in:
parent
24281a6322
commit
0a41568a2e
31 changed files with 1577 additions and 472 deletions
|
@ -1,154 +0,0 @@
|
|||
const express = require("express");
|
||||
const db = require("./db");
|
||||
const port = 3000;
|
||||
const cors = require("cors");
|
||||
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
// ####### ROUTES #######
|
||||
app.get("/backend/test", async (req, res) => {
|
||||
console.log("test");
|
||||
res.json({ env: process.env.NODE_ENV });
|
||||
});
|
||||
// ### GET ALL RECIPES ###
|
||||
app.get("/backend/recipes", async (req, res) => {
|
||||
try {
|
||||
const recipes = await db("recipes").select("id", "name", "cuisine", "stars", "prep_minutes", "cook_minutes");
|
||||
res.json(recipes);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
// ### GET ALL RECIPE_INGREDIENTS ###
|
||||
app.get("/backend/recipe-ingredients", async (req, res) => {
|
||||
try {
|
||||
const recipe_ingredients = await db("recipe_ingredients").select("*");
|
||||
res.json(recipe_ingredients);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
// ### GET ALL RECIPE_STEPS ###
|
||||
app.get("/backend/recipe-steps", async (req, res) => {
|
||||
try {
|
||||
const recipe_steps = await db("recipe_steps").select("*");
|
||||
res.json(recipe_steps);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ### GET RECIPE ###
|
||||
app.get("/backend/recipe/:id", async (req, res) => {
|
||||
const id = req.params.id;
|
||||
try {
|
||||
const recipeQuery = db("recipes")
|
||||
.where("id", "=", id)
|
||||
.select("id", "name", "author", "cuisine", "stars", "prep_minutes", "cook_minutes");
|
||||
|
||||
const ingredientsQuery = db
|
||||
.from("recipe_ingredients")
|
||||
.where("recipe_id", "=", id)
|
||||
.select("raw");
|
||||
|
||||
const stepsQuery = db("recipe_steps")
|
||||
.where("recipe_id", id)
|
||||
.select("step_number", "instruction");
|
||||
|
||||
const [recipe, ingredients, steps] = await Promise.all([
|
||||
recipeQuery,
|
||||
ingredientsQuery,
|
||||
stepsQuery,
|
||||
]);
|
||||
|
||||
const result = {
|
||||
details: recipe[0],
|
||||
ingredients: ingredients,
|
||||
steps: steps,
|
||||
};
|
||||
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ### ADD RECIPE ###
|
||||
app.post("/backend/add-recipe", async (req, res) => {
|
||||
if (process.env.NODE_ENV === 'demo') { return; };
|
||||
const { name, author, cuisine, stars, ingredients, steps, prep_minutes, cook_minutes } = req.body;
|
||||
try {
|
||||
const [id] = await db("recipes").insert(
|
||||
{
|
||||
name: name,
|
||||
author: author,
|
||||
cuisine: cuisine,
|
||||
prep_minutes: prep_minutes,
|
||||
cook_minutes: cook_minutes,
|
||||
stars: stars,
|
||||
},
|
||||
["id"],
|
||||
);
|
||||
|
||||
const ingredientInserts = ingredients.map((ing) => ({
|
||||
recipe_id: id.id,
|
||||
raw: ing,
|
||||
}));
|
||||
//
|
||||
await db("recipe_ingredients").insert(ingredientInserts);
|
||||
|
||||
const stepInserts = Object.keys(steps).map((stepNumber) => ({
|
||||
recipe_id: id.id,
|
||||
step_number: parseInt(stepNumber),
|
||||
instruction: steps[stepNumber],
|
||||
}));
|
||||
await db("recipe_steps").insert(stepInserts);
|
||||
|
||||
res.status(200).send({ message: "Recipe added", id: id.id });
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ### SET STARS ###
|
||||
app.post("/backend/set-stars", async (req, res) => {
|
||||
if (process.env.NODE_ENV === 'demo') { return; };
|
||||
const { id, stars } = req.body;
|
||||
try {
|
||||
await db("recipes").where({ id: id }).update({ stars: stars });
|
||||
res.status(200).send({ message: "stars updated" });
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ### DELETE RECIPE ###
|
||||
app.delete("/backend/delete-recipe", async (req, res) => {
|
||||
if (process.env.NODE_ENV === 'demo') { return; };
|
||||
const { id } = req.body;
|
||||
try {
|
||||
await db("recipe_steps").where({ recipe_id: id }).del();
|
||||
await db("recipe_ingredients").where({ recipe_id: id }).del();
|
||||
await db("recipes").where({ id: id }).del();
|
||||
res.status(200).send({ message: "Recipe deleted" });
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(port, () => console.log(`Server has started on port: ${port}`));
|
||||
|
||||
process.on("SIGINT", async () => {
|
||||
console.log("Closing database connection...");
|
||||
await db.destroy();
|
||||
process.exit(0);
|
||||
});
|
|
@ -1,10 +0,0 @@
|
|||
const knex = require('knex');
|
||||
const knexConfig = require('./knexfile.js');
|
||||
|
||||
const environment = process.env.NODE_ENV || 'dev';
|
||||
const config = knexConfig[environment];
|
||||
|
||||
const db = knex(config);
|
||||
|
||||
module.exports = db;
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
require("dotenv").config();
|
||||
|
||||
module.exports = {
|
||||
dev: {
|
||||
client: "postgresql",
|
||||
connection: {
|
||||
host: "db",
|
||||
port: 5432,
|
||||
database: process.env.DB_NAME,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
},
|
||||
pool: {
|
||||
min: 2,
|
||||
max: 10,
|
||||
},
|
||||
migrations: {
|
||||
tableName: "knex_migrations",
|
||||
directory: "./migrations",
|
||||
},
|
||||
},
|
||||
|
||||
demo: {
|
||||
client: "postgresql",
|
||||
connection: {
|
||||
host: "db",
|
||||
port: process.env.DB_PORT,
|
||||
database: process.env.DB_NAME,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
},
|
||||
pool: {
|
||||
min: 2,
|
||||
max: 10,
|
||||
},
|
||||
migrations: {
|
||||
tableName: "knex_migrations",
|
||||
directory: "./migrations",
|
||||
},
|
||||
},
|
||||
|
||||
production: {
|
||||
client: "postgresql",
|
||||
connection: {
|
||||
host: "db",
|
||||
port: process.env.DB_PORT,
|
||||
database: process.env.DB_NAME,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
},
|
||||
pool: {
|
||||
min: 2,
|
||||
max: 10,
|
||||
},
|
||||
migrations: {
|
||||
tableName: "knex_migrations",
|
||||
directory: "./migrations",
|
||||
},
|
||||
},
|
||||
};
|
|
@ -1,21 +0,0 @@
|
|||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.createTable('recipes', function (table) {
|
||||
table.increments('id').primary();
|
||||
table.string('name').notNullable().unique();
|
||||
table.string('cuisine').notNullable();
|
||||
table.timestamps(true, true);
|
||||
})
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.dropTable('users');
|
||||
};
|
|
@ -1,43 +0,0 @@
|
|||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.createTable('ingredients', function (table) {
|
||||
table.increments('id').primary();
|
||||
table.string('name').notNullable().unique();
|
||||
table.string('type');
|
||||
table.string('notes');
|
||||
table.timestamps(true, true);
|
||||
}).
|
||||
createTable('recipe_ingredients', function (table) {
|
||||
table.increments('id').primary();
|
||||
table.integer('recipe_id').notNullable();
|
||||
table.integer('ingredient_id').notNullable();
|
||||
table.string('quantity').notNullable();
|
||||
table.string('unit').notNullable();
|
||||
table.string('notes');
|
||||
table.index(['recipe_id']);
|
||||
table.index(['ingredient_id']);
|
||||
table.timestamps(true, true);
|
||||
}).
|
||||
createTable('recipe_steps', function (table) {
|
||||
table.increments('id').primary();
|
||||
table.integer('recipe_id');
|
||||
table.integer('step_number');
|
||||
table.string('instruction');
|
||||
table.unique(['recipe_id', 'step_number']);
|
||||
table.index(['recipe_id'])
|
||||
table.timestamps(true, true);
|
||||
})
|
||||
};
|
||||
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.dropTableIfExists('ingredients')
|
||||
.dropTableIfExists('recipe_ingredients')
|
||||
.dropTableIfExists('recipe_steps')
|
||||
};
|
|
@ -1,24 +0,0 @@
|
|||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.table('recipe_ingredients', table => {
|
||||
table.string('raw', 255).defaultTo('');
|
||||
table.integer('ingredient_id').nullable().alter();
|
||||
table.string('quantity').nullable().alter();
|
||||
table.string('unit').nullable().alter();
|
||||
}).table('recipe_steps', table => {
|
||||
table.string('instruction', 510).alter();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.table('recipe_ingredients', table => {
|
||||
table.dropColumn('raw');
|
||||
});
|
||||
};
|
|
@ -1,22 +0,0 @@
|
|||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.up = function (knex) {
|
||||
|
||||
return knex.schema.table('recipes', table => {
|
||||
table.string('author');
|
||||
table.integer('stars');
|
||||
})
|
||||
};
|
||||
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.table('recipes', table => {
|
||||
table.dropColumn('author');
|
||||
table.dropColumn('stars')
|
||||
})
|
||||
};
|
|
@ -1,22 +0,0 @@
|
|||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.up = function (knex) {
|
||||
|
||||
return knex.schema.table('recipes', table => {
|
||||
table.integer('prep_minutes');
|
||||
table.integer('cook_minutes');
|
||||
})
|
||||
};
|
||||
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.table('recipes', table => {
|
||||
table.dropColumn('prep_minutes');
|
||||
table.dropColumn('cook_minutes')
|
||||
})
|
||||
};
|
938
backend/package-lock.json
generated
938
backend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -3,19 +3,26 @@
|
|||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "node backendServer.js",
|
||||
"demo": "node backendServer.js",
|
||||
"production": "node backendServer.js"
|
||||
"dev": "nodemon ./src/index.js",
|
||||
"production": "node ./src/index.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"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"
|
||||
"pg": "^8.16.3",
|
||||
"prisma": "^6.14.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.9.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.1.10"
|
||||
}
|
||||
}
|
||||
|
|
93
backend/prisma/migrations/0_init/migration.sql
Normal file
93
backend/prisma/migrations/0_init/migration.sql
Normal 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");
|
||||
|
|
@ -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";
|
|
@ -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;
|
|
@ -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;
|
3
backend/prisma/migrations/migration_lock.toml
Normal file
3
backend/prisma/migrations/migration_lock.toml
Normal 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"
|
53
backend/prisma/schema.prisma
Normal file
53
backend/prisma/schema.prisma
Normal 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")
|
||||
}
|
89
backend/src/controllers/recipeController.js
Normal file
89
backend/src/controllers/recipeController.js
Normal 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.json({ success: "true" });
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
msg: "Failed to delete recipe",
|
||||
source: "recipeController",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
41
backend/src/index.js
Normal file
41
backend/src/index.js
Normal file
|
@ -0,0 +1,41 @@
|
|||
const express = require("express");
|
||||
const cors = require("cors");
|
||||
const { PrismaClient } = require("@prisma/client");
|
||||
const appRoutes = require("./routes/appRoutes");
|
||||
|
||||
const app = express();
|
||||
const port = process.env.PORT || 3000;
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
function setupMiddleware(app) {
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
app.use("/api", appRoutes);
|
||||
}
|
||||
|
||||
setupMiddleware(app);
|
||||
|
||||
// Start server
|
||||
async function startServer() {
|
||||
try {
|
||||
app.listen(port);
|
||||
console.log(`Server is running on http://localhost:${port}`);
|
||||
} catch (error) {
|
||||
console.error("Error starting the server:", error);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
startServer();
|
||||
|
||||
module.exports = { app, prisma };
|
134
backend/src/models/recipeModel.js
Normal file
134
backend/src/models/recipeModel.js
Normal file
|
@ -0,0 +1,134 @@
|
|||
const { PrismaClient } = require("@prisma/client");
|
||||
const Logger = require("../utils/logger.js");
|
||||
const logger = new Logger();
|
||||
|
||||
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) {
|
||||
logger.warn(`recipe with id ${id} cannot be found`);
|
||||
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_number: step.step_number,
|
||||
instruction: step.instruction,
|
||||
})),
|
||||
};
|
||||
return data;
|
||||
} catch (err) {
|
||||
console.log("Error finding recipe:", err);
|
||||
logger.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],
|
||||
})),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
logger.info("new recipe created", {
|
||||
id: createdRecipe.id,
|
||||
name: createdRecipe.name,
|
||||
});
|
||||
return createdRecipe;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
logger.error("error creating recipe", err);
|
||||
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);
|
||||
logger.error("error setting stars", err);
|
||||
throw new Error(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteRecipe(id) {
|
||||
try {
|
||||
await this.prisma.recipe_ingredients.deleteMany({
|
||||
where: { recipe_id: id },
|
||||
});
|
||||
|
||||
await this.prisma.recipe_steps.deleteMany({
|
||||
where: { recipe_id: id },
|
||||
});
|
||||
const deletedRecipe = await this.prisma.recipes.delete({
|
||||
where: { id },
|
||||
});
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
module.exports = recipeModel;
|
18
backend/src/routes/appRoutes.js
Normal file
18
backend/src/routes/appRoutes.js
Normal 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;
|
33
backend/src/utils/logger.js
Normal file
33
backend/src/utils/logger.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
const fs = require("fs");
|
||||
|
||||
class Logger {
|
||||
constructor(filePath) {
|
||||
this.filePath = "/logs/app.log";
|
||||
}
|
||||
|
||||
log(level, message, params) {
|
||||
const logEntry = {
|
||||
timestamp: new Date().toISOString(),
|
||||
level: level,
|
||||
message: message,
|
||||
params: params,
|
||||
};
|
||||
fs.appendFile(this.filePath, JSON.stringify(logEntry) + "\n", (err) => {
|
||||
if (err) throw err;
|
||||
});
|
||||
}
|
||||
|
||||
info(message, params = {}) {
|
||||
this.log("info", message, params);
|
||||
}
|
||||
|
||||
warn(message, params = {}) {
|
||||
this.log("warn", message, params);
|
||||
}
|
||||
|
||||
error(message, params = {}) {
|
||||
this.log("error", message, params);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Logger;
|
|
@ -1,14 +0,0 @@
|
|||
const db = require('./db');
|
||||
|
||||
async function testConnection() {
|
||||
try {
|
||||
await db.raw('SELECT 1+1 as result');
|
||||
console.log('Database connected successfully!');
|
||||
} catch (error) {
|
||||
console.error('Database connection failed:', error);
|
||||
} finally {
|
||||
await db.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
testConnection();
|
16
backend/tsconfig.json
Normal file
16
backend/tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue