refactor: migrate backend to NestJS
All checks were successful
/ build (push) Successful in 1m5s

This commit is contained in:
fred 2026-02-11 09:36:55 -08:00
parent 7258d283ed
commit 31f5bdc254
42 changed files with 21523 additions and 1040 deletions

View file

@ -0,0 +1,7 @@
import { Prisma } from '@prisma/client';
// also used for updateRecipe
export type CreateRecipeDto = Prisma.recipesCreateInput & {
recipeIngredients: Prisma.recipe_ingredientsCreateWithoutRecipesInput[];
recipeSteps: Prisma.recipe_stepsCreateWithoutRecipesInput[];
};

View file

@ -0,0 +1,20 @@
import { Test, TestingModule } from '@nestjs/testing';
import { RecipesController } from './recipes.controller';
import { RecipesService } from './recipes.service';
describe('RecipesController', () => {
let controller: RecipesController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [RecipesController],
providers: [RecipesService],
}).compile();
controller = module.get<RecipesController>(RecipesController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View file

@ -0,0 +1,45 @@
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
} from '@nestjs/common';
import { RecipesService } from './recipes.service';
import type { CreateRecipeDto } from './create-recipe.dto';
@Controller('recipe')
export class RecipesController {
constructor(private readonly recipeService: RecipesService) {}
@Post()
create(@Body() createRecipeDto: CreateRecipeDto) {
return this.recipeService.create(createRecipeDto);
}
@Get()
findAll() {
return this.recipeService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.recipeService.findOne(+id);
}
@Patch(':id')
update(@Param('id') id: string, @Body() updateRecipeDto: CreateRecipeDto) {
return this.recipeService.update(+id, updateRecipeDto);
}
@Patch(':id/stars')
setStars(@Param('id') id: string, @Body('stars') stars: number) {
return this.recipeService.setStars(+id, stars);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.recipeService.remove(+id);
}
}

View file

@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { RecipesService } from './recipes.service';
import { RecipesController } from './recipes.controller';
@Module({
controllers: [RecipesController],
providers: [RecipesService],
})
export class RecipesModule {}

