import { RecipeModel } from '../../schemas/recipe.schema.js'; import type { CreateRecipeInput, UpdateRecipeInput, RecipeQueryInput } from '@meshitrack/shared'; import type { RecipeIngredient as StoredIngredient } from '@meshitrack/shared'; interface NutritionInfo { calories: number; protein: number; carbs: number; fat: number; fiber?: number; sugar?: number; sodium?: number; saturatedFat?: number; cholesterol?: number; } interface ComputedFields { ingredients: StoredIngredient[]; totalNutrition: NutritionInfo; perServingNutrition: NutritionInfo; warnings: string[]; } export class RecipesRepository { public async findByHousehold(householdId: string, query: RecipeQueryInput) { const filter: Record = { householdId, deletedAt: { $exists: false } }; if (query.q) filter['$text'] = { $search: query.q }; if (query.cuisine) filter['cuisine'] = { $regex: query.cuisine, $options: 'i' }; if (query.isFavorite !== undefined) filter['isFavorite'] = query.isFavorite; if (query.maxCalories !== undefined) filter['perServingNutrition.calories'] = { $lte: query.maxCalories }; if (query.tags) { const tagList = query.tags .split(',') .map((t) => t.trim()) .filter(Boolean); if (tagList.length > 0) filter['tags'] = { $all: tagList }; } if (query.cursor) { const id = Buffer.from(query.cursor, 'base64').toString(); filter['_id'] = { $gt: id }; } const limit = query.limit; const items = await RecipeModel.find(filter) .sort({ _id: 1 }) .limit(limit + 1) .lean() .exec(); const hasMore = items.length > limit; const data = hasMore ? items.slice(0, limit) : items; const cursor = data.length > 0 ? Buffer.from(data[data.length - 1]._id.toString()).toString('base64') : null; return { data, pagination: { cursor: hasMore ? cursor : null, hasMore } }; } public async findById(id: string, householdId: string) { return RecipeModel.findOne({ _id: id, householdId, deletedAt: { $exists: false } }) .lean() .exec(); } public async findByProductId( householdId: string, productId: string, query: { cursor?: string; limit?: number }, ) { const filter: Record = { householdId, 'ingredients.productId': productId, deletedAt: { $exists: false }, }; const limit = query.limit ?? 20; if (query.cursor) { const id = Buffer.from(query.cursor, 'base64').toString(); filter['_id'] = { $gt: id }; } const items = await RecipeModel.find(filter) .sort({ _id: 1 }) .limit(limit + 1) .lean() .exec(); const hasMore = items.length > limit; const data = hasMore ? items.slice(0, limit) : items; const cursor = data.length > 0 ? Buffer.from(data[data.length - 1]._id.toString()).toString('base64') : null; return { data, pagination: { cursor: hasMore ? cursor : null, hasMore } }; } public async findAllByProductId(householdId: string, productId: string) { return RecipeModel.find({ householdId, 'ingredients.productId': productId, deletedAt: { $exists: false }, }) .lean() .exec(); } public async create( data: Omit, computed: ComputedFields, householdId: string, createdBy: string, ) { const recipe = new RecipeModel({ ...data, ...computed, householdId, createdBy }); const saved = await recipe.save(); return saved.toObject(); } public async update( id: string, householdId: string, data: Partial, computed?: Partial, ) { const update: Record = { ...data }; if (computed) Object.assign(update, computed); return RecipeModel.findOneAndUpdate( { _id: id, householdId, deletedAt: { $exists: false } }, { $set: update }, { new: true, lean: true }, ).exec(); } public async softDelete(id: string, householdId: string) { return RecipeModel.findOneAndUpdate( { _id: id, householdId, deletedAt: { $exists: false } }, { $set: { deletedAt: new Date() } }, { new: true, lean: true }, ).exec(); } }