Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions app/api/meal-plan-generator/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,18 +66,18 @@ function validateBody(body: PlanGeneratorFormInput) {

async function mealsByMealTypeAndTagIds(mealType: MealType, tagIds: number[]) {
return await prisma.meal.findMany({
where: {
where: {
tags: { some: { id: { in: tagIds } } },
suggestedMealType: { has: mealType }
},
include: { tags: true }
});
}

async function mealsByTagIds(tagIds: number[]) {
async function mealsByMealType(mealType: MealType) {
return await prisma.meal.findMany({
where: {
tags: { some: { id: { in: tagIds } } }
where: {
suggestedMealType: { has: mealType }
},
include: { tags: true }
});
Expand Down Expand Up @@ -129,22 +129,22 @@ export async function generateMealPlan(body: PlanGeneratorFormInput, userId: str
const failedToCreateObject = await buildFailedToCreateObject(tagIds, parsedDate, mealType)
failedToCreate.push(failedToCreateObject)

const mealsByTags = await mealsByTagIds(tagIds)
const mealsByType = await mealsByMealType(mealType)

return findBestMeal(mealsByTags, tagIds)
return findBestMeal(mealsByType, tagIds)
}
else {
return findBestMeal(mealsWithMealType, tagIds)
}
})()

if (meal === undefined) {
continue
continue
}

const mealPlan = {
date: parsedDate,
type: mealType,
type: mealType,
mealId: meal.id,
userId
};
Expand Down
2 changes: 1 addition & 1 deletion app/api/meals/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export async function POST(request: Request) {
name: body.name,
description: body.description || null,
ingredients: body.ingredients,
recipe: body.recipe || null,
recipe: body.recipe || [],
suggestedMealType: body.suggestedMealType,
userId: session.user.id,
tags: {
Expand Down
6 changes: 6 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ const eslintConfig = defineConfig([
"build/**",
"next-env.d.ts",
]),
{
files: ["test/**/*.ts", "test/**/*.tsx"],
rules: {
"@typescript-eslint/no-explicit-any": "off",
},
},
]);

export default eslintConfig;
18 changes: 18 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"dotenv-cli": "^11.0.0",
"eslint": "^9",
"eslint-config-next": "16.0.1",
"fishery": "^2.4.0",
"jsdom": "^26.0.0",
"prisma": "^6.19.0",
"tsx": "^4.20.6",
Expand Down
4 changes: 2 additions & 2 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ model Meal {
mealPlans MealPlan[]
suggestedMealType MealType[]
tags Tag[] @relation("MealTags")
userId String?
userId String?
user User? @relation(fields: [userId], references: [id])
}

Expand Down Expand Up @@ -52,7 +52,7 @@ model User {
createdAt DateTime @default(now())
mealPlans MealPlan[] //one to many
preferences UserPreference[]
meals Meal[] // user-created meals
meals Meal[] // user-created meals
}

enum MealType {
Expand Down
34 changes: 34 additions & 0 deletions test/api/meal-plan-generator/services/find_best_meal.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { describe, it, expect } from "vitest"
import { findBestMeal } from "@/app/api/meal-plan-generator/services"
import tagFactory from "@/test/factories/tag_factory"
import mealFactory from "@/test/factories/meal_factory"

describe("findBestMeal", () => {
it('finds the meal with the most matched tags', async () => {
const vegetarianTag = await tagFactory.vegetarian().create()
const noGlutenTag = await tagFactory.noGluten().create()
const fruitsTag = await tagFactory.fruits().create()
const meatTag = await tagFactory.meat().create()
const veganTag = await tagFactory.vegan().create()
const quickTag = await tagFactory.quick().create()

const meal1 = await mealFactory.create({
name: "Owsianka z Owocami",
tags: [vegetarianTag, fruitsTag, quickTag, noGlutenTag]
})
const meal2 = await mealFactory.create({
name: "Kotlet schabowy",
tags: [meatTag]
})
const meal3 = await mealFactory.create({
name: "Makaron bez niczego",
tags: [veganTag]
})

const meals = [meal1, meal2, meal3]
const searchedTags = [vegetarianTag.id, quickTag.id, veganTag.id]
const bestMeal = findBestMeal(meals, searchedTags)

expect(bestMeal.name).toBe("Owsianka z Owocami")
})
})
78 changes: 78 additions & 0 deletions test/api/meal-plan-generator/services/generate_meal_plan.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { describe, it, expect, beforeEach } from "vitest"
import prisma from "@/lib/prisma"
import { generateMealPlan } from "@/app/api/meal-plan-generator/services"
import tagFactory from "@/test/factories/tag_factory"
import mealFactory, { MealWithTags } from "@/test/factories/meal_factory"
import { MealType } from "@/app/generated/prisma/enums"
import userFactory from "@/test/factories/user_factory"
import { type Tag, type User, type MealPlan } from "@/app/generated/prisma/client"

describe("generateMealPlan", () => {
let user: User
let noGlutenTag: Tag
let fruitsTag: Tag
let meatTag: Tag
let quickTag: Tag
let dessert: MealWithTags
let lunch: MealWithTags

beforeEach(async() => {
user = await userFactory.create()
noGlutenTag = await tagFactory.noGluten().create()
fruitsTag = await tagFactory.fruits().create()
meatTag = await tagFactory.meat().create()
quickTag = await tagFactory.quick().create()
dessert = await mealFactory.create({
name: "Deser owocowy",
suggestedMealType: [MealType.DESSERT],
tags: [noGlutenTag, fruitsTag]
})
lunch = await mealFactory.create({
name: "Schabowy",
suggestedMealType: [MealType.LUNCH],
tags: [meatTag, quickTag]
})
})

it("Generates meal plan with meal with provided meal type and tags", async () => {
const requestBody = {
"2025-12-06": {
"BREAKFAST": [],
"LUNCH": [meatTag, quickTag].map((tag) => tag.id),
"DESSERT": [],
"DINNER": []
},
}

const { count, failedToCreate } = await generateMealPlan(requestBody, user.id)

expect(count).toEqual(1)
expect(failedToCreate.length).toEqual(0)

const mealPlan = await prisma.mealPlan.findFirst() as MealPlan
expect(mealPlan.type).toEqual(MealType.LUNCH)
expect(mealPlan.mealId).toEqual(lunch.id)
expect(mealPlan.date).toEqual(new Date("2025-12-06"))
})

it("Generates meal plan with random meal with provided meal type if no meal with type and tags is found", async () => {
const requestBody = {
"2025-12-06": {
"BREAKFAST": [],
"LUNCH": [],
"DESSERT": [meatTag, quickTag].map((tag) => tag.id),
"DINNER": []
},
}

const { count, failedToCreate } = await generateMealPlan(requestBody, user.id)

expect(count).toEqual(1)
expect(failedToCreate.length).toEqual(1)

const mealPlan = await prisma.mealPlan.findFirst() as MealPlan
expect(mealPlan.type).toEqual(MealType.DESSERT)
expect(mealPlan.mealId).toEqual(dessert.id)
expect(mealPlan.date).toEqual(new Date("2025-12-06"))
})
})
25 changes: 8 additions & 17 deletions test/api/meals.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import prisma from "@/lib/prisma";
import { createTag } from "../helpers/records_creator";
import tagFactory from "@/test/factories/tag_factory"
import userFactory from "@/test/factories/user_factory"

vi.mock("@/lib/auth", () => ({
getSession: vi.fn(),
Expand All @@ -15,22 +16,12 @@ describe("POST /api/meals", () => {
});

it("creates a meal owned by the current user and connects tags", async () => {
const userId = "user_1";

await prisma.user.create({
data: {
id: userId,
email: "user_1@example.com",
password: "hashed-password",
name: "User 1",
},
});

const tag1 = await createTag("Wegetariańskie");
const tag2 = await createTag("Szybkie");
const user = await userFactory.create()
const tag1 = await tagFactory.vegetarian().create()
const tag2 = await tagFactory.quick().create()

vi.mocked(getSession).mockResolvedValue({
user: { id: userId },
user: { id: user.id },
} as any);

const { POST } = await import("@/app/api/meals/route");
Expand All @@ -54,13 +45,13 @@ describe("POST /api/meals", () => {
const payload = await res.json();
expect(payload.ok).toBe(true);
expect(payload.meal.name).toBe("Owsianka");
expect(payload.meal.userId).toBe(userId);
expect(payload.meal.userId).toBe(user.id);
expect(payload.meal.tags.map((t: any) => t.id).sort()).toEqual(
[tag1.id, tag2.id].sort()
);

const created = await prisma.meal.findFirst({
where: { name: "Owsianka", userId },
where: { name: "Owsianka", userId: user.id },
include: { tags: true },
});

Expand Down
52 changes: 0 additions & 52 deletions test/api/services.test.ts

This file was deleted.

42 changes: 42 additions & 0 deletions test/factories/meal_factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import prisma from "@/lib/prisma"
import { Factory } from "fishery"
import { Prisma, type Meal, type Tag} from "@/app/generated/prisma/client"
import { JsonValue } from "@prisma/client/runtime/library"

export interface MealWithTags extends Meal {
tags: Tag[];
}

class MealFactory extends Factory<MealWithTags> { }

const mealFactory = MealFactory.define(({ sequence, params, onCreate }) => {
onCreate((meal) => {
const tags = meal.tags || []

return prisma.meal.create({
data: {
...meal,
id: undefined,
ingredients: (meal.ingredients ?? {}) as Prisma.InputJsonValue,
recipe: (meal.recipe ?? Prisma.JsonNull) as Prisma.InputJsonValue,
tags: {
connect: tags.map(tag => ({ id: tag.id }))
}
},
include: { tags: true }
})
})

return {
id: sequence,
name: params.name!,
ingredients: (params.ingredients || {}) as JsonValue,
description: params.description ?? null,
recipe: (params.recipe || []) as JsonValue,
suggestedMealType: params.suggestedMealType || [],
userId: params.userId ?? null,
tags: params.tags || []
}
})

export default mealFactory
Loading