View file

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { RecipesService } from './recipes.service';
describe('RecipesService', () => {
let service: RecipesService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [RecipesService],
}).compile();
service = module.get<RecipesService>(RecipesService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View file

@ -0,0 +1,222 @@
import { Injectable, Logger } from '@nestjs/common';
import { DatabaseService } from '../database/database.service';
import type { CreateRecipeDto } from './create-recipe.dto';
@Injectable()
export class RecipesService {
constructor(private readonly databaseService: DatabaseService) {}
async create(createRecipeDto: CreateRecipeDto) {
try {
const createdRecipe = await this.databaseService.recipes.create({
data: {
name: createRecipeDto.name,
author: createRecipeDto.author,
cuisine: createRecipeDto.cuisine,
stars: createRecipeDto.stars,
prep_minutes: createRecipeDto.prep_minutes,
cook_minutes: createRecipeDto.cook_minutes,
recipeIngredients: {
create: createRecipeDto.recipeIngredients.map((ingredient) => ({
raw: ingredient.raw,
})),
},
recipeSteps: {
create: createRecipeDto.recipeSteps || [],
},
},
});
Logger.log('New recipe created', {
id: createdRecipe.id,
name: createdRecipe.name,
});
return createdRecipe;
} catch (err) {
Logger.error('Error creating recipe', {
message: err instanceof Error ? err.message : 'Unknown error',
});
throw new Error('Failed to add recipe');
}
}
async findAll() {
try {
Logger.log('index page view');
return await this.databaseService.recipes.findMany({});
} catch (err) {
Logger.error('Error fetching all recipes', {
message: err instanceof Error ? err.message : 'Unknown error',
});
throw new Error(err instanceof Error ? err.message : 'Unknown error');
}
}
async findOne(id: number) {
try {
const recipe = await this.databaseService.recipes.findUnique({
where: { id },
include: { recipeSteps: true, recipeIngredients: true },
});
if (!recipe) {
Logger.warn(`Recipe with id ${id} cannot be found`);
return null;
}
const data = {
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,
recipeIngredients: recipe.recipeIngredients.map((ing) => ing.raw),
recipeSteps: recipe.recipeSteps.map((step) => ({
step_number: step.step_number ?? 0,
instruction: step.instruction ?? '',
})),
};
Logger.log('Recipe page view', {
recipe_id: data.id,
recipe_name: data.name,
});
return data;
} catch (err) {
Logger.error('Error finding recipe', {
message: err instanceof Error ? err.message : 'Unknown error',
});
throw new Error(err instanceof Error ? err.message : 'Unknown error');
}
}
async update(id: number, updateRecipeDto: CreateRecipeDto) {
try {
const updatedRecipe = await this.databaseService.$transaction(
async (prisma) => {
await prisma.recipe_ingredients.deleteMany({
where: { recipe_id: id },
});
await prisma.recipe_steps.deleteMany({
where: { recipe_id: id },
});
const recipeUpdate = await prisma.recipes.update({
where: { id },
data: {
name: updateRecipeDto.name,
author: updateRecipeDto.author,
cuisine: updateRecipeDto.cuisine,
stars: updateRecipeDto.stars,
prep_minutes: updateRecipeDto.prep_minutes,
cook_minutes: updateRecipeDto.cook_minutes,
updated_at: new Date(),
},
});
const ingredientsData = updateRecipeDto.recipeIngredients.map(
(ingredient) => ({
raw: ingredient.raw ?? '',
quantity: ingredient.quantity ?? null,
unit: ingredient.unit ?? null,
notes: ingredient.notes ?? null,
recipe_id: id,
}),
);
const stepsData = updateRecipeDto.recipeSteps.map((step) => ({
step_number: step.step_number ?? null,
instruction: step.instruction ?? '',
recipe_id: id,
}));
if (ingredientsData.length > 0) {
await prisma.recipe_ingredients.createMany({
data: ingredientsData,
skipDuplicates: true,
});
}
if (stepsData.length > 0) {
await prisma.recipe_steps.createMany({
data: stepsData,
skipDuplicates: true,
});
}
return recipeUpdate;
},
);
Logger.log('Updated Recipe', {
id: updatedRecipe.id,
name: updatedRecipe.name,
});
return updatedRecipe;
} catch (err) {
console.error('Update method caught error:', err);
Logger.error('Error updating recipe', {
message: err instanceof Error ? err.message : 'Unknown error',
stack: err instanceof Error ? err.stack : 'No stack available',
id,
updateRecipeDto,
});
throw new Error('Failed to update recipe');
}
}
private async deleteRecipeData(id: number) {
try {
await this.databaseService.recipe_ingredients.deleteMany({
where: { recipe_id: id },
});
await this.databaseService.recipe_steps.deleteMany({
where: { recipe_id: id },
});
Logger.log(`Recipe data deleted successfully for recipe id: ${id}`);
} catch (err) {
Logger.error('Error deleting recipe data', {
message: err instanceof Error ? err.message : 'Unknown error',
});
throw new Error(err instanceof Error ? err.message : 'Unknown error');
}
}
async setStars(id: number, stars: number) {
try {
await this.databaseService.recipes.update({
where: { id },
data: { stars },
});
return { message: 'Stars updated' };
} catch (err) {
Logger.error('Error setting stars', {
message: err instanceof Error ? err.message : 'Unknown error',
});
throw new Error(err instanceof Error ? err.message : 'Unknown error');
}
}
async remove(id: number) {
try {
await this.deleteRecipeData(id);
const deletedRecipe = await this.databaseService.recipes.delete({
where: { id },
});
Logger.log('Recipe deleted', {
id: deletedRecipe.id,
name: deletedRecipe.name,
});
return { message: 'Recipe deleted successfully' };
} catch (err) {
Logger.error('Error deleting recipe', {
message: err instanceof Error ? err.message : 'Unknown error',
});
throw new Error(err instanceof Error ? err.message : 'Unknown error');
}
}
}