import { pgTable, text, timestamp, boolean, integer, decimal, jsonb, pgEnum, index, uniqueIndex, } from "drizzle-orm/pg-core"; import { relations } from "drizzle-orm"; import { users } from "./users"; export const visibilityEnum = pgEnum("visibility", ["private", "unlisted", "public", "followers"]); export const difficultyEnum = pgEnum("difficulty", ["easy", "medium", "hard"]); export const recipeTypeEnum = pgEnum("recipe_type", ["dish", "drink"]); export type DietaryTags = { vegan?: boolean; vegetarian?: boolean; glutenFree?: boolean; dairyFree?: boolean; nutFree?: boolean; halal?: boolean; kosher?: boolean; }; export const recipes = pgTable("recipes", { id: text("id").primaryKey(), authorId: text("author_id").notNull().references(() => users.id, { onDelete: "cascade" }), title: text("title").notNull(), description: text("description"), baseServings: integer("base_servings").notNull().default(4), recipeType: recipeTypeEnum("recipe_type").notNull().default("dish"), visibility: visibilityEnum("visibility").notNull().default("private"), sourceUrl: text("source_url"), aiGenerated: boolean("ai_generated").notNull().default(false), aiModel: text("ai_model"), aiPrompt: text("ai_prompt"), language: text("language"), dietaryTags: jsonb("dietary_tags").$type().default({}), dietaryVerified: boolean("dietary_verified").notNull().default(false), nutritionData: jsonb("nutrition_data").$type<{ perServing: { calories: number; proteinG: number; carbsG: number; fatG: number; fiberG: number; sodiumMg: number } }>(), nutritionManual: boolean("nutrition_manual").notNull().default(false), difficulty: difficultyEnum("difficulty"), prepMins: integer("prep_mins"), cookMins: integer("cook_mins"), tags: text("tags").array().notNull().default([]), isBatchCook: boolean("is_batch_cook").notNull().default(false), // User-chosen cover for when there's no photo — null falls back to the // deterministic hash-based placeholder (see lib/recipe-placeholder.ts). coverIcon: text("cover_icon"), coverColor: text("cover_color"), createdAt: timestamp("created_at").notNull().defaultNow(), updatedAt: timestamp("updated_at").notNull().defaultNow(), }, (t) => [ index("recipes_author_idx").on(t.authorId), index("recipes_visibility_idx").on(t.visibility), index("recipes_dietary_tags_gin").using("gin", t.dietaryTags), index("recipes_batch_cook_idx").on(t.isBatchCook), index("recipes_type_idx").on(t.recipeType), ]); export const ingredients = pgTable("ingredients", { id: text("id").primaryKey(), name: text("name").notNull().unique(), aliases: text("aliases").array().notNull().default([]), category: text("category"), knownAllergens: text("known_allergens").array().notNull().default([]), createdAt: timestamp("created_at").notNull().defaultNow(), }); export const recipeIngredients = pgTable("recipe_ingredients", { id: text("id").primaryKey(), recipeId: text("recipe_id").notNull().references(() => recipes.id, { onDelete: "cascade" }), ingredientId: text("ingredient_id").references(() => ingredients.id, { onDelete: "set null" }), rawName: text("raw_name").notNull(), quantity: decimal("quantity", { precision: 10, scale: 4 }), unit: text("unit"), note: text("note"), order: integer("order").notNull().default(0), }, (t) => [ index("recipe_ingredients_recipe_idx").on(t.recipeId), ]); export const recipeSteps = pgTable("recipe_steps", { id: text("id").primaryKey(), recipeId: text("recipe_id").notNull().references(() => recipes.id, { onDelete: "cascade" }), order: integer("order").notNull(), instruction: text("instruction").notNull(), timerSeconds: integer("timer_seconds"), photoUrl: text("photo_url"), appliesTo: text("applies_to").array().notNull().default([]), }, (t) => [ index("recipe_steps_recipe_idx").on(t.recipeId), ]); export const recipeBatchDishes = pgTable("recipe_batch_dishes", { id: text("id").primaryKey(), recipeId: text("recipe_id").notNull().references(() => recipes.id, { onDelete: "cascade" }), name: text("name").notNull(), description: text("description"), order: integer("order").notNull().default(0), fridgeDays: integer("fridge_days").notNull(), freezerFriendly: boolean("freezer_friendly").notNull().default(false), freezerNote: text("freezer_note"), dayOfInstructions: text("day_of_instructions").notNull(), }, (t) => [ index("recipe_batch_dishes_recipe_idx").on(t.recipeId), ]); export const recipePhotos = pgTable("recipe_photos", { id: text("id").primaryKey(), recipeId: text("recipe_id").notNull().references(() => recipes.id, { onDelete: "cascade" }), storageKey: text("storage_key").notNull(), order: integer("order").notNull().default(0), isCover: boolean("is_cover").notNull().default(false), sizeMb: integer("size_mb").notNull().default(0), createdAt: timestamp("created_at").notNull().defaultNow(), }); export const recipeNotes = pgTable("recipe_notes", { id: text("id").primaryKey(), recipeId: text("recipe_id").notNull().references(() => recipes.id, { onDelete: "cascade" }), userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }), content: text("content").notNull(), createdAt: timestamp("created_at").notNull().defaultNow(), updatedAt: timestamp("updated_at").notNull().defaultNow(), }, (t) => [ uniqueIndex("recipe_notes_recipe_user_idx").on(t.recipeId, t.userId), ]); export const recipeVariations = pgTable("recipe_variations", { id: text("id").primaryKey(), parentRecipeId: text("parent_recipe_id").notNull().references(() => recipes.id, { onDelete: "cascade" }), childRecipeId: text("child_recipe_id").notNull().references(() => recipes.id, { onDelete: "cascade" }), description: text("description"), aiGenerated: boolean("ai_generated").notNull().default(false), createdAt: timestamp("created_at").notNull().defaultNow(), }); export const userAllergens = pgTable("user_allergens", { userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }), allergenTag: text("allergen_tag").notNull(), }); export const recipesRelations = relations(recipes, ({ one, many }) => ({ author: one(users, { fields: [recipes.authorId], references: [users.id] }), ingredients: many(recipeIngredients), steps: many(recipeSteps), photos: many(recipePhotos), notes: many(recipeNotes), childVariations: many(recipeVariations, { relationName: "parent" }), parentVariations: many(recipeVariations, { relationName: "child" }), batchDishes: many(recipeBatchDishes), })); export const recipeBatchDishesRelations = relations(recipeBatchDishes, ({ one }) => ({ recipe: one(recipes, { fields: [recipeBatchDishes.recipeId], references: [recipes.id] }), })); export const recipeIngredientsRelations = relations(recipeIngredients, ({ one }) => ({ recipe: one(recipes, { fields: [recipeIngredients.recipeId], references: [recipes.id] }), ingredient: one(ingredients, { fields: [recipeIngredients.ingredientId], references: [ingredients.id] }), })); export const recipePhotosRelations = relations(recipePhotos, ({ one }) => ({ recipe: one(recipes, { fields: [recipePhotos.recipeId], references: [recipes.id] }), })); export const recipeStepsRelations = relations(recipeSteps, ({ one }) => ({ recipe: one(recipes, { fields: [recipeSteps.recipeId], references: [recipes.id] }), })); export const recipeNotesRelations = relations(recipeNotes, ({ one }) => ({ recipe: one(recipes, { fields: [recipeNotes.recipeId], references: [recipes.id] }), })); export const recipeVariationsRelations = relations(recipeVariations, ({ one }) => ({ parent: one(recipes, { fields: [recipeVariations.parentRecipeId], references: [recipes.id], relationName: "parent" }), child: one(recipes, { fields: [recipeVariations.childRecipeId], references: [recipes.id], relationName: "child" }), })); export const recipeSnapshots = pgTable("recipe_snapshots", { id: text("id").primaryKey(), recipeId: text("recipe_id").notNull().references(() => recipes.id, { onDelete: "cascade" }), authorId: text("author_id").notNull().references(() => users.id, { onDelete: "cascade" }), version: integer("version").notNull(), title: text("title").notNull(), snapshotData: jsonb("snapshot_data").notNull().$type<{ title: string; description: string | null; baseServings: number; difficulty: string | null; prepMins: number | null; cookMins: number | null; dietaryTags: Record; ingredients: Array<{ rawName: string; quantity: string | null; unit: string | null; note: string | null; order: number }>; steps: Array<{ instruction: string; timerSeconds: number | null; order: number }>; }>(), createdAt: timestamp("created_at").notNull().defaultNow(), }, (t) => [ index("recipe_snapshots_recipe_idx").on(t.recipeId, t.version), ]); export const recipeSnapshotsRelations = relations(recipeSnapshots, ({ one }) => ({ recipe: one(recipes, { fields: [recipeSnapshots.recipeId], references: [recipes.id] }), author: one(users, { fields: [recipeSnapshots.authorId], references: [users.id] }), }));