diff --git a/apps/web/app/(app)/recipes/[id]/page.tsx b/apps/web/app/(app)/recipes/[id]/page.tsx
index 3f0a4e7..2b89ef1 100644
--- a/apps/web/app/(app)/recipes/[id]/page.tsx
+++ b/apps/web/app/(app)/recipes/[id]/page.tsx
@@ -26,6 +26,7 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/comp
import { ServingScaler } from "@/components/recipe/serving-scaler";
import { FavoriteButton } from "@/components/social/favorite-button";
import { RatingStars } from "@/components/social/rating-stars";
+import { CookedItReview } from "@/components/social/cooked-it-review";
import { CommentsSection } from "@/components/social/comments-section";
import { getPublicUrl } from "@/lib/storage";
import { cn } from "@/lib/utils";
@@ -386,7 +387,16 @@ export default async function RecipePage({ params }: Params) {
<>
-
+ {isOwner ? (
+
+ ) : (
+
+ )}
diff --git a/apps/web/app/api/v1/recipes/[id]/rate/route.ts b/apps/web/app/api/v1/recipes/[id]/rate/route.ts
index 3238c9b..f256877 100644
--- a/apps/web/app/api/v1/recipes/[id]/rate/route.ts
+++ b/apps/web/app/api/v1/recipes/[id]/rate/route.ts
@@ -7,6 +7,7 @@ import { createNotification } from "@/lib/notifications";
const Schema = z.object({
score: z.number().int().min(1).max(5),
reviewText: z.string().max(2000).optional(),
+ photoKey: z.string().max(500).optional(),
});
type Params = { params: Promise<{ id: string }> };
@@ -34,7 +35,12 @@ export async function POST(req: NextRequest, { params }: Params) {
if (existing) {
await db.update(ratings)
- .set({ score: parsed.data.score, reviewText: parsed.data.reviewText, updatedAt: new Date() })
+ .set({
+ score: parsed.data.score,
+ reviewText: parsed.data.reviewText,
+ photoKey: parsed.data.photoKey,
+ updatedAt: new Date(),
+ })
.where(eq(ratings.id, existing.id));
return NextResponse.json({ updated: true });
}
@@ -45,6 +51,7 @@ export async function POST(req: NextRequest, { params }: Params) {
userId: session!.user.id,
score: parsed.data.score,
reviewText: parsed.data.reviewText,
+ photoKey: parsed.data.photoKey,
});
void createNotification({ userId: recipe.authorId, type: "rating", actorId: session!.user.id, recipeId: id });
return NextResponse.json({ created: true }, { status: 201 });
diff --git a/apps/web/app/api/v1/recipes/[id]/reviews/route.ts b/apps/web/app/api/v1/recipes/[id]/reviews/route.ts
new file mode 100644
index 0000000..b740f03
--- /dev/null
+++ b/apps/web/app/api/v1/recipes/[id]/reviews/route.ts
@@ -0,0 +1,42 @@
+import { NextRequest, NextResponse } from "next/server";
+import { db, recipes, ratings, eq, and, or, inArray, desc } from "@epicure/db";
+import { requireSession } from "@/lib/api-auth";
+
+type Params = { params: Promise<{ id: string }> };
+
+export async function GET(_req: NextRequest, { params }: Params) {
+ const { session, response } = await requireSession();
+ if (response) return response;
+ const { id } = await params;
+
+ const recipe = await db.query.recipes.findFirst({
+ where: and(
+ eq(recipes.id, id),
+ or(eq(recipes.authorId, session!.user.id), inArray(recipes.visibility, ["public", "unlisted"]))
+ ),
+ columns: { id: true },
+ });
+ if (!recipe) return NextResponse.json({ error: "Not found" }, { status: 404 });
+
+ const rows = await db.query.ratings.findMany({
+ where: eq(ratings.recipeId, id),
+ orderBy: desc(ratings.createdAt),
+ limit: 50,
+ with: {
+ user: { columns: { id: true, name: true, username: true, avatarUrl: true } },
+ },
+ });
+
+ const reviews = rows
+ .filter((r) => r.reviewText || r.photoKey)
+ .map((r) => ({
+ id: r.id,
+ score: r.score,
+ reviewText: r.reviewText,
+ photoKey: r.photoKey,
+ createdAt: r.createdAt,
+ user: r.user,
+ }));
+
+ return NextResponse.json({ data: reviews });
+}
diff --git a/apps/web/app/api/v1/upload/presign/route.ts b/apps/web/app/api/v1/upload/presign/route.ts
index 18a20fa..c9c5e3e 100644
--- a/apps/web/app/api/v1/upload/presign/route.ts
+++ b/apps/web/app/api/v1/upload/presign/route.ts
@@ -12,6 +12,7 @@ const Schema = z.object({
message: "Content type must be jpeg, png, webp, or avif",
}),
recipeId: z.string().uuid(),
+ purpose: z.enum(["recipe", "review"]).default("recipe"),
});
export async function POST(req: NextRequest) {
@@ -24,15 +25,22 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ error: "Validation error", issues: parsed.error.issues }, { status: 400 });
}
- const owned = await db.query.recipes.findFirst({
- where: and(eq(recipes.id, parsed.data.recipeId), eq(recipes.authorId, session!.user.id)),
- columns: { id: true },
+ const { recipeId, contentType, purpose } = parsed.data;
+ const recipe = await db.query.recipes.findFirst({
+ where: purpose === "recipe"
+ ? and(eq(recipes.id, recipeId), eq(recipes.authorId, session!.user.id))
+ : eq(recipes.id, recipeId),
+ columns: { id: true, visibility: true, authorId: true },
});
- if (!owned) return NextResponse.json({ error: "Not found" }, { status: 404 });
+ if (!recipe) return NextResponse.json({ error: "Not found" }, { status: 404 });
+ if (purpose === "review" && recipe.visibility === "private" && recipe.authorId !== session!.user.id) {
+ return NextResponse.json({ error: "Not found" }, { status: 404 });
+ }
- const ext = parsed.data.contentType.split("/")[1] ?? "jpg";
- const key = `recipes/${parsed.data.recipeId}/photos/${session!.user.id}-${crypto.randomUUID()}.${ext}`;
- const url = await createPresignedUploadUrl(key, parsed.data.contentType);
+ const ext = contentType.split("/")[1] ?? "jpg";
+ const folder = purpose === "review" ? "reviews" : "photos";
+ const key = `recipes/${recipeId}/${folder}/${session!.user.id}-${crypto.randomUUID()}.${ext}`;
+ const url = await createPresignedUploadUrl(key, contentType);
return NextResponse.json({ url, key });
}
diff --git a/apps/web/components/social/cooked-it-review.tsx b/apps/web/components/social/cooked-it-review.tsx
new file mode 100644
index 0000000..801329e
--- /dev/null
+++ b/apps/web/components/social/cooked-it-review.tsx
@@ -0,0 +1,220 @@
+"use client";
+
+import { useState, useEffect, useRef } from "react";
+import { useTranslations, useLocale } from "next-intl";
+import { Star, Camera, X, ChefHat } from "lucide-react";
+import { toast } from "sonner";
+import { Button } from "@/components/ui/button";
+import { Textarea } from "@/components/ui/textarea";
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
+import { getPublicUrl } from "@/lib/storage";
+import { cn } from "@/lib/utils";
+
+type Review = {
+ id: string;
+ score: number;
+ reviewText: string | null;
+ photoKey: string | null;
+ createdAt: string;
+ user: { id: string; name: string; username: string | null; avatarUrl: string | null };
+};
+
+function Stars({
+ value,
+ hovered,
+ onHover,
+ onPick,
+}: {
+ value: number;
+ hovered: number;
+ onHover: (v: number) => void;
+ onPick: (v: number) => void;
+}) {
+ const display = hovered || value;
+ return (
+
+ {[1, 2, 3, 4, 5].map((i) => (
+
+ ))}
+
+ );
+}
+
+export function CookedItReview({
+ recipeId,
+ initialScore = 0,
+ initialText = "",
+ initialPhotoKey = null,
+}: {
+ recipeId: string;
+ initialScore?: number;
+ initialText?: string;
+ initialPhotoKey?: string | null;
+}) {
+ const t = useTranslations("social");
+ const locale = useLocale();
+ const [score, setScore] = useState(initialScore);
+ const [hovered, setHovered] = useState(0);
+ const [text, setText] = useState(initialText);
+ const [photoKey, setPhotoKey] = useState(initialPhotoKey);
+ const [preview, setPreview] = useState(null);
+ const [uploading, setUploading] = useState(false);
+ const [saving, setSaving] = useState(false);
+ const [reviews, setReviews] = useState([]);
+ const [loadingReviews, setLoadingReviews] = useState(true);
+ const inputRef = useRef(null);
+
+ useEffect(() => {
+ fetch(`/api/v1/recipes/${recipeId}/reviews`)
+ .then((res) => (res.ok ? res.json() : null))
+ .then((data: { data: Review[] } | null) => setReviews(data?.data ?? []))
+ .finally(() => setLoadingReviews(false));
+ }, [recipeId]);
+
+ async function handlePhoto(file: File) {
+ setUploading(true);
+ try {
+ const res = await fetch("/api/v1/upload/presign", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ recipeId, contentType: file.type, purpose: "review" }),
+ });
+ if (!res.ok) {
+ toast.error(t("reviewPhotoFailed"));
+ return;
+ }
+ const { url, key } = (await res.json()) as { url: string; key: string };
+ await fetch(url, { method: "PUT", body: file, headers: { "Content-Type": file.type } });
+ setPhotoKey(key);
+ setPreview(URL.createObjectURL(file));
+ } finally {
+ setUploading(false);
+ }
+ }
+
+ async function submit() {
+ if (score < 1) return;
+ setSaving(true);
+ try {
+ const res = await fetch(`/api/v1/recipes/${recipeId}/rate`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ score, reviewText: text.trim() || undefined, photoKey: photoKey ?? undefined }),
+ });
+ if (!res.ok) {
+ const err = (await res.json()) as { error?: string };
+ toast.error(err.error ?? t("ratingFailed"));
+ return;
+ }
+ toast.success(t("ratingSaved"));
+ const listRes = await fetch(`/api/v1/recipes/${recipeId}/reviews`);
+ if (listRes.ok) {
+ const data = (await listRes.json()) as { data: Review[] };
+ setReviews(data.data);
+ }
+ } finally {
+ setSaving(false);
+ }
+ }
+
+ return (
+
+
+
+
+ {t("cookedItPrompt")}
+
+
+
+
+ {!loadingReviews && reviews.length > 0 && (
+
+
{t("reviewsTitle", { count: reviews.length })}
+ {reviews.map((r) => (
+
+
+
+ {r.user.name.slice(0, 2).toUpperCase()}
+
+
+
+
{r.user.name}
+
+ {[1, 2, 3, 4, 5].map((i) => (
+
+ ))}
+
+
+ {new Date(r.createdAt).toLocaleDateString(locale)}
+
+
+ {r.reviewText &&
{r.reviewText}
}
+ {r.photoKey && (
+
})
+ )}
+
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json
index 33fb6dd..642726a 100644
--- a/apps/web/messages/en.json
+++ b/apps/web/messages/en.json
@@ -744,6 +744,14 @@
"replyButton": "Reply",
"reportButton": "Report",
"ratingFailed": "Failed to rate",
+ "cookedItPrompt": "Cooked it? Rate it and share a photo",
+ "reviewTextPlaceholder": "How did it turn out?",
+ "reviewPhotoAdd": "Add photo",
+ "reviewPhotoUploading": "Uploading…",
+ "reviewPhotoFailed": "Failed to upload photo",
+ "reviewSubmit": "Post review",
+ "reviewSubmitting": "Posting…",
+ "reviewsTitle": "{count, plural, one {# review} other {# reviews}}",
"justNow": "just now",
"minutesAgo": "{mins}m ago",
"hoursAgo": "{hours}h ago",
diff --git a/apps/web/messages/fr.json b/apps/web/messages/fr.json
index 71c35c1..8f91188 100644
--- a/apps/web/messages/fr.json
+++ b/apps/web/messages/fr.json
@@ -732,6 +732,14 @@
"replyButton": "Répondre",
"reportButton": "Signaler",
"ratingFailed": "Échec de la notation",
+ "cookedItPrompt": "Vous l'avez cuisiné ? Notez-la et partagez une photo",
+ "reviewTextPlaceholder": "Comment était le résultat ?",
+ "reviewPhotoAdd": "Ajouter une photo",
+ "reviewPhotoUploading": "Envoi…",
+ "reviewPhotoFailed": "Échec de l'envoi de la photo",
+ "reviewSubmit": "Publier l'avis",
+ "reviewSubmitting": "Publication…",
+ "reviewsTitle": "{count, plural, one {# avis} other {# avis}}",
"justNow": "à l'instant",
"minutesAgo": "il y a {mins} min",
"hoursAgo": "il y a {hours} h",
diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts
index f5feba4..addf140 100644
--- a/apps/web/next.config.ts
+++ b/apps/web/next.config.ts
@@ -1,5 +1,8 @@
import type { NextConfig } from "next";
+const storagePublicUrl = process.env["STORAGE_PUBLIC_URL"] ?? "http://localhost:9000";
+const storageOrigin = new URL(storagePublicUrl).origin;
+
const securityHeaders = [
{
key: "X-Content-Type-Options",
@@ -31,9 +34,9 @@ const securityHeaders = [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval'", // unsafe-eval needed by Next.js dev; tighten in prod
"style-src 'self' 'unsafe-inline'",
- "img-src 'self' data: blob:",
+ `img-src 'self' data: blob: ${storageOrigin}`,
"font-src 'self'",
- "connect-src 'self'",
+ `connect-src 'self' ${storageOrigin}`,
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
diff --git a/packages/db/src/migrations/0021_mysterious_madame_masque.sql b/packages/db/src/migrations/0021_mysterious_madame_masque.sql
new file mode 100644
index 0000000..26e81d7
--- /dev/null
+++ b/packages/db/src/migrations/0021_mysterious_madame_masque.sql
@@ -0,0 +1,2 @@
+ALTER TABLE "ratings" ADD COLUMN "photo_key" text;--> statement-breakpoint
+CREATE INDEX "ratings_recipe_idx" ON "ratings" USING btree ("recipe_id");
\ No newline at end of file
diff --git a/packages/db/src/migrations/meta/0021_snapshot.json b/packages/db/src/migrations/meta/0021_snapshot.json
new file mode 100644
index 0000000..fd7b979
--- /dev/null
+++ b/packages/db/src/migrations/meta/0021_snapshot.json
@@ -0,0 +1,4179 @@
+{
+ "id": "743335d7-dd29-42d1-bde0-7d7ab55e58b6",
+ "prevId": "9919c538-994d-4617-adcd-e4a86a336cad",
+ "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
+ },
+ "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.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_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
+ },
+ "bio": {
+ "name": "bio",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "private_bio": {
+ "name": "private_bio",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": 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'"
+ },
+ "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_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": {},
+ "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": {},
+ "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
+ }
+ },
+ "indexes": {},
+ "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
+ },
+ "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
+ },
+ "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": "'{}'"
+ },
+ "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": {}
+ }
+ },
+ "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_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"
+ }
+ },
+ "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
+ },
+ "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
+ }
+ },
+ "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": {}
+ }
+ },
+ "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"
+ }
+ },
+ "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
+ },
+ "servings": {
+ "name": "servings",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 2
+ },
+ "note": {
+ "name": "note",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "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"
+ }
+ },
+ "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": {},
+ "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": {},
+ "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
+ }
+ },
+ "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
+ },
+ "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
+ }
+ },
+ "enums": {
+ "public.tier": {
+ "name": "tier",
+ "schema": "public",
+ "values": [
+ "free",
+ "pro"
+ ]
+ },
+ "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.visibility": {
+ "name": "visibility",
+ "schema": "public",
+ "values": [
+ "private",
+ "unlisted",
+ "public"
+ ]
+ },
+ "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"
+ ]
+ }
+ },
+ "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 06ad03d..adc3012 100644
--- a/packages/db/src/migrations/meta/_journal.json
+++ b/packages/db/src/migrations/meta/_journal.json
@@ -148,6 +148,13 @@
"when": 1783109825851,
"tag": "0020_acoustic_exiles",
"breakpoints": true
+ },
+ {
+ "idx": 21,
+ "version": "7",
+ "when": 1783602591480,
+ "tag": "0021_mysterious_madame_masque",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/packages/db/src/schema/social.ts b/packages/db/src/schema/social.ts
index 42669d0..468923e 100644
--- a/packages/db/src/schema/social.ts
+++ b/packages/db/src/schema/social.ts
@@ -26,10 +26,12 @@ export const ratings = pgTable("ratings", {
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
score: integer("score").notNull(),
reviewText: text("review_text"),
+ photoKey: text("photo_key"),
createdAt: timestamp("created_at").notNull().defaultNow(),
updatedAt: timestamp("updated_at").notNull().defaultNow(),
}, (t) => [
index("ratings_user_idx").on(t.userId),
+ index("ratings_recipe_idx").on(t.recipeId),
]);
export const favorites = pgTable("favorites", {