From 51e6722f4c6056f77d665c2ab4499718a40e098e Mon Sep 17 00:00:00 2001 From: Arnaud Date: Sun, 19 Jul 2026 10:32:31 +0200 Subject: [PATCH] feat: custom recipe cover color/icon + mute auto placeholder palette (v0.51.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two changes to the no-photo cover placeholder shipped last version: 1. Muted the gradient palette (100/40-opacity tints instead of solid -200/ -950 stops) — the original was too saturated next to real cover photos in the same grid, per feedback. 2. New coverIcon/coverColor columns on recipes (nullable text, migration 0048, additive-only) let the author pin a specific color+icon from the recipe editor instead of the automatic per-id pick. getRecipePlaceholder now checks these first, falling back to the deterministic hash pick when unset — existing recipes are unaffected until edited. Wired coverIcon/coverColor through every explicit-column recipe select that feeds a grid card (explore trending/recent, search, feed, for-you) — the relational-query call sites (recipes page, collections, profile pages) already return all columns and needed no changes. Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 5 + apps/web/app/(app)/explore/page.tsx | 2 + apps/web/app/(app)/recipes/[id]/edit/page.tsx | 2 + apps/web/app/api/v1/feed/for-you/route.ts | 2 + apps/web/app/api/v1/feed/route.ts | 2 + apps/web/app/api/v1/recipes/[id]/route.ts | 4 + apps/web/app/api/v1/recipes/route.ts | 4 + apps/web/app/api/v1/search/route.ts | 2 + apps/web/components/recipe/recipe-card.tsx | 2 + .../components/recipe/recipe-cover-picker.tsx | 80 + .../recipe/recipe-cover-placeholder.tsx | 6 +- apps/web/components/recipe/recipe-form.tsx | 21 + .../components/recipe/recipe-grid-card.tsx | 2 + apps/web/components/recipe/recipes-grid.tsx | 2 + apps/web/lib/changelog.ts | 9 +- apps/web/lib/openapi.ts | 7 + apps/web/lib/recipe-placeholder.ts | 88 +- apps/web/messages/en.json | 3 + apps/web/messages/fr.json | 3 + apps/web/package.json | 2 +- package.json | 2 +- .../src/migrations/0048_normal_shiver_man.sql | 2 + .../db/src/migrations/meta/0048_snapshot.json | 5488 +++++++++++++++++ packages/db/src/migrations/meta/_journal.json | 7 + packages/db/src/schema/recipes.ts | 4 + 25 files changed, 5721 insertions(+), 30 deletions(-) create mode 100644 apps/web/components/recipe/recipe-cover-picker.tsx create mode 100644 packages/db/src/migrations/0048_normal_shiver_man.sql create mode 100644 packages/db/src/migrations/meta/0048_snapshot.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 273f77c..533c069 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to Epicure are documented here. This file is mirrored in-app at `/changelog` (and in the admin dashboard) via `apps/web/lib/changelog.ts` — update both together. +## 0.51.0 — 2026-07-19 11:40 + +### Added +- Pick your own recipe cover color + icon in the recipe editor (below Photos) — used whenever that recipe has no photo. Also toned down the auto-generated placeholder colors from the previous release, which were too saturated next to real cover photos. + ## 0.50.2 — 2026-07-19 10:15 ### Fixed diff --git a/apps/web/app/(app)/explore/page.tsx b/apps/web/app/(app)/explore/page.tsx index 1244176..19fec26 100644 --- a/apps/web/app/(app)/explore/page.tsx +++ b/apps/web/app/(app)/explore/page.tsx @@ -61,6 +61,8 @@ export default async function ExplorePage({ isBatchCook: recipes.isBatchCook, sourceUrl: recipes.sourceUrl, recipeType: recipes.recipeType, + coverIcon: recipes.coverIcon, + coverColor: recipes.coverColor, }; // Trending: public recipes ordered by favorite count in last 7 days diff --git a/apps/web/app/(app)/recipes/[id]/edit/page.tsx b/apps/web/app/(app)/recipes/[id]/edit/page.tsx index 03fcaa7..16546b7 100644 --- a/apps/web/app/(app)/recipes/[id]/edit/page.tsx +++ b/apps/web/app/(app)/recipes/[id]/edit/page.tsx @@ -60,6 +60,8 @@ export default async function EditRecipePage({ params }: Params) { isCover: photo.isCover, preview: getPublicUrl(photo.storageKey), })), + coverIcon: recipe.coverIcon, + coverColor: recipe.coverColor, isBatchCook: recipe.isBatchCook, // Only pre-fill the manual-nutrition editor with existing values when // they were actually manually entered — AI-estimated data shouldn't diff --git a/apps/web/app/api/v1/feed/for-you/route.ts b/apps/web/app/api/v1/feed/for-you/route.ts index 8ceedff..c46b42f 100644 --- a/apps/web/app/api/v1/feed/for-you/route.ts +++ b/apps/web/app/api/v1/feed/for-you/route.ts @@ -51,6 +51,8 @@ export async function GET(req: NextRequest) { isBatchCook: recipes.isBatchCook, sourceUrl: recipes.sourceUrl, recipeType: recipes.recipeType, + coverIcon: recipes.coverIcon, + coverColor: recipes.coverColor, }) .from(recipes) .innerJoin(users, eq(recipes.authorId, users.id)) diff --git a/apps/web/app/api/v1/feed/route.ts b/apps/web/app/api/v1/feed/route.ts index 04634b3..2560607 100644 --- a/apps/web/app/api/v1/feed/route.ts +++ b/apps/web/app/api/v1/feed/route.ts @@ -41,6 +41,8 @@ export async function GET(req: NextRequest) { isBatchCook: recipes.isBatchCook, sourceUrl: recipes.sourceUrl, recipeType: recipes.recipeType, + coverIcon: recipes.coverIcon, + coverColor: recipes.coverColor, createdAt: recipes.createdAt, updatedAt: recipes.updatedAt, authorId: recipes.authorId, diff --git a/apps/web/app/api/v1/recipes/[id]/route.ts b/apps/web/app/api/v1/recipes/[id]/route.ts index cfab2b3..3eb367f 100644 --- a/apps/web/app/api/v1/recipes/[id]/route.ts +++ b/apps/web/app/api/v1/recipes/[id]/route.ts @@ -47,6 +47,8 @@ const UpdateRecipeSchema = z.object({ key: z.string().min(1).max(500), isCover: z.boolean().default(false), })).max(20).optional(), + coverIcon: z.string().max(50).nullable().optional(), + coverColor: z.string().max(50).nullable().optional(), isBatchCook: z.boolean().optional(), dishes: z.array(z.object({ name: z.string().min(1).max(100), @@ -169,6 +171,8 @@ export async function PUT(req: NextRequest, { params }: Params) { if (data.tags !== undefined) updates.tags = data.tags; if (data.dietaryTags !== undefined) updates.dietaryTags = data.dietaryTags; if (data.isBatchCook !== undefined) updates.isBatchCook = data.isBatchCook; + if (data.coverIcon !== undefined) updates.coverIcon = data.coverIcon; + if (data.coverColor !== undefined) updates.coverColor = data.coverColor; if (data.nutrition !== undefined) { updates.nutritionData = data.nutrition ? { perServing: data.nutrition } : null; updates.nutritionManual = !!data.nutrition; diff --git a/apps/web/app/api/v1/recipes/route.ts b/apps/web/app/api/v1/recipes/route.ts index 64e8a9e..f0ea4ed 100644 --- a/apps/web/app/api/v1/recipes/route.ts +++ b/apps/web/app/api/v1/recipes/route.ts @@ -51,6 +51,8 @@ const CreateRecipeSchema = z.object({ key: z.string().min(1).max(500), isCover: z.boolean().default(false), })).max(20).default([]), + coverIcon: z.string().max(50).nullable().optional(), + coverColor: z.string().max(50).nullable().optional(), isBatchCook: z.boolean().default(false), dishes: z.array(z.object({ name: z.string().min(1).max(100), @@ -142,6 +144,8 @@ export async function POST(req: NextRequest) { aiGenerated: data.aiGenerated ?? false, language: data.language, sourceUrl: data.sourceUrl, + coverIcon: data.coverIcon ?? null, + coverColor: data.coverColor ?? null, isBatchCook: data.isBatchCook, nutritionData: data.nutrition ? { perServing: data.nutrition } : undefined, nutritionManual: !!data.nutrition, diff --git a/apps/web/app/api/v1/search/route.ts b/apps/web/app/api/v1/search/route.ts index dc71425..2f0db0a 100644 --- a/apps/web/app/api/v1/search/route.ts +++ b/apps/web/app/api/v1/search/route.ts @@ -137,6 +137,8 @@ export async function GET(req: NextRequest) { isBatchCook: recipes.isBatchCook, sourceUrl: recipes.sourceUrl, recipeType: recipes.recipeType, + coverIcon: recipes.coverIcon, + coverColor: recipes.coverColor, authorId: recipes.authorId, authorName: users.name, createdAt: recipes.createdAt, diff --git a/apps/web/components/recipe/recipe-card.tsx b/apps/web/components/recipe/recipe-card.tsx index e3c1ec4..ab8fddc 100644 --- a/apps/web/components/recipe/recipe-card.tsx +++ b/apps/web/components/recipe/recipe-card.tsx @@ -20,6 +20,8 @@ type Recipe = { visibility: "private" | "unlisted" | "public" | "followers"; updatedAt: Date; photos?: Array<{ storageKey: string; isCover: boolean }>; + coverIcon?: string | null; + coverColor?: string | null; sourceUrl?: string | null; recipeType?: "dish" | "drink"; }; diff --git a/apps/web/components/recipe/recipe-cover-picker.tsx b/apps/web/components/recipe/recipe-cover-picker.tsx new file mode 100644 index 0000000..b57e206 --- /dev/null +++ b/apps/web/components/recipe/recipe-cover-picker.tsx @@ -0,0 +1,80 @@ +"use client"; + +import { Shuffle } from "lucide-react"; +import { useTranslations } from "next-intl"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; +import { GRADIENT_OPTIONS, DISH_ICON_OPTIONS, DRINK_ICON_OPTIONS } from "@/lib/recipe-placeholder"; + +/** Lets the user pin a specific color + icon for a recipe's cover placeholder, + * used whenever there's no uploaded photo. Leaving both unset (null) keeps + * the automatic per-recipe pick from getRecipePlaceholder. */ +export function RecipeCoverPicker({ + recipeType, + color, + icon, + onChange, +}: { + recipeType: "dish" | "drink"; + color: string | null; + icon: string | null; + onChange: (next: { color: string | null; icon: string | null }) => void; +}) { + const t = useTranslations("recipeForm"); + const iconOptions = recipeType === "drink" ? DRINK_ICON_OPTIONS : DISH_ICON_OPTIONS; + + return ( +
+
+

{t("coverHint")}

+ {(color || icon) && ( + + )} +
+ +
+ {GRADIENT_OPTIONS.map((g) => ( +
+ +
+ {iconOptions.map(({ key, Icon }) => ( + + ))} +
+
+ ); +} diff --git a/apps/web/components/recipe/recipe-cover-placeholder.tsx b/apps/web/components/recipe/recipe-cover-placeholder.tsx index 5658432..b80621a 100644 --- a/apps/web/components/recipe/recipe-cover-placeholder.tsx +++ b/apps/web/components/recipe/recipe-cover-placeholder.tsx @@ -7,14 +7,14 @@ export function RecipeCoverPlaceholder({ className, iconClassName, }: { - recipe: { id: string; recipeType?: "dish" | "drink" }; + recipe: { id: string; recipeType?: "dish" | "drink"; coverIcon?: string | null; coverColor?: string | null }; className?: string; iconClassName?: string; }) { const { gradient, Icon } = getRecipePlaceholder(recipe); return ( -
- +
+
); } diff --git a/apps/web/components/recipe/recipe-form.tsx b/apps/web/components/recipe/recipe-form.tsx index 2c11007..c8d5fc9 100644 --- a/apps/web/components/recipe/recipe-form.tsx +++ b/apps/web/components/recipe/recipe-form.tsx @@ -24,6 +24,7 @@ import { } from "@/components/ui/alert-dialog"; import { DietaryTagPicker } from "./dietary-tag-picker"; import { PhotoUploader, type PhotoEntry } from "./photo-uploader"; +import { RecipeCoverPicker } from "./recipe-cover-picker"; import { RegenerateRecipeButton } from "./regenerate-recipe-button"; import type { RegeneratedRecipe } from "@/lib/ai/features/regenerate-recipe"; @@ -78,6 +79,8 @@ type RecipeFormProps = { ingredients?: IngredientRow[]; steps?: StepRow[]; photos?: PhotoEntry[]; + coverIcon?: string | null; + coverColor?: string | null; isBatchCook?: boolean; dishes?: DishRow[]; nutrition?: { @@ -155,6 +158,8 @@ export function RecipeForm({ recipeId, defaultValues }: RecipeFormProps) { defaultValues?.steps?.length ? defaultValues.steps : [newStep()] ); const [photos, setPhotos] = useState(defaultValues?.photos ?? []); + const [coverIcon, setCoverIcon] = useState(defaultValues?.coverIcon ?? null); + const [coverColor, setCoverColor] = useState(defaultValues?.coverColor ?? null); const [isBatchCook, setIsBatchCook] = useState(defaultValues?.isBatchCook ?? false); const [dishes, setDishes] = useState( defaultValues?.dishes?.length ? defaultValues.dishes : [newDish()] @@ -376,6 +381,8 @@ export function RecipeForm({ recipeId, defaultValues }: RecipeFormProps) { ingredients: filteredIngredients, steps: filteredSteps, photos: photos.map((p) => ({ key: p.key, isCover: p.isCover })), + coverIcon, + coverColor, isBatchCook, dishes: filteredDishes, nutrition: addNutrition @@ -684,6 +691,20 @@ export function RecipeForm({ recipeId, defaultValues }: RecipeFormProps) {
+ {/* Cover placeholder (used when there's no photo above) */} +
+ + { + setCoverColor(color); + setCoverIcon(icon); + }} + /> +
+ {/* Ingredients */} diff --git a/apps/web/components/recipe/recipe-grid-card.tsx b/apps/web/components/recipe/recipe-grid-card.tsx index 7188e5e..b764d5c 100644 --- a/apps/web/components/recipe/recipe-grid-card.tsx +++ b/apps/web/components/recipe/recipe-grid-card.tsx @@ -21,6 +21,8 @@ export type GridCardRecipe = { visibility: "private" | "unlisted" | "public" | "followers"; tags: string[]; photos?: Array<{ storageKey: string; isCover: boolean }>; + coverIcon?: string | null; + coverColor?: string | null; isFavorited?: boolean; isBatchCook?: boolean; dishCount?: number; diff --git a/apps/web/components/recipe/recipes-grid.tsx b/apps/web/components/recipe/recipes-grid.tsx index f22de65..224a3d4 100644 --- a/apps/web/components/recipe/recipes-grid.tsx +++ b/apps/web/components/recipe/recipes-grid.tsx @@ -46,6 +46,8 @@ type Recipe = { tags: string[]; updatedAt: Date; photos?: Array<{ storageKey: string; isCover: boolean }>; + coverIcon?: string | null; + coverColor?: string | null; isFavorited?: boolean; isBatchCook?: boolean; dishCount?: number; diff --git a/apps/web/lib/changelog.ts b/apps/web/lib/changelog.ts index 4634374..2bb6c42 100644 --- a/apps/web/lib/changelog.ts +++ b/apps/web/lib/changelog.ts @@ -1,5 +1,5 @@ // Mirrors CHANGELOG.md at the repo root — update both together. -export const APP_VERSION = "0.50.2"; +export const APP_VERSION = "0.51.0"; export type ChangelogEntry = { version: string; @@ -11,6 +11,13 @@ export type ChangelogEntry = { }; export const CHANGELOG: ChangelogEntry[] = [ + { + version: "0.51.0", + date: "2026-07-19 11:40", + added: [ + "Pick your own recipe cover color + icon in the recipe editor (below Photos) — used whenever that recipe has no photo. Also toned down the auto-generated placeholder colors from the previous release, which were too saturated next to real cover photos.", + ], + }, { version: "0.50.2", date: "2026-07-19 10:15", diff --git a/apps/web/lib/openapi.ts b/apps/web/lib/openapi.ts index 12b4401..9de0d11 100644 --- a/apps/web/lib/openapi.ts +++ b/apps/web/lib/openapi.ts @@ -62,6 +62,8 @@ export function generateOpenApiSpec(): object { difficulty: z.enum(["easy", "medium", "hard"]).nullable(), prepMins: z.number().int().nullable(), cookMins: z.number().int().nullable(), tags: z.array(z.string()), sourceUrl: z.string().nullable(), language: z.string().nullable(), + coverIcon: z.string().nullable().describe("Cover placeholder icon key, used when there's no photo. Null falls back to a deterministic pick from the recipe id."), + coverColor: z.string().nullable().describe("Cover placeholder gradient key, used when there's no photo. Null falls back to a deterministic pick from the recipe id."), isBatchCook: z.boolean(), nutritionData: z.object({ perServing: NutritionInputRef }).nullable(), nutritionManual: z.boolean().describe("True if nutritionData was entered by the author rather than AI-estimated."), dietaryTags: DietaryTagsRef, aiGenerated: z.boolean(), @@ -77,6 +79,7 @@ export function generateOpenApiSpec(): object { prepMins: z.number().int().nullable(), cookMins: z.number().int().nullable(), tags: z.array(z.string()), dietaryTags: DietaryTagsRef, aiGenerated: z.boolean(), language: z.string().nullable(), sourceUrl: z.string().nullable(), + coverIcon: z.string().nullable(), coverColor: z.string().nullable(), isBatchCook: z.boolean(), nutritionData: z.object({ perServing: NutritionInputRef }).nullable(), nutritionManual: z.boolean(), createdAt: z.string().datetime(), updatedAt: z.string().datetime(), @@ -113,6 +116,8 @@ export function generateOpenApiSpec(): object { key: z.string().min(1).max(500), isCover: z.boolean().default(false), })).max(20).default([]), + coverIcon: z.string().max(50).nullable().optional(), + coverColor: z.string().max(50).nullable().optional(), isBatchCook: z.boolean().default(false), dishes: z.array(z.object({ name: z.string().min(1).max(100), @@ -155,6 +160,8 @@ export function generateOpenApiSpec(): object { key: z.string().min(1).max(500), isCover: z.boolean().default(false), })).max(20).optional(), + coverIcon: z.string().max(50).nullable().optional(), + coverColor: z.string().max(50).nullable().optional(), isBatchCook: z.boolean().optional(), dishes: z.array(z.object({ name: z.string().min(1).max(100), diff --git a/apps/web/lib/recipe-placeholder.ts b/apps/web/lib/recipe-placeholder.ts index 1297edf..ae6e165 100644 --- a/apps/web/lib/recipe-placeholder.ts +++ b/apps/web/lib/recipe-placeholder.ts @@ -5,23 +5,52 @@ import { type LucideIcon, } from "lucide-react"; -const GRADIENTS = [ - "from-orange-200 to-rose-200 dark:from-orange-950 dark:to-rose-950", - "from-amber-200 to-lime-200 dark:from-amber-950 dark:to-lime-950", - "from-sky-200 to-indigo-200 dark:from-sky-950 dark:to-indigo-950", - "from-emerald-200 to-teal-200 dark:from-emerald-950 dark:to-teal-950", - "from-fuchsia-200 to-purple-200 dark:from-fuchsia-950 dark:to-purple-950", - "from-yellow-200 to-orange-200 dark:from-yellow-950 dark:to-orange-950", - "from-rose-200 to-pink-200 dark:from-rose-950 dark:to-pink-950", - "from-teal-200 to-cyan-200 dark:from-teal-950 dark:to-cyan-950", -]; +// Muted on purpose — these sit behind a lucide icon on every photo-less card +// in a grid, so anything more saturated than "barely-there tint" overpowers +// real cover photos next to it and reads as loud rather than as a placeholder. +export const GRADIENT_OPTIONS = [ + { key: "orange-rose", classes: "from-orange-100/70 to-rose-100/70 dark:from-orange-950/40 dark:to-rose-950/40" }, + { key: "amber-lime", classes: "from-amber-100/70 to-lime-100/70 dark:from-amber-950/40 dark:to-lime-950/40" }, + { key: "sky-indigo", classes: "from-sky-100/70 to-indigo-100/70 dark:from-sky-950/40 dark:to-indigo-950/40" }, + { key: "emerald-teal", classes: "from-emerald-100/70 to-teal-100/70 dark:from-emerald-950/40 dark:to-teal-950/40" }, + { key: "fuchsia-purple", classes: "from-fuchsia-100/70 to-purple-100/70 dark:from-fuchsia-950/40 dark:to-purple-950/40" }, + { key: "yellow-orange", classes: "from-yellow-100/70 to-orange-100/70 dark:from-yellow-950/40 dark:to-orange-950/40" }, + { key: "rose-pink", classes: "from-rose-100/70 to-pink-100/70 dark:from-rose-950/40 dark:to-pink-950/40" }, + { key: "teal-cyan", classes: "from-teal-100/70 to-cyan-100/70 dark:from-teal-950/40 dark:to-cyan-950/40" }, +] as const; -const DISH_ICONS: LucideIcon[] = [ - ChefHat, Soup, Salad, Pizza, Sandwich, IceCream, Cookie, Beef, Fish, - Cherry, Carrot, Egg, Drumstick, Croissant, Donut, Cake, -]; +export const DISH_ICON_OPTIONS = [ + { key: "chef-hat", Icon: ChefHat }, + { key: "soup", Icon: Soup }, + { key: "salad", Icon: Salad }, + { key: "pizza", Icon: Pizza }, + { key: "sandwich", Icon: Sandwich }, + { key: "ice-cream", Icon: IceCream }, + { key: "cookie", Icon: Cookie }, + { key: "beef", Icon: Beef }, + { key: "fish", Icon: Fish }, + { key: "cherry", Icon: Cherry }, + { key: "carrot", Icon: Carrot }, + { key: "egg", Icon: Egg }, + { key: "drumstick", Icon: Drumstick }, + { key: "croissant", Icon: Croissant }, + { key: "donut", Icon: Donut }, + { key: "cake", Icon: Cake }, +] as const; -const DRINK_ICONS: LucideIcon[] = [Coffee, Wine, Beer, GlassWater, Martini, CupSoda]; +export const DRINK_ICON_OPTIONS = [ + { key: "coffee", Icon: Coffee }, + { key: "wine", Icon: Wine }, + { key: "beer", Icon: Beer }, + { key: "glass-water", Icon: GlassWater }, + { key: "martini", Icon: Martini }, + { key: "cup-soda", Icon: CupSoda }, +] as const; + +export const ALL_ICON_OPTIONS: { key: string; Icon: LucideIcon }[] = [...DISH_ICON_OPTIONS, ...DRINK_ICON_OPTIONS]; + +const ICON_BY_KEY = new Map(ALL_ICON_OPTIONS.map((o) => [o.key, o.Icon])); +const GRADIENT_BY_KEY = new Map(GRADIENT_OPTIONS.map((o) => [o.key, o.classes])); /** Small, fast, deterministic string hash (djb2) — same recipe always gets the same placeholder. */ function hashString(s: string): number { @@ -32,16 +61,27 @@ function hashString(s: string): number { return hash >>> 0; } -export function getRecipePlaceholder(recipe: { id: string; recipeType?: "dish" | "drink" }): { - gradient: string; - Icon: LucideIcon; -} { +/** Resolves a recipe's cover placeholder — user-chosen coverIcon/coverColor + * win when set and recognized, else falls back to a deterministic pick + * from the recipe id so it's still stable without a fresh hash on rename. */ +export function getRecipePlaceholder(recipe: { + id: string; + recipeType?: "dish" | "drink"; + coverIcon?: string | null; + coverColor?: string | null; +}): { gradient: string; Icon: LucideIcon } { const hash = hashString(recipe.id); - const icons = recipe.recipeType === "drink" ? DRINK_ICONS : DISH_ICONS; - return { - gradient: GRADIENTS[hash % GRADIENTS.length]!, + const dishOrDrinkIcons = recipe.recipeType === "drink" ? DRINK_ICON_OPTIONS : DISH_ICON_OPTIONS; + + const gradient = + (recipe.coverColor ? GRADIENT_BY_KEY.get(recipe.coverColor) : undefined) ?? + GRADIENT_OPTIONS[hash % GRADIENT_OPTIONS.length]!.classes; + + const Icon = + (recipe.coverIcon ? ICON_BY_KEY.get(recipe.coverIcon) : undefined) ?? // Different modulus base than gradient's so icon/color pairing doesn't // repeat in lockstep across ids that happen to share a gradient. - Icon: icons[Math.floor(hash / GRADIENTS.length) % icons.length]!, - }; + dishOrDrinkIcons[Math.floor(hash / GRADIENT_OPTIONS.length) % dishOrDrinkIcons.length]!.Icon; + + return { gradient, Icon }; } diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 68d4b53..9eb0b65 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -872,6 +872,9 @@ "nutritionToggleHelp": "Per serving. Turns off the AI-estimate action on the recipe page — clear these fields (turn this off) to let AI estimate it instead.", "calories": "Calories (kcal)", "photos": "Photos", + "cover": "Cover", + "coverHint": "Only used when this recipe has no photo.", + "coverReset": "Reset to automatic", "ingredients": "Ingredients", "amount": "Amount", "unit": "Unit", diff --git a/apps/web/messages/fr.json b/apps/web/messages/fr.json index 27f1c9f..8d3d4f7 100644 --- a/apps/web/messages/fr.json +++ b/apps/web/messages/fr.json @@ -863,6 +863,9 @@ "nutritionToggleHelp": "Par portion. Désactive l'estimation IA sur la page de la recette — videz ces champs (désactivez ceci) pour laisser l'IA l'estimer à nouveau.", "calories": "Calories (kcal)", "photos": "Photos", + "cover": "Couverture", + "coverHint": "Utilisé uniquement si cette recette n'a pas de photo.", + "coverReset": "Réinitialiser en automatique", "ingredients": "Ingrédients", "amount": "Quantité", "unit": "Unité", diff --git a/apps/web/package.json b/apps/web/package.json index 9c3af3d..81962cd 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@epicure/web", - "version": "0.50.2", + "version": "0.51.0", "private": true, "scripts": { "dev": "next dev", diff --git a/package.json b/package.json index 333bbff..0973914 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "epicure", - "version": "0.50.2", + "version": "0.51.0", "private": true, "scripts": { "dev": "pnpm --filter web dev", diff --git a/packages/db/src/migrations/0048_normal_shiver_man.sql b/packages/db/src/migrations/0048_normal_shiver_man.sql new file mode 100644 index 0000000..5e2bf80 --- /dev/null +++ b/packages/db/src/migrations/0048_normal_shiver_man.sql @@ -0,0 +1,2 @@ +ALTER TABLE "recipes" ADD COLUMN "cover_icon" text;--> statement-breakpoint +ALTER TABLE "recipes" ADD COLUMN "cover_color" text; \ No newline at end of file diff --git a/packages/db/src/migrations/meta/0048_snapshot.json b/packages/db/src/migrations/meta/0048_snapshot.json new file mode 100644 index 0000000..d89ed14 --- /dev/null +++ b/packages/db/src/migrations/meta/0048_snapshot.json @@ -0,0 +1,5488 @@ +{ + "id": "10179f8c-50c6-49d4-bbab-ddcf627d8f5b", + "prevId": "d3d160e2-c956-4b1e-ab60-a2dc1746d453", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "api_key_scope", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'full'" + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_keys_key_hash_unique": { + "name": "api_keys_key_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "key_hash" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.push_subscriptions": { + "name": "push_subscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "p256dh": { + "name": "p256dh", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth": { + "name": "auth", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "push_subscriptions_user_id_users_id_fk": { + "name": "push_subscriptions_user_id_users_id_fk", + "tableFrom": "push_subscriptions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "push_subscriptions_endpoint_unique": { + "name": "push_subscriptions_endpoint_unique", + "nullsNotDistinct": false, + "columns": [ + "endpoint" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sessions_token_unique": { + "name": "sessions_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.two_factors": { + "name": "two_factors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backup_codes": { + "name": "backup_codes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "two_factors_user_id_users_id_fk": { + "name": "two_factors_user_id_users_id_fk", + "tableFrom": "two_factors", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_ai_keys": { + "name": "user_ai_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_ai_keys_user_id_users_id_fk": { + "name": "user_ai_keys_user_id_users_id_fk", + "tableFrom": "user_ai_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_blocks": { + "name": "user_blocks", + "schema": "", + "columns": { + "blocker_id": { + "name": "blocker_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "blocked_id": { + "name": "blocked_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_blocks_blocker_id_users_id_fk": { + "name": "user_blocks_blocker_id_users_id_fk", + "tableFrom": "user_blocks", + "tableTo": "users", + "columnsFrom": [ + "blocker_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_blocks_blocked_id_users_id_fk": { + "name": "user_blocks_blocked_id_users_id_fk", + "tableFrom": "user_blocks", + "tableTo": "users", + "columnsFrom": [ + "blocked_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_blocks_blocker_id_blocked_id_pk": { + "name": "user_blocks_blocker_id_blocked_id_pk", + "columns": [ + "blocker_id", + "blocked_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_follows": { + "name": "user_follows", + "schema": "", + "columns": { + "follower_id": { + "name": "follower_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "following_id": { + "name": "following_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_follows_follower_id_users_id_fk": { + "name": "user_follows_follower_id_users_id_fk", + "tableFrom": "user_follows", + "tableTo": "users", + "columnsFrom": [ + "follower_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_follows_following_id_users_id_fk": { + "name": "user_follows_following_id_users_id_fk", + "tableFrom": "user_follows", + "tableTo": "users", + "columnsFrom": [ + "following_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_follows_follower_id_following_id_pk": { + "name": "user_follows_follower_id_following_id_pk", + "columns": [ + "follower_id", + "following_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_model_prefs": { + "name": "user_model_prefs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text_provider": { + "name": "text_provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "text_model": { + "name": "text_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vision_provider": { + "name": "vision_provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vision_model": { + "name": "vision_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "meal_plan_provider": { + "name": "meal_plan_provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "meal_plan_model": { + "name": "meal_plan_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_model_prefs_user_id_users_id_fk": { + "name": "user_model_prefs_user_id_users_id_fk", + "tableFrom": "user_model_prefs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_model_prefs_user_id_unique": { + "name": "user_model_prefs_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_notification_prefs": { + "name": "user_notification_prefs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "follow": { + "name": "follow", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "comment": { + "name": "comment", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "reply": { + "name": "reply", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "reaction": { + "name": "reaction", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "rating": { + "name": "rating", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "mention": { + "name": "mention", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "leftover_expiring": { + "name": "leftover_expiring", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "shopping_list": { + "name": "shopping_list", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_notification_prefs_user_id_users_id_fk": { + "name": "user_notification_prefs_user_id_users_id_fk", + "tableFrom": "user_notification_prefs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_notification_prefs_user_id_unique": { + "name": "user_notification_prefs_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_nutrition_goals": { + "name": "user_nutrition_goals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "calories_kcal": { + "name": "calories_kcal", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "protein_g": { + "name": "protein_g", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "carbs_g": { + "name": "carbs_g", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "fat_g": { + "name": "fat_g", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_nutrition_goals_user_id_users_id_fk": { + "name": "user_nutrition_goals_user_id_users_id_fk", + "tableFrom": "user_nutrition_goals", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_nutrition_goals_user_id_unique": { + "name": "user_nutrition_goals_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "has_custom_avatar": { + "name": "has_custom_avatar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "use_gravatar": { + "name": "use_gravatar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "private_bio": { + "name": "private_bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_private": { + "name": "is_private", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "user_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "tier": { + "name": "tier", + "type": "tier", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'free'" + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "unit_pref": { + "name": "unit_pref", + "type": "unit_pref", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'metric'" + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'en'" + }, + "two_factor_enabled": { + "name": "two_factor_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "users_username_unique": { + "name": "users_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + }, + "users_stripe_customer_id_unique": { + "name": "users_stripe_customer_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stripe_customer_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verifications": { + "name": "verifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ingredients": { + "name": "ingredients", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "aliases": { + "name": "aliases", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "known_allergens": { + "name": "known_allergens", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ingredients_name_unique": { + "name": "ingredients_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recipe_batch_dishes": { + "name": "recipe_batch_dishes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "recipe_id": { + "name": "recipe_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "fridge_days": { + "name": "fridge_days", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "freezer_friendly": { + "name": "freezer_friendly", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "freezer_note": { + "name": "freezer_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "day_of_instructions": { + "name": "day_of_instructions", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "recipe_batch_dishes_recipe_idx": { + "name": "recipe_batch_dishes_recipe_idx", + "columns": [ + { + "expression": "recipe_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recipe_batch_dishes_recipe_id_recipes_id_fk": { + "name": "recipe_batch_dishes_recipe_id_recipes_id_fk", + "tableFrom": "recipe_batch_dishes", + "tableTo": "recipes", + "columnsFrom": [ + "recipe_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recipe_ingredients": { + "name": "recipe_ingredients", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "recipe_id": { + "name": "recipe_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ingredient_id": { + "name": "ingredient_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "raw_name": { + "name": "raw_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": false + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "recipe_ingredients_recipe_idx": { + "name": "recipe_ingredients_recipe_idx", + "columns": [ + { + "expression": "recipe_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recipe_ingredients_recipe_id_recipes_id_fk": { + "name": "recipe_ingredients_recipe_id_recipes_id_fk", + "tableFrom": "recipe_ingredients", + "tableTo": "recipes", + "columnsFrom": [ + "recipe_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recipe_ingredients_ingredient_id_ingredients_id_fk": { + "name": "recipe_ingredients_ingredient_id_ingredients_id_fk", + "tableFrom": "recipe_ingredients", + "tableTo": "ingredients", + "columnsFrom": [ + "ingredient_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recipe_notes": { + "name": "recipe_notes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "recipe_id": { + "name": "recipe_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "recipe_notes_recipe_user_idx": { + "name": "recipe_notes_recipe_user_idx", + "columns": [ + { + "expression": "recipe_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recipe_notes_recipe_id_recipes_id_fk": { + "name": "recipe_notes_recipe_id_recipes_id_fk", + "tableFrom": "recipe_notes", + "tableTo": "recipes", + "columnsFrom": [ + "recipe_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recipe_notes_user_id_users_id_fk": { + "name": "recipe_notes_user_id_users_id_fk", + "tableFrom": "recipe_notes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recipe_photos": { + "name": "recipe_photos", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "recipe_id": { + "name": "recipe_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_cover": { + "name": "is_cover", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "recipe_photos_recipe_id_recipes_id_fk": { + "name": "recipe_photos_recipe_id_recipes_id_fk", + "tableFrom": "recipe_photos", + "tableTo": "recipes", + "columnsFrom": [ + "recipe_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recipe_snapshots": { + "name": "recipe_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "recipe_id": { + "name": "recipe_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "snapshot_data": { + "name": "snapshot_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "recipe_snapshots_recipe_idx": { + "name": "recipe_snapshots_recipe_idx", + "columns": [ + { + "expression": "recipe_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recipe_snapshots_recipe_id_recipes_id_fk": { + "name": "recipe_snapshots_recipe_id_recipes_id_fk", + "tableFrom": "recipe_snapshots", + "tableTo": "recipes", + "columnsFrom": [ + "recipe_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recipe_snapshots_author_id_users_id_fk": { + "name": "recipe_snapshots_author_id_users_id_fk", + "tableFrom": "recipe_snapshots", + "tableTo": "users", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recipe_steps": { + "name": "recipe_steps", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "recipe_id": { + "name": "recipe_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "instruction": { + "name": "instruction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timer_seconds": { + "name": "timer_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "photo_url": { + "name": "photo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applies_to": { + "name": "applies_to", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + } + }, + "indexes": { + "recipe_steps_recipe_idx": { + "name": "recipe_steps_recipe_idx", + "columns": [ + { + "expression": "recipe_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recipe_steps_recipe_id_recipes_id_fk": { + "name": "recipe_steps_recipe_id_recipes_id_fk", + "tableFrom": "recipe_steps", + "tableTo": "recipes", + "columnsFrom": [ + "recipe_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recipe_variations": { + "name": "recipe_variations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "parent_recipe_id": { + "name": "parent_recipe_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_recipe_id": { + "name": "child_recipe_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ai_generated": { + "name": "ai_generated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "recipe_variations_parent_recipe_id_recipes_id_fk": { + "name": "recipe_variations_parent_recipe_id_recipes_id_fk", + "tableFrom": "recipe_variations", + "tableTo": "recipes", + "columnsFrom": [ + "parent_recipe_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recipe_variations_child_recipe_id_recipes_id_fk": { + "name": "recipe_variations_child_recipe_id_recipes_id_fk", + "tableFrom": "recipe_variations", + "tableTo": "recipes", + "columnsFrom": [ + "child_recipe_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recipes": { + "name": "recipes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_servings": { + "name": "base_servings", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 4 + }, + "recipe_type": { + "name": "recipe_type", + "type": "recipe_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'dish'" + }, + "visibility": { + "name": "visibility", + "type": "visibility", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'private'" + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ai_generated": { + "name": "ai_generated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "ai_model": { + "name": "ai_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ai_prompt": { + "name": "ai_prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dietary_tags": { + "name": "dietary_tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "dietary_verified": { + "name": "dietary_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "nutrition_data": { + "name": "nutrition_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "nutrition_manual": { + "name": "nutrition_manual", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "difficulty": { + "name": "difficulty", + "type": "difficulty", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "prep_mins": { + "name": "prep_mins", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cook_mins": { + "name": "cook_mins", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "is_batch_cook": { + "name": "is_batch_cook", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cover_icon": { + "name": "cover_icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cover_color": { + "name": "cover_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "recipes_author_idx": { + "name": "recipes_author_idx", + "columns": [ + { + "expression": "author_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "recipes_visibility_idx": { + "name": "recipes_visibility_idx", + "columns": [ + { + "expression": "visibility", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "recipes_dietary_tags_gin": { + "name": "recipes_dietary_tags_gin", + "columns": [ + { + "expression": "dietary_tags", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "recipes_batch_cook_idx": { + "name": "recipes_batch_cook_idx", + "columns": [ + { + "expression": "is_batch_cook", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "recipes_type_idx": { + "name": "recipes_type_idx", + "columns": [ + { + "expression": "recipe_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recipes_author_id_users_id_fk": { + "name": "recipes_author_id_users_id_fk", + "tableFrom": "recipes", + "tableTo": "users", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_allergens": { + "name": "user_allergens", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allergen_tag": { + "name": "allergen_tag", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "user_allergens_user_id_users_id_fk": { + "name": "user_allergens_user_id_users_id_fk", + "tableFrom": "user_allergens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.collection_favorites": { + "name": "collection_favorites", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "collection_id": { + "name": "collection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "collection_favorites_collection_idx": { + "name": "collection_favorites_collection_idx", + "columns": [ + { + "expression": "collection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "collection_favorites_user_collection_idx": { + "name": "collection_favorites_user_collection_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "collection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "collection_favorites_user_id_users_id_fk": { + "name": "collection_favorites_user_id_users_id_fk", + "tableFrom": "collection_favorites", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "collection_favorites_collection_id_collections_id_fk": { + "name": "collection_favorites_collection_id_collections_id_fk", + "tableFrom": "collection_favorites", + "tableTo": "collections", + "columnsFrom": [ + "collection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.collection_members": { + "name": "collection_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "collection_id": { + "name": "collection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "collection_member_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'viewer'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "collection_members_user_idx": { + "name": "collection_members_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "collection_members_collection_id_collections_id_fk": { + "name": "collection_members_collection_id_collections_id_fk", + "tableFrom": "collection_members", + "tableTo": "collections", + "columnsFrom": [ + "collection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "collection_members_user_id_users_id_fk": { + "name": "collection_members_user_id_users_id_fk", + "tableFrom": "collection_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.collection_recipes": { + "name": "collection_recipes", + "schema": "", + "columns": { + "collection_id": { + "name": "collection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recipe_id": { + "name": "recipe_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "added_at": { + "name": "added_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "collection_recipes_collection_id_collections_id_fk": { + "name": "collection_recipes_collection_id_collections_id_fk", + "tableFrom": "collection_recipes", + "tableTo": "collections", + "columnsFrom": [ + "collection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "collection_recipes_recipe_id_recipes_id_fk": { + "name": "collection_recipes_recipe_id_recipes_id_fk", + "tableFrom": "collection_recipes", + "tableTo": "recipes", + "columnsFrom": [ + "recipe_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.collections": { + "name": "collections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "collections_user_idx": { + "name": "collections_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "collections_user_id_users_id_fk": { + "name": "collections_user_id_users_id_fk", + "tableFrom": "collections", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.comment_reactions": { + "name": "comment_reactions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "comment_id": { + "name": "comment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "comment_reaction_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "comment_reactions_comment_idx": { + "name": "comment_reactions_comment_idx", + "columns": [ + { + "expression": "comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "comment_reactions_comment_id_comments_id_fk": { + "name": "comment_reactions_comment_id_comments_id_fk", + "tableFrom": "comment_reactions", + "tableTo": "comments", + "columnsFrom": [ + "comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "comment_reactions_user_id_users_id_fk": { + "name": "comment_reactions_user_id_users_id_fk", + "tableFrom": "comment_reactions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.comments": { + "name": "comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "recipe_id": { + "name": "recipe_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "comments_recipe_idx": { + "name": "comments_recipe_idx", + "columns": [ + { + "expression": "recipe_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "comments_recipe_id_recipes_id_fk": { + "name": "comments_recipe_id_recipes_id_fk", + "tableFrom": "comments", + "tableTo": "recipes", + "columnsFrom": [ + "recipe_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "comments_user_id_users_id_fk": { + "name": "comments_user_id_users_id_fk", + "tableFrom": "comments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "comments_parent_id_comments_id_fk": { + "name": "comments_parent_id_comments_id_fk", + "tableFrom": "comments", + "tableTo": "comments", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cooking_history": { + "name": "cooking_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recipe_id": { + "name": "recipe_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "batch_dish_id": { + "name": "batch_dish_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cooked_at": { + "name": "cooked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "servings": { + "name": "servings", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expiry_reminder_sent_at": { + "name": "expiry_reminder_sent_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "cooking_history_user_idx": { + "name": "cooking_history_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cooking_history_batch_dish_idx": { + "name": "cooking_history_batch_dish_idx", + "columns": [ + { + "expression": "batch_dish_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cooking_history_user_id_users_id_fk": { + "name": "cooking_history_user_id_users_id_fk", + "tableFrom": "cooking_history", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cooking_history_recipe_id_recipes_id_fk": { + "name": "cooking_history_recipe_id_recipes_id_fk", + "tableFrom": "cooking_history", + "tableTo": "recipes", + "columnsFrom": [ + "recipe_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cooking_history_batch_dish_id_recipe_batch_dishes_id_fk": { + "name": "cooking_history_batch_dish_id_recipe_batch_dishes_id_fk", + "tableFrom": "cooking_history", + "tableTo": "recipe_batch_dishes", + "columnsFrom": [ + "batch_dish_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.favorites": { + "name": "favorites", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recipe_id": { + "name": "recipe_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "favorites_user_idx": { + "name": "favorites_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "favorites_user_id_users_id_fk": { + "name": "favorites_user_id_users_id_fk", + "tableFrom": "favorites", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "favorites_recipe_id_recipes_id_fk": { + "name": "favorites_recipe_id_recipes_id_fk", + "tableFrom": "favorites", + "tableTo": "recipes", + "columnsFrom": [ + "recipe_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notifications": { + "name": "notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "notification_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recipe_id": { + "name": "recipe_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "comment_id": { + "name": "comment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "read": { + "name": "read", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "notifications_user_idx": { + "name": "notifications_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notifications_user_unread_idx": { + "name": "notifications_user_unread_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "read", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notifications_user_id_users_id_fk": { + "name": "notifications_user_id_users_id_fk", + "tableFrom": "notifications", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notifications_actor_id_users_id_fk": { + "name": "notifications_actor_id_users_id_fk", + "tableFrom": "notifications", + "tableTo": "users", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notifications_recipe_id_recipes_id_fk": { + "name": "notifications_recipe_id_recipes_id_fk", + "tableFrom": "notifications", + "tableTo": "recipes", + "columnsFrom": [ + "recipe_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notifications_comment_id_comments_id_fk": { + "name": "notifications_comment_id_comments_id_fk", + "tableFrom": "notifications", + "tableTo": "comments", + "columnsFrom": [ + "comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ratings": { + "name": "ratings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "recipe_id": { + "name": "recipe_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "score": { + "name": "score", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "review_text": { + "name": "review_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "photo_key": { + "name": "photo_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ratings_user_idx": { + "name": "ratings_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ratings_recipe_idx": { + "name": "ratings_recipe_idx", + "columns": [ + { + "expression": "recipe_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ratings_recipe_id_recipes_id_fk": { + "name": "ratings_recipe_id_recipes_id_fk", + "tableFrom": "ratings", + "tableTo": "recipes", + "columnsFrom": [ + "recipe_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ratings_user_id_users_id_fk": { + "name": "ratings_user_id_users_id_fk", + "tableFrom": "ratings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.meal_plan_entries": { + "name": "meal_plan_entries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "meal_plan_id": { + "name": "meal_plan_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "day": { + "name": "day", + "type": "weekday", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "meal_type": { + "name": "meal_type", + "type": "meal_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "recipe_id": { + "name": "recipe_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "batch_dish_id": { + "name": "batch_dish_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "servings": { + "name": "servings", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 2 + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "meal_plan_entries_plan_idx": { + "name": "meal_plan_entries_plan_idx", + "columns": [ + { + "expression": "meal_plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "meal_plan_entries_plan_day_meal_idx": { + "name": "meal_plan_entries_plan_day_meal_idx", + "columns": [ + { + "expression": "meal_plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "day", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "meal_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "meal_plan_entries_meal_plan_id_meal_plans_id_fk": { + "name": "meal_plan_entries_meal_plan_id_meal_plans_id_fk", + "tableFrom": "meal_plan_entries", + "tableTo": "meal_plans", + "columnsFrom": [ + "meal_plan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "meal_plan_entries_recipe_id_recipes_id_fk": { + "name": "meal_plan_entries_recipe_id_recipes_id_fk", + "tableFrom": "meal_plan_entries", + "tableTo": "recipes", + "columnsFrom": [ + "recipe_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "meal_plan_entries_batch_dish_id_recipe_batch_dishes_id_fk": { + "name": "meal_plan_entries_batch_dish_id_recipe_batch_dishes_id_fk", + "tableFrom": "meal_plan_entries", + "tableTo": "recipe_batch_dishes", + "columnsFrom": [ + "batch_dish_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.meal_plan_members": { + "name": "meal_plan_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "meal_plan_id": { + "name": "meal_plan_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "collection_member_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'viewer'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "meal_plan_members_plan_idx": { + "name": "meal_plan_members_plan_idx", + "columns": [ + { + "expression": "meal_plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "meal_plan_members_user_idx": { + "name": "meal_plan_members_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "meal_plan_members_meal_plan_id_meal_plans_id_fk": { + "name": "meal_plan_members_meal_plan_id_meal_plans_id_fk", + "tableFrom": "meal_plan_members", + "tableTo": "meal_plans", + "columnsFrom": [ + "meal_plan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "meal_plan_members_user_id_users_id_fk": { + "name": "meal_plan_members_user_id_users_id_fk", + "tableFrom": "meal_plan_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.meal_plans": { + "name": "meal_plans", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "week_start": { + "name": "week_start", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "meal_plans_user_idx": { + "name": "meal_plans_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "meal_plans_user_week_idx": { + "name": "meal_plans_user_week_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "week_start", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "meal_plans_user_id_users_id_fk": { + "name": "meal_plans_user_id_users_id_fk", + "tableFrom": "meal_plans", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pantry_items": { + "name": "pantry_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ingredient_id": { + "name": "ingredient_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "raw_name": { + "name": "raw_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": false + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pantry_items_user_idx": { + "name": "pantry_items_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pantry_items_user_id_users_id_fk": { + "name": "pantry_items_user_id_users_id_fk", + "tableFrom": "pantry_items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pantry_items_ingredient_id_ingredients_id_fk": { + "name": "pantry_items_ingredient_id_ingredients_id_fk", + "tableFrom": "pantry_items", + "tableTo": "ingredients", + "columnsFrom": [ + "ingredient_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shopping_list_items": { + "name": "shopping_list_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "list_id": { + "name": "list_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ingredient_id": { + "name": "ingredient_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "raw_name": { + "name": "raw_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": false + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "aisle": { + "name": "aisle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "checked": { + "name": "checked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "in_pantry": { + "name": "in_pantry", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "shopping_list_items_list_id_shopping_lists_id_fk": { + "name": "shopping_list_items_list_id_shopping_lists_id_fk", + "tableFrom": "shopping_list_items", + "tableTo": "shopping_lists", + "columnsFrom": [ + "list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shopping_list_items_ingredient_id_ingredients_id_fk": { + "name": "shopping_list_items_ingredient_id_ingredients_id_fk", + "tableFrom": "shopping_list_items", + "tableTo": "ingredients", + "columnsFrom": [ + "ingredient_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shopping_list_members": { + "name": "shopping_list_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "list_id": { + "name": "list_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "collection_member_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'viewer'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "shopping_list_members_list_idx": { + "name": "shopping_list_members_list_idx", + "columns": [ + { + "expression": "list_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "shopping_list_members_user_idx": { + "name": "shopping_list_members_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shopping_list_members_list_id_shopping_lists_id_fk": { + "name": "shopping_list_members_list_id_shopping_lists_id_fk", + "tableFrom": "shopping_list_members", + "tableTo": "shopping_lists", + "columnsFrom": [ + "list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shopping_list_members_user_id_users_id_fk": { + "name": "shopping_list_members_user_id_users_id_fk", + "tableFrom": "shopping_list_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shopping_lists": { + "name": "shopping_lists", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "public_editable": { + "name": "public_editable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "generated_at": { + "name": "generated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "shopping_lists_user_id_users_id_fk": { + "name": "shopping_lists_user_id_users_id_fk", + "tableFrom": "shopping_lists", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_logs_created_idx": { + "name": "audit_logs_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invites": { + "name": "invites", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "user_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "tier": { + "name": "tier", + "type": "tier", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'free'" + }, + "created_by_id": { + "name": "created_by_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "used_at": { + "name": "used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "used_by_id": { + "name": "used_by_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "invites_created_by_id_users_id_fk": { + "name": "invites_created_by_id_users_id_fk", + "tableFrom": "invites", + "tableTo": "users", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invites_used_by_id_users_id_fk": { + "name": "invites_used_by_id_users_id_fk", + "tableFrom": "invites", + "tableTo": "users", + "columnsFrom": [ + "used_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invites_token_uniq": { + "name": "invites_token_uniq", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reports": { + "name": "reports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "reporter_id": { + "name": "reporter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "report_target_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "report_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "reviewed_by_id": { + "name": "reviewed_by_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "reports_status_idx": { + "name": "reports_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reports_reporter_id_users_id_fk": { + "name": "reports_reporter_id_users_id_fk", + "tableFrom": "reports", + "tableTo": "users", + "columnsFrom": [ + "reporter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "reports_reviewed_by_id_users_id_fk": { + "name": "reports_reviewed_by_id_users_id_fk", + "tableFrom": "reports", + "tableTo": "users", + "columnsFrom": [ + "reviewed_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.site_settings": { + "name": "site_settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_secret": { + "name": "is_secret", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_by_id": { + "name": "updated_by_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "site_settings_updated_by_id_users_id_fk": { + "name": "site_settings_updated_by_id_users_id_fk", + "tableFrom": "site_settings", + "tableTo": "users", + "columnsFrom": [ + "updated_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tier_definitions": { + "name": "tier_definitions", + "schema": "", + "columns": { + "tier": { + "name": "tier", + "type": "tier", + "typeSchema": "public", + "primaryKey": true, + "notNull": true + }, + "max_recipes": { + "name": "max_recipes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ai_calls_per_month": { + "name": "ai_calls_per_month", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "storage_mb": { + "name": "storage_mb", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "max_public_recipes": { + "name": "max_public_recipes", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_usage": { + "name": "user_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "month": { + "name": "month", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ai_calls_used": { + "name": "ai_calls_used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "recipe_count": { + "name": "recipe_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "storage_used_mb": { + "name": "storage_used_mb", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "user_usage_user_month_idx": { + "name": "user_usage_user_month_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "month", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_usage_user_id_users_id_fk": { + "name": "user_usage_user_id_users_id_fk", + "tableFrom": "user_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_usage_user_month_uniq": { + "name": "user_usage_user_month_uniq", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "month" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_deliveries": { + "name": "webhook_deliveries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhook_id": { + "name": "webhook_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event": { + "name": "event", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "webhook_deliveries_webhook_id_webhooks_id_fk": { + "name": "webhook_deliveries_webhook_id_webhooks_id_fk", + "tableFrom": "webhook_deliveries", + "tableTo": "webhooks", + "columnsFrom": [ + "webhook_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhooks": { + "name": "webhooks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "events": { + "name": "events", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "webhooks_user_id_users_id_fk": { + "name": "webhooks_user_id_users_id_fk", + "tableFrom": "webhooks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.conversation_reads": { + "name": "conversation_reads", + "schema": "", + "columns": { + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_read_at": { + "name": "last_read_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "conversation_reads_conversation_id_conversations_id_fk": { + "name": "conversation_reads_conversation_id_conversations_id_fk", + "tableFrom": "conversation_reads", + "tableTo": "conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "conversation_reads_user_id_users_id_fk": { + "name": "conversation_reads_user_id_users_id_fk", + "tableFrom": "conversation_reads", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "conversation_reads_conversation_id_user_id_pk": { + "name": "conversation_reads_conversation_id_user_id_pk", + "columns": [ + "conversation_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.conversations": { + "name": "conversations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_a_id": { + "name": "user_a_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_b_id": { + "name": "user_b_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_message_at": { + "name": "last_message_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "conversations_last_message_idx": { + "name": "conversations_last_message_idx", + "columns": [ + { + "expression": "last_message_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "conversations_user_a_id_users_id_fk": { + "name": "conversations_user_a_id_users_id_fk", + "tableFrom": "conversations", + "tableTo": "users", + "columnsFrom": [ + "user_a_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "conversations_user_b_id_users_id_fk": { + "name": "conversations_user_b_id_users_id_fk", + "tableFrom": "conversations", + "tableTo": "users", + "columnsFrom": [ + "user_b_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "conversations_pair_uniq": { + "name": "conversations_pair_uniq", + "nullsNotDistinct": false, + "columns": [ + "user_a_id", + "user_b_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.messages": { + "name": "messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sender_id": { + "name": "sender_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "messages_conversation_idx": { + "name": "messages_conversation_idx", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "messages_conversation_id_conversations_id_fk": { + "name": "messages_conversation_id_conversations_id_fk", + "tableFrom": "messages", + "tableTo": "conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "messages_sender_id_users_id_fk": { + "name": "messages_sender_id_users_id_fk", + "tableFrom": "messages", + "tableTo": "users", + "columnsFrom": [ + "sender_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.processed_stripe_events": { + "name": "processed_stripe_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_conversations": { + "name": "ai_conversations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ai_conversations_user_idx": { + "name": "ai_conversations_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_conversations_user_id_users_id_fk": { + "name": "ai_conversations_user_id_users_id_fk", + "tableFrom": "ai_conversations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_messages": { + "name": "chat_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recipe_id": { + "name": "recipe_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "chat_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "chat_messages_user_idx": { + "name": "chat_messages_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_messages_recipe_idx": { + "name": "chat_messages_recipe_idx", + "columns": [ + { + "expression": "recipe_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_messages_conversation_idx": { + "name": "chat_messages_conversation_idx", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_messages_user_id_users_id_fk": { + "name": "chat_messages_user_id_users_id_fk", + "tableFrom": "chat_messages", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_messages_recipe_id_recipes_id_fk": { + "name": "chat_messages_recipe_id_recipes_id_fk", + "tableFrom": "chat_messages", + "tableTo": "recipes", + "columnsFrom": [ + "recipe_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_messages_conversation_id_ai_conversations_id_fk": { + "name": "chat_messages_conversation_id_ai_conversations_id_fk", + "tableFrom": "chat_messages", + "tableTo": "ai_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.support_ticket_attachments": { + "name": "support_ticket_attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "ticket_id": { + "name": "ticket_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "support_ticket_attachments_ticket_idx": { + "name": "support_ticket_attachments_ticket_idx", + "columns": [ + { + "expression": "ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "support_ticket_attachments_ticket_id_support_tickets_id_fk": { + "name": "support_ticket_attachments_ticket_id_support_tickets_id_fk", + "tableFrom": "support_ticket_attachments", + "tableTo": "support_tickets", + "columnsFrom": [ + "ticket_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.support_tickets": { + "name": "support_tickets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "support_ticket_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "support_ticket_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "gitea_issue_url": { + "name": "gitea_issue_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitea_error": { + "name": "gitea_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "support_tickets_user_idx": { + "name": "support_tickets_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "support_tickets_status_idx": { + "name": "support_tickets_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "support_tickets_user_id_users_id_fk": { + "name": "support_tickets_user_id_users_id_fk", + "tableFrom": "support_tickets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.feature_flags": { + "name": "feature_flags", + "schema": "", + "columns": { + "feature_key": { + "name": "feature_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tier": { + "name": "tier", + "type": "tier", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_by_id": { + "name": "updated_by_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "feature_flags_updated_by_id_users_id_fk": { + "name": "feature_flags_updated_by_id_users_id_fk", + "tableFrom": "feature_flags", + "tableTo": "users", + "columnsFrom": [ + "updated_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "feature_flags_feature_key_tier_pk": { + "name": "feature_flags_feature_key_tier_pk", + "columns": [ + "feature_key", + "tier" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.api_key_scope": { + "name": "api_key_scope", + "schema": "public", + "values": [ + "full", + "read" + ] + }, + "public.tier": { + "name": "tier", + "schema": "public", + "values": [ + "free", + "pro", + "family" + ] + }, + "public.unit_pref": { + "name": "unit_pref", + "schema": "public", + "values": [ + "metric", + "imperial" + ] + }, + "public.user_role": { + "name": "user_role", + "schema": "public", + "values": [ + "user", + "moderator", + "admin" + ] + }, + "public.difficulty": { + "name": "difficulty", + "schema": "public", + "values": [ + "easy", + "medium", + "hard" + ] + }, + "public.recipe_type": { + "name": "recipe_type", + "schema": "public", + "values": [ + "dish", + "drink" + ] + }, + "public.visibility": { + "name": "visibility", + "schema": "public", + "values": [ + "private", + "unlisted", + "public", + "followers" + ] + }, + "public.collection_member_role": { + "name": "collection_member_role", + "schema": "public", + "values": [ + "viewer", + "editor" + ] + }, + "public.comment_reaction_type": { + "name": "comment_reaction_type", + "schema": "public", + "values": [ + "like", + "love", + "laugh", + "wow", + "sad", + "fire" + ] + }, + "public.notification_type": { + "name": "notification_type", + "schema": "public", + "values": [ + "follow", + "comment", + "reply", + "reaction", + "rating", + "mention" + ] + }, + "public.meal_type": { + "name": "meal_type", + "schema": "public", + "values": [ + "breakfast", + "lunch", + "dinner", + "snack" + ] + }, + "public.weekday": { + "name": "weekday", + "schema": "public", + "values": [ + "mon", + "tue", + "wed", + "thu", + "fri", + "sat", + "sun" + ] + }, + "public.report_status": { + "name": "report_status", + "schema": "public", + "values": [ + "pending", + "reviewed", + "dismissed" + ] + }, + "public.report_target_type": { + "name": "report_target_type", + "schema": "public", + "values": [ + "recipe", + "comment", + "user" + ] + }, + "public.chat_role": { + "name": "chat_role", + "schema": "public", + "values": [ + "user", + "assistant" + ] + }, + "public.support_ticket_status": { + "name": "support_ticket_status", + "schema": "public", + "values": [ + "open", + "triaged", + "closed" + ] + }, + "public.support_ticket_type": { + "name": "support_ticket_type", + "schema": "public", + "values": [ + "bug", + "suggestion", + "question" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/src/migrations/meta/_journal.json b/packages/db/src/migrations/meta/_journal.json index f2024dc..4d81f04 100644 --- a/packages/db/src/migrations/meta/_journal.json +++ b/packages/db/src/migrations/meta/_journal.json @@ -337,6 +337,13 @@ "when": 1784394631882, "tag": "0047_acoustic_goblin_queen", "breakpoints": true + }, + { + "idx": 48, + "version": "7", + "when": 1784449674049, + "tag": "0048_normal_shiver_man", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/schema/recipes.ts b/packages/db/src/schema/recipes.ts index 47d855a..7a73869 100644 --- a/packages/db/src/schema/recipes.ts +++ b/packages/db/src/schema/recipes.ts @@ -49,6 +49,10 @@ export const recipes = pgTable("recipes", { 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) => [