feat(db): Drizzle ORM schema and migrations
Schema domains: users/auth, recipes, social, meal-planning, tiers/usage, webhooks. Includes Better Auth tables, audit_logs, site_settings, push_subscriptions, user_model_prefs, user_nutrition_goals. Migrations 0000–0008 applied.
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
import { defineConfig } from "drizzle-kit";
|
||||
|
||||
export default defineConfig({
|
||||
schema: "./src/schema/index.ts",
|
||||
out: "./src/migrations",
|
||||
dialect: "postgresql",
|
||||
dbCredentials: {
|
||||
url: process.env["DATABASE_URL"] ?? "postgresql://epicure:epicure@localhost:5432/epicure",
|
||||
},
|
||||
verbose: true,
|
||||
strict: true,
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@epicure/db",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./schema": "./src/schema/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"generate": "drizzle-kit generate",
|
||||
"migrate": "drizzle-kit migrate",
|
||||
"studio": "drizzle-kit studio",
|
||||
"seed": "tsx src/seed.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"drizzle-orm": "^0.44.7",
|
||||
"postgres": "^3.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"drizzle-kit": "^0.31.1",
|
||||
"tsx": "^4.20.3",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { drizzle } from "drizzle-orm/postgres-js";
|
||||
import postgres from "postgres";
|
||||
import * as schema from "./schema";
|
||||
|
||||
const connectionString =
|
||||
process.env["DATABASE_URL"] ?? "postgresql://epicure:epicure@localhost:5432/epicure";
|
||||
|
||||
const client = postgres(connectionString, { max: 10 });
|
||||
|
||||
export const db = drizzle(client, { schema });
|
||||
export type Db = typeof db;
|
||||
@@ -0,0 +1,5 @@
|
||||
export { db } from "./client";
|
||||
export type { Db } from "./client";
|
||||
export * from "./schema";
|
||||
// Re-export query helpers so all callers use the same drizzle-orm instance
|
||||
export { eq, and, or, desc, asc, gt, lt, gte, lte, ne, inArray, isNull, isNotNull, sql, count, avg, sum, min, max, ilike, like } from "drizzle-orm";
|
||||
@@ -0,0 +1,338 @@
|
||||
CREATE TYPE "public"."tier" AS ENUM('free', 'pro');--> statement-breakpoint
|
||||
CREATE TYPE "public"."unit_pref" AS ENUM('metric', 'imperial');--> statement-breakpoint
|
||||
CREATE TYPE "public"."user_role" AS ENUM('user', 'moderator', 'admin');--> statement-breakpoint
|
||||
CREATE TYPE "public"."difficulty" AS ENUM('easy', 'medium', 'hard');--> statement-breakpoint
|
||||
CREATE TYPE "public"."visibility" AS ENUM('private', 'unlisted', 'public');--> statement-breakpoint
|
||||
CREATE TYPE "public"."feed_item_type" AS ENUM('new_recipe', 'new_follow', 'recipe_rated');--> statement-breakpoint
|
||||
CREATE TYPE "public"."meal_type" AS ENUM('breakfast', 'lunch', 'dinner', 'snack');--> statement-breakpoint
|
||||
CREATE TYPE "public"."weekday" AS ENUM('mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun');--> statement-breakpoint
|
||||
CREATE TABLE "accounts" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"account_id" text NOT NULL,
|
||||
"provider_id" text NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"access_token" text,
|
||||
"refresh_token" text,
|
||||
"id_token" text,
|
||||
"access_token_expires_at" timestamp,
|
||||
"refresh_token_expires_at" timestamp,
|
||||
"scope" text,
|
||||
"password" text,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "api_keys" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"key_hash" text NOT NULL,
|
||||
"last_used_at" timestamp,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "api_keys_key_hash_unique" UNIQUE("key_hash")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "sessions" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"expires_at" timestamp NOT NULL,
|
||||
"token" text NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL,
|
||||
"ip_address" text,
|
||||
"user_agent" text,
|
||||
"user_id" text NOT NULL,
|
||||
CONSTRAINT "sessions_token_unique" UNIQUE("token")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "user_follows" (
|
||||
"follower_id" text NOT NULL,
|
||||
"following_id" text NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "users" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"email" text NOT NULL,
|
||||
"email_verified" boolean DEFAULT false NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"avatar_url" text,
|
||||
"bio" text,
|
||||
"username" text,
|
||||
"role" "user_role" DEFAULT 'user' NOT NULL,
|
||||
"tier" "tier" DEFAULT 'free' NOT NULL,
|
||||
"unit_pref" "unit_pref" DEFAULT 'metric' NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "users_email_unique" UNIQUE("email"),
|
||||
CONSTRAINT "users_username_unique" UNIQUE("username")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "verifications" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"identifier" text NOT NULL,
|
||||
"value" text NOT NULL,
|
||||
"expires_at" timestamp NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "ingredients" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"aliases" text[] DEFAULT '{}' NOT NULL,
|
||||
"category" text,
|
||||
"known_allergens" text[] DEFAULT '{}' NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "ingredients_name_unique" UNIQUE("name")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "recipe_ingredients" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"recipe_id" text NOT NULL,
|
||||
"ingredient_id" text,
|
||||
"raw_name" text NOT NULL,
|
||||
"quantity" numeric(10, 4),
|
||||
"unit" text,
|
||||
"note" text,
|
||||
"order" integer DEFAULT 0 NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "recipe_notes" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"recipe_id" text NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"content" text NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "recipe_photos" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"recipe_id" text NOT NULL,
|
||||
"storage_key" text NOT NULL,
|
||||
"order" integer DEFAULT 0 NOT NULL,
|
||||
"is_cover" boolean DEFAULT false NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "recipe_steps" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"recipe_id" text NOT NULL,
|
||||
"order" integer NOT NULL,
|
||||
"instruction" text NOT NULL,
|
||||
"timer_seconds" integer,
|
||||
"photo_url" text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "recipe_variations" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"parent_recipe_id" text NOT NULL,
|
||||
"child_recipe_id" text NOT NULL,
|
||||
"description" text,
|
||||
"ai_generated" boolean DEFAULT false NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "recipes" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"author_id" text NOT NULL,
|
||||
"title" text NOT NULL,
|
||||
"description" text,
|
||||
"base_servings" integer DEFAULT 4 NOT NULL,
|
||||
"visibility" "visibility" DEFAULT 'private' NOT NULL,
|
||||
"source_url" text,
|
||||
"ai_generated" boolean DEFAULT false NOT NULL,
|
||||
"ai_model" text,
|
||||
"ai_prompt" text,
|
||||
"dietary_tags" jsonb DEFAULT '{}'::jsonb,
|
||||
"dietary_verified" boolean DEFAULT false NOT NULL,
|
||||
"difficulty" "difficulty",
|
||||
"prep_mins" integer,
|
||||
"cook_mins" integer,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "user_allergens" (
|
||||
"user_id" text NOT NULL,
|
||||
"allergen_tag" text NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "collection_recipes" (
|
||||
"collection_id" text NOT NULL,
|
||||
"recipe_id" text NOT NULL,
|
||||
"added_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "collections" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"description" text,
|
||||
"is_public" boolean DEFAULT false NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "comments" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"recipe_id" text NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"parent_id" text,
|
||||
"content" text NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "cooking_history" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"recipe_id" text NOT NULL,
|
||||
"cooked_at" timestamp DEFAULT now() NOT NULL,
|
||||
"servings" integer,
|
||||
"notes" text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "favorites" (
|
||||
"user_id" text NOT NULL,
|
||||
"recipe_id" text NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "feed_items" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"type" "feed_item_type" NOT NULL,
|
||||
"actor_id" text NOT NULL,
|
||||
"subject_id" text NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "ratings" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"recipe_id" text NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"score" integer NOT NULL,
|
||||
"review_text" text,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "meal_plan_entries" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"meal_plan_id" text NOT NULL,
|
||||
"day" "weekday" NOT NULL,
|
||||
"meal_type" "meal_type" NOT NULL,
|
||||
"recipe_id" text,
|
||||
"servings" integer DEFAULT 2 NOT NULL,
|
||||
"note" text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "meal_plans" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"week_start" date NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "pantry_items" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"ingredient_id" text,
|
||||
"raw_name" text NOT NULL,
|
||||
"quantity" numeric(10, 4),
|
||||
"unit" text,
|
||||
"expires_at" timestamp,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "shopping_list_items" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"list_id" text NOT NULL,
|
||||
"ingredient_id" text,
|
||||
"raw_name" text NOT NULL,
|
||||
"quantity" numeric(10, 4),
|
||||
"unit" text,
|
||||
"aisle" text,
|
||||
"checked" boolean DEFAULT false NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "shopping_lists" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"user_id" text,
|
||||
"name" text NOT NULL,
|
||||
"generated_at" timestamp,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "audit_logs" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"user_id" text,
|
||||
"action" text NOT NULL,
|
||||
"target_type" text,
|
||||
"target_id" text,
|
||||
"metadata" text,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "tier_definitions" (
|
||||
"tier" "tier" PRIMARY KEY NOT NULL,
|
||||
"max_recipes" integer NOT NULL,
|
||||
"ai_calls_per_month" integer NOT NULL,
|
||||
"storage_mb" integer NOT NULL,
|
||||
"max_public_recipes" integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "user_usage" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"month" text NOT NULL,
|
||||
"ai_calls_used" integer DEFAULT 0 NOT NULL,
|
||||
"recipe_count" integer DEFAULT 0 NOT NULL,
|
||||
"storage_used_mb" integer DEFAULT 0 NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "accounts" ADD CONSTRAINT "accounts_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "api_keys" ADD CONSTRAINT "api_keys_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "user_follows" ADD CONSTRAINT "user_follows_follower_id_users_id_fk" FOREIGN KEY ("follower_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "user_follows" ADD CONSTRAINT "user_follows_following_id_users_id_fk" FOREIGN KEY ("following_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "recipe_ingredients" ADD CONSTRAINT "recipe_ingredients_recipe_id_recipes_id_fk" FOREIGN KEY ("recipe_id") REFERENCES "public"."recipes"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "recipe_ingredients" ADD CONSTRAINT "recipe_ingredients_ingredient_id_ingredients_id_fk" FOREIGN KEY ("ingredient_id") REFERENCES "public"."ingredients"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "recipe_notes" ADD CONSTRAINT "recipe_notes_recipe_id_recipes_id_fk" FOREIGN KEY ("recipe_id") REFERENCES "public"."recipes"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "recipe_notes" ADD CONSTRAINT "recipe_notes_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "recipe_photos" ADD CONSTRAINT "recipe_photos_recipe_id_recipes_id_fk" FOREIGN KEY ("recipe_id") REFERENCES "public"."recipes"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "recipe_steps" ADD CONSTRAINT "recipe_steps_recipe_id_recipes_id_fk" FOREIGN KEY ("recipe_id") REFERENCES "public"."recipes"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "recipe_variations" ADD CONSTRAINT "recipe_variations_parent_recipe_id_recipes_id_fk" FOREIGN KEY ("parent_recipe_id") REFERENCES "public"."recipes"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "recipe_variations" ADD CONSTRAINT "recipe_variations_child_recipe_id_recipes_id_fk" FOREIGN KEY ("child_recipe_id") REFERENCES "public"."recipes"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "recipes" ADD CONSTRAINT "recipes_author_id_users_id_fk" FOREIGN KEY ("author_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "user_allergens" ADD CONSTRAINT "user_allergens_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "collection_recipes" ADD CONSTRAINT "collection_recipes_collection_id_collections_id_fk" FOREIGN KEY ("collection_id") REFERENCES "public"."collections"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "collection_recipes" ADD CONSTRAINT "collection_recipes_recipe_id_recipes_id_fk" FOREIGN KEY ("recipe_id") REFERENCES "public"."recipes"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "collections" ADD CONSTRAINT "collections_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "comments" ADD CONSTRAINT "comments_recipe_id_recipes_id_fk" FOREIGN KEY ("recipe_id") REFERENCES "public"."recipes"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "comments" ADD CONSTRAINT "comments_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "cooking_history" ADD CONSTRAINT "cooking_history_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "cooking_history" ADD CONSTRAINT "cooking_history_recipe_id_recipes_id_fk" FOREIGN KEY ("recipe_id") REFERENCES "public"."recipes"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "favorites" ADD CONSTRAINT "favorites_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "favorites" ADD CONSTRAINT "favorites_recipe_id_recipes_id_fk" FOREIGN KEY ("recipe_id") REFERENCES "public"."recipes"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "feed_items" ADD CONSTRAINT "feed_items_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "feed_items" ADD CONSTRAINT "feed_items_actor_id_users_id_fk" FOREIGN KEY ("actor_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "ratings" ADD CONSTRAINT "ratings_recipe_id_recipes_id_fk" FOREIGN KEY ("recipe_id") REFERENCES "public"."recipes"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "ratings" ADD CONSTRAINT "ratings_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "meal_plan_entries" ADD CONSTRAINT "meal_plan_entries_meal_plan_id_meal_plans_id_fk" FOREIGN KEY ("meal_plan_id") REFERENCES "public"."meal_plans"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "meal_plan_entries" ADD CONSTRAINT "meal_plan_entries_recipe_id_recipes_id_fk" FOREIGN KEY ("recipe_id") REFERENCES "public"."recipes"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "meal_plans" ADD CONSTRAINT "meal_plans_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "pantry_items" ADD CONSTRAINT "pantry_items_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "pantry_items" ADD CONSTRAINT "pantry_items_ingredient_id_ingredients_id_fk" FOREIGN KEY ("ingredient_id") REFERENCES "public"."ingredients"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "shopping_list_items" ADD CONSTRAINT "shopping_list_items_list_id_shopping_lists_id_fk" FOREIGN KEY ("list_id") REFERENCES "public"."shopping_lists"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "shopping_list_items" ADD CONSTRAINT "shopping_list_items_ingredient_id_ingredients_id_fk" FOREIGN KEY ("ingredient_id") REFERENCES "public"."ingredients"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "shopping_lists" ADD CONSTRAINT "shopping_lists_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "audit_logs" ADD CONSTRAINT "audit_logs_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "user_usage" ADD CONSTRAINT "user_usage_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE INDEX "recipes_author_idx" ON "recipes" USING btree ("author_id");--> statement-breakpoint
|
||||
CREATE INDEX "recipes_visibility_idx" ON "recipes" USING btree ("visibility");--> statement-breakpoint
|
||||
CREATE INDEX "comments_recipe_idx" ON "comments" USING btree ("recipe_id");--> statement-breakpoint
|
||||
CREATE INDEX "feed_items_user_idx" ON "feed_items" USING btree ("user_id","created_at");--> statement-breakpoint
|
||||
CREATE INDEX "audit_logs_created_idx" ON "audit_logs" USING btree ("created_at");--> statement-breakpoint
|
||||
CREATE INDEX "user_usage_user_month_idx" ON "user_usage" USING btree ("user_id","month");
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE "user_usage" ADD CONSTRAINT "user_usage_user_month_uniq" UNIQUE("user_id","month");
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE "recipes" ADD COLUMN "nutrition_data" jsonb;
|
||||
@@ -0,0 +1,23 @@
|
||||
CREATE TABLE "webhook_deliveries" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"webhook_id" text NOT NULL,
|
||||
"event" text NOT NULL,
|
||||
"payload" jsonb,
|
||||
"status_code" integer,
|
||||
"success" boolean DEFAULT false NOT NULL,
|
||||
"attempts" integer DEFAULT 0 NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "webhooks" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"url" text NOT NULL,
|
||||
"events" text[] DEFAULT '{}' NOT NULL,
|
||||
"secret" text NOT NULL,
|
||||
"active" boolean DEFAULT true NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "webhook_deliveries" ADD CONSTRAINT "webhook_deliveries_webhook_id_webhooks_id_fk" FOREIGN KEY ("webhook_id") REFERENCES "public"."webhooks"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "webhooks" ADD CONSTRAINT "webhooks_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE "users" ADD COLUMN "locale" text DEFAULT 'en' NOT NULL;
|
||||
@@ -0,0 +1,9 @@
|
||||
CREATE TABLE "user_ai_keys" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"provider" text NOT NULL,
|
||||
"encrypted_key" text NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "user_ai_keys" ADD CONSTRAINT "user_ai_keys_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
|
||||
@@ -0,0 +1,59 @@
|
||||
CREATE TYPE "public"."collection_member_role" AS ENUM('viewer', 'editor');--> statement-breakpoint
|
||||
CREATE TYPE "public"."comment_reaction_type" AS ENUM('like', 'love', 'laugh', 'wow', 'sad', 'fire');--> statement-breakpoint
|
||||
CREATE TABLE "push_subscriptions" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"endpoint" text NOT NULL,
|
||||
"p256dh" text NOT NULL,
|
||||
"auth" text NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "push_subscriptions_endpoint_unique" UNIQUE("endpoint")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "user_nutrition_goals" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"calories_kcal" integer,
|
||||
"protein_g" integer,
|
||||
"carbs_g" integer,
|
||||
"fat_g" integer,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "user_nutrition_goals_user_id_unique" UNIQUE("user_id")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "recipe_snapshots" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"recipe_id" text NOT NULL,
|
||||
"author_id" text NOT NULL,
|
||||
"version" integer NOT NULL,
|
||||
"title" text NOT NULL,
|
||||
"snapshot_data" jsonb NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "collection_members" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"collection_id" text NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"role" "collection_member_role" DEFAULT 'viewer' NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "comment_reactions" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"comment_id" text NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"type" "comment_reaction_type" NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "push_subscriptions" ADD CONSTRAINT "push_subscriptions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "user_nutrition_goals" ADD CONSTRAINT "user_nutrition_goals_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "recipe_snapshots" ADD CONSTRAINT "recipe_snapshots_recipe_id_recipes_id_fk" FOREIGN KEY ("recipe_id") REFERENCES "public"."recipes"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "recipe_snapshots" ADD CONSTRAINT "recipe_snapshots_author_id_users_id_fk" FOREIGN KEY ("author_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "collection_members" ADD CONSTRAINT "collection_members_collection_id_collections_id_fk" FOREIGN KEY ("collection_id") REFERENCES "public"."collections"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "collection_members" ADD CONSTRAINT "collection_members_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "comment_reactions" ADD CONSTRAINT "comment_reactions_comment_id_comments_id_fk" FOREIGN KEY ("comment_id") REFERENCES "public"."comments"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "comment_reactions" ADD CONSTRAINT "comment_reactions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE INDEX "recipe_snapshots_recipe_idx" ON "recipe_snapshots" USING btree ("recipe_id","version");--> statement-breakpoint
|
||||
CREATE INDEX "comment_reactions_comment_idx" ON "comment_reactions" USING btree ("comment_id");
|
||||
@@ -0,0 +1,14 @@
|
||||
CREATE TABLE "user_model_prefs" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"text_provider" text,
|
||||
"text_model" text,
|
||||
"vision_provider" text,
|
||||
"vision_model" text,
|
||||
"meal_plan_provider" text,
|
||||
"meal_plan_model" text,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "user_model_prefs_user_id_unique" UNIQUE("user_id")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "user_model_prefs" ADD CONSTRAINT "user_model_prefs_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
|
||||
@@ -0,0 +1,9 @@
|
||||
CREATE TABLE "site_settings" (
|
||||
"key" text PRIMARY KEY NOT NULL,
|
||||
"value" text,
|
||||
"is_secret" boolean DEFAULT false NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_by_id" text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "site_settings" ADD CONSTRAINT "site_settings_updated_by_id_users_id_fk" FOREIGN KEY ("updated_by_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1782374484140,
|
||||
"tag": "0000_motionless_cardiac",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "7",
|
||||
"when": 1782394660764,
|
||||
"tag": "0001_curvy_crystal",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "7",
|
||||
"when": 1782408717265,
|
||||
"tag": "0002_freezing_prowler",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 3,
|
||||
"version": "7",
|
||||
"when": 1782408796095,
|
||||
"tag": "0003_robust_bullseye",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 4,
|
||||
"version": "7",
|
||||
"when": 1782854086888,
|
||||
"tag": "0004_chemical_deadpool",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 5,
|
||||
"version": "7",
|
||||
"when": 1782854378834,
|
||||
"tag": "0005_massive_spot",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 6,
|
||||
"version": "7",
|
||||
"when": 1782856370142,
|
||||
"tag": "0006_worthless_roxanne_simpson",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 7,
|
||||
"version": "7",
|
||||
"when": 1782859664569,
|
||||
"tag": "0007_majestic_retro_girl",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 8,
|
||||
"version": "7",
|
||||
"when": 1782883184215,
|
||||
"tag": "0008_violet_centennial",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export * from "./users";
|
||||
export * from "./recipes";
|
||||
export * from "./social";
|
||||
export * from "./meal-planning";
|
||||
export * from "./tiers";
|
||||
export * from "./webhooks";
|
||||
@@ -0,0 +1,83 @@
|
||||
import {
|
||||
pgTable,
|
||||
text,
|
||||
timestamp,
|
||||
integer,
|
||||
boolean,
|
||||
date,
|
||||
decimal,
|
||||
pgEnum,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { relations } from "drizzle-orm";
|
||||
import { users } from "./users";
|
||||
import { recipes } from "./recipes";
|
||||
import { ingredients } from "./recipes";
|
||||
|
||||
export const mealTypeEnum = pgEnum("meal_type", ["breakfast", "lunch", "dinner", "snack"]);
|
||||
export const weekdayEnum = pgEnum("weekday", ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]);
|
||||
|
||||
export const mealPlans = pgTable("meal_plans", {
|
||||
id: text("id").primaryKey(),
|
||||
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
weekStart: date("week_start").notNull(),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const mealPlanEntries = pgTable("meal_plan_entries", {
|
||||
id: text("id").primaryKey(),
|
||||
mealPlanId: text("meal_plan_id").notNull().references(() => mealPlans.id, { onDelete: "cascade" }),
|
||||
day: weekdayEnum("day").notNull(),
|
||||
mealType: mealTypeEnum("meal_type").notNull(),
|
||||
recipeId: text("recipe_id").references(() => recipes.id, { onDelete: "set null" }),
|
||||
servings: integer("servings").notNull().default(2),
|
||||
note: text("note"),
|
||||
});
|
||||
|
||||
export const pantryItems = pgTable("pantry_items", {
|
||||
id: text("id").primaryKey(),
|
||||
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
ingredientId: text("ingredient_id").references(() => ingredients.id, { onDelete: "set null" }),
|
||||
rawName: text("raw_name").notNull(),
|
||||
quantity: decimal("quantity", { precision: 10, scale: 4 }),
|
||||
unit: text("unit"),
|
||||
expiresAt: timestamp("expires_at"),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const shoppingLists = pgTable("shopping_lists", {
|
||||
id: text("id").primaryKey(),
|
||||
userId: text("user_id").references(() => users.id, { onDelete: "cascade" }),
|
||||
name: text("name").notNull(),
|
||||
generatedAt: timestamp("generated_at"),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const shoppingListItems = pgTable("shopping_list_items", {
|
||||
id: text("id").primaryKey(),
|
||||
listId: text("list_id").notNull().references(() => shoppingLists.id, { onDelete: "cascade" }),
|
||||
ingredientId: text("ingredient_id").references(() => ingredients.id, { onDelete: "set null" }),
|
||||
rawName: text("raw_name").notNull(),
|
||||
quantity: decimal("quantity", { precision: 10, scale: 4 }),
|
||||
unit: text("unit"),
|
||||
aisle: text("aisle"),
|
||||
checked: boolean("checked").notNull().default(false),
|
||||
});
|
||||
|
||||
export const mealPlansRelations = relations(mealPlans, ({ one, many }) => ({
|
||||
user: one(users, { fields: [mealPlans.userId], references: [users.id] }),
|
||||
entries: many(mealPlanEntries),
|
||||
}));
|
||||
|
||||
export const mealPlanEntriesRelations = relations(mealPlanEntries, ({ one }) => ({
|
||||
mealPlan: one(mealPlans, { fields: [mealPlanEntries.mealPlanId], references: [mealPlans.id] }),
|
||||
recipe: one(recipes, { fields: [mealPlanEntries.recipeId], references: [recipes.id] }),
|
||||
}));
|
||||
|
||||
export const shoppingListsRelations = relations(shoppingLists, ({ one, many }) => ({
|
||||
user: one(users, { fields: [shoppingLists.userId], references: [users.id] }),
|
||||
items: many(shoppingListItems),
|
||||
}));
|
||||
|
||||
export const shoppingListItemsRelations = relations(shoppingListItems, ({ one }) => ({
|
||||
list: one(shoppingLists, { fields: [shoppingListItems.listId], references: [shoppingLists.id] }),
|
||||
}));
|
||||
@@ -0,0 +1,170 @@
|
||||
import {
|
||||
pgTable,
|
||||
text,
|
||||
timestamp,
|
||||
boolean,
|
||||
integer,
|
||||
decimal,
|
||||
jsonb,
|
||||
pgEnum,
|
||||
index,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { relations } from "drizzle-orm";
|
||||
import { users } from "./users";
|
||||
|
||||
export const visibilityEnum = pgEnum("visibility", ["private", "unlisted", "public"]);
|
||||
export const difficultyEnum = pgEnum("difficulty", ["easy", "medium", "hard"]);
|
||||
|
||||
export type DietaryTags = {
|
||||
vegan?: boolean;
|
||||
vegetarian?: boolean;
|
||||
glutenFree?: boolean;
|
||||
dairyFree?: boolean;
|
||||
nutFree?: boolean;
|
||||
halal?: boolean;
|
||||
kosher?: boolean;
|
||||
};
|
||||
|
||||
export const recipes = pgTable("recipes", {
|
||||
id: text("id").primaryKey(),
|
||||
authorId: text("author_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
title: text("title").notNull(),
|
||||
description: text("description"),
|
||||
baseServings: integer("base_servings").notNull().default(4),
|
||||
visibility: visibilityEnum("visibility").notNull().default("private"),
|
||||
sourceUrl: text("source_url"),
|
||||
aiGenerated: boolean("ai_generated").notNull().default(false),
|
||||
aiModel: text("ai_model"),
|
||||
aiPrompt: text("ai_prompt"),
|
||||
dietaryTags: jsonb("dietary_tags").$type<DietaryTags>().default({}),
|
||||
dietaryVerified: boolean("dietary_verified").notNull().default(false),
|
||||
nutritionData: jsonb("nutrition_data").$type<{ perServing: { calories: number; proteinG: number; carbsG: number; fatG: number; fiberG: number; sodiumMg: number } }>(),
|
||||
difficulty: difficultyEnum("difficulty"),
|
||||
prepMins: integer("prep_mins"),
|
||||
cookMins: integer("cook_mins"),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
||||
}, (t) => [
|
||||
index("recipes_author_idx").on(t.authorId),
|
||||
index("recipes_visibility_idx").on(t.visibility),
|
||||
]);
|
||||
|
||||
export const ingredients = pgTable("ingredients", {
|
||||
id: text("id").primaryKey(),
|
||||
name: text("name").notNull().unique(),
|
||||
aliases: text("aliases").array().notNull().default([]),
|
||||
category: text("category"),
|
||||
knownAllergens: text("known_allergens").array().notNull().default([]),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const recipeIngredients = pgTable("recipe_ingredients", {
|
||||
id: text("id").primaryKey(),
|
||||
recipeId: text("recipe_id").notNull().references(() => recipes.id, { onDelete: "cascade" }),
|
||||
ingredientId: text("ingredient_id").references(() => ingredients.id, { onDelete: "set null" }),
|
||||
rawName: text("raw_name").notNull(),
|
||||
quantity: decimal("quantity", { precision: 10, scale: 4 }),
|
||||
unit: text("unit"),
|
||||
note: text("note"),
|
||||
order: integer("order").notNull().default(0),
|
||||
});
|
||||
|
||||
export const recipeSteps = pgTable("recipe_steps", {
|
||||
id: text("id").primaryKey(),
|
||||
recipeId: text("recipe_id").notNull().references(() => recipes.id, { onDelete: "cascade" }),
|
||||
order: integer("order").notNull(),
|
||||
instruction: text("instruction").notNull(),
|
||||
timerSeconds: integer("timer_seconds"),
|
||||
photoUrl: text("photo_url"),
|
||||
});
|
||||
|
||||
export const recipePhotos = pgTable("recipe_photos", {
|
||||
id: text("id").primaryKey(),
|
||||
recipeId: text("recipe_id").notNull().references(() => recipes.id, { onDelete: "cascade" }),
|
||||
storageKey: text("storage_key").notNull(),
|
||||
order: integer("order").notNull().default(0),
|
||||
isCover: boolean("is_cover").notNull().default(false),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const recipeNotes = pgTable("recipe_notes", {
|
||||
id: text("id").primaryKey(),
|
||||
recipeId: text("recipe_id").notNull().references(() => recipes.id, { onDelete: "cascade" }),
|
||||
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
content: text("content").notNull(),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const recipeVariations = pgTable("recipe_variations", {
|
||||
id: text("id").primaryKey(),
|
||||
parentRecipeId: text("parent_recipe_id").notNull().references(() => recipes.id, { onDelete: "cascade" }),
|
||||
childRecipeId: text("child_recipe_id").notNull().references(() => recipes.id, { onDelete: "cascade" }),
|
||||
description: text("description"),
|
||||
aiGenerated: boolean("ai_generated").notNull().default(false),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const userAllergens = pgTable("user_allergens", {
|
||||
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
allergenTag: text("allergen_tag").notNull(),
|
||||
});
|
||||
|
||||
export const recipesRelations = relations(recipes, ({ one, many }) => ({
|
||||
author: one(users, { fields: [recipes.authorId], references: [users.id] }),
|
||||
ingredients: many(recipeIngredients),
|
||||
steps: many(recipeSteps),
|
||||
photos: many(recipePhotos),
|
||||
notes: many(recipeNotes),
|
||||
childVariations: many(recipeVariations, { relationName: "parent" }),
|
||||
parentVariations: many(recipeVariations, { relationName: "child" }),
|
||||
}));
|
||||
|
||||
export const recipeIngredientsRelations = relations(recipeIngredients, ({ one }) => ({
|
||||
recipe: one(recipes, { fields: [recipeIngredients.recipeId], references: [recipes.id] }),
|
||||
ingredient: one(ingredients, { fields: [recipeIngredients.ingredientId], references: [ingredients.id] }),
|
||||
}));
|
||||
|
||||
export const recipePhotosRelations = relations(recipePhotos, ({ one }) => ({
|
||||
recipe: one(recipes, { fields: [recipePhotos.recipeId], references: [recipes.id] }),
|
||||
}));
|
||||
|
||||
export const recipeStepsRelations = relations(recipeSteps, ({ one }) => ({
|
||||
recipe: one(recipes, { fields: [recipeSteps.recipeId], references: [recipes.id] }),
|
||||
}));
|
||||
|
||||
export const recipeNotesRelations = relations(recipeNotes, ({ one }) => ({
|
||||
recipe: one(recipes, { fields: [recipeNotes.recipeId], references: [recipes.id] }),
|
||||
}));
|
||||
|
||||
export const recipeVariationsRelations = relations(recipeVariations, ({ one }) => ({
|
||||
parent: one(recipes, { fields: [recipeVariations.parentRecipeId], references: [recipes.id], relationName: "parent" }),
|
||||
child: one(recipes, { fields: [recipeVariations.childRecipeId], references: [recipes.id], relationName: "child" }),
|
||||
}));
|
||||
|
||||
export const recipeSnapshots = pgTable("recipe_snapshots", {
|
||||
id: text("id").primaryKey(),
|
||||
recipeId: text("recipe_id").notNull().references(() => recipes.id, { onDelete: "cascade" }),
|
||||
authorId: text("author_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
version: integer("version").notNull(),
|
||||
title: text("title").notNull(),
|
||||
snapshotData: jsonb("snapshot_data").notNull().$type<{
|
||||
title: string;
|
||||
description: string | null;
|
||||
baseServings: number;
|
||||
difficulty: string | null;
|
||||
prepMins: number | null;
|
||||
cookMins: number | null;
|
||||
dietaryTags: Record<string, boolean>;
|
||||
ingredients: Array<{ rawName: string; quantity: string | null; unit: string | null; note: string | null; order: number }>;
|
||||
steps: Array<{ instruction: string; timerSeconds: number | null; order: number }>;
|
||||
}>(),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
}, (t) => [
|
||||
index("recipe_snapshots_recipe_idx").on(t.recipeId, t.version),
|
||||
]);
|
||||
|
||||
export const recipeSnapshotsRelations = relations(recipeSnapshots, ({ one }) => ({
|
||||
recipe: one(recipes, { fields: [recipeSnapshots.recipeId], references: [recipes.id] }),
|
||||
author: one(users, { fields: [recipeSnapshots.authorId], references: [users.id] }),
|
||||
}));
|
||||
@@ -0,0 +1,129 @@
|
||||
import {
|
||||
pgTable,
|
||||
text,
|
||||
timestamp,
|
||||
integer,
|
||||
boolean,
|
||||
pgEnum,
|
||||
index,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { relations } from "drizzle-orm";
|
||||
import { users } from "./users";
|
||||
import { recipes } from "./recipes";
|
||||
|
||||
export const feedItemTypeEnum = pgEnum("feed_item_type", [
|
||||
"new_recipe",
|
||||
"new_follow",
|
||||
"recipe_rated",
|
||||
]);
|
||||
|
||||
export const ratings = pgTable("ratings", {
|
||||
id: text("id").primaryKey(),
|
||||
recipeId: text("recipe_id").notNull().references(() => recipes.id, { onDelete: "cascade" }),
|
||||
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
score: integer("score").notNull(),
|
||||
reviewText: text("review_text"),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const favorites = pgTable("favorites", {
|
||||
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
recipeId: text("recipe_id").notNull().references(() => recipes.id, { onDelete: "cascade" }),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const comments = pgTable("comments", {
|
||||
id: text("id").primaryKey(),
|
||||
recipeId: text("recipe_id").notNull().references(() => recipes.id, { onDelete: "cascade" }),
|
||||
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
parentId: text("parent_id"),
|
||||
content: text("content").notNull(),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
||||
}, (t) => [
|
||||
index("comments_recipe_idx").on(t.recipeId),
|
||||
]);
|
||||
|
||||
export const collections = pgTable("collections", {
|
||||
id: text("id").primaryKey(),
|
||||
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
name: text("name").notNull(),
|
||||
description: text("description"),
|
||||
isPublic: boolean("is_public").notNull().default(false),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const collectionRecipes = pgTable("collection_recipes", {
|
||||
collectionId: text("collection_id").notNull().references(() => collections.id, { onDelete: "cascade" }),
|
||||
recipeId: text("recipe_id").notNull().references(() => recipes.id, { onDelete: "cascade" }),
|
||||
addedAt: timestamp("added_at").notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const cookingHistory = pgTable("cooking_history", {
|
||||
id: text("id").primaryKey(),
|
||||
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
recipeId: text("recipe_id").notNull().references(() => recipes.id, { onDelete: "cascade" }),
|
||||
cookedAt: timestamp("cooked_at").notNull().defaultNow(),
|
||||
servings: integer("servings"),
|
||||
notes: text("notes"),
|
||||
});
|
||||
|
||||
export const feedItems = pgTable("feed_items", {
|
||||
id: text("id").primaryKey(),
|
||||
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
type: feedItemTypeEnum("type").notNull(),
|
||||
actorId: text("actor_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
subjectId: text("subject_id").notNull(),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
}, (t) => [
|
||||
index("feed_items_user_idx").on(t.userId, t.createdAt),
|
||||
]);
|
||||
|
||||
export const ratingsRelations = relations(ratings, ({ one }) => ({
|
||||
recipe: one(recipes, { fields: [ratings.recipeId], references: [recipes.id] }),
|
||||
user: one(users, { fields: [ratings.userId], references: [users.id] }),
|
||||
}));
|
||||
|
||||
export const collectionsRelations = relations(collections, ({ one, many }) => ({
|
||||
user: one(users, { fields: [collections.userId], references: [users.id] }),
|
||||
recipes: many(collectionRecipes),
|
||||
}));
|
||||
|
||||
export const collectionRecipesRelations = relations(collectionRecipes, ({ one }) => ({
|
||||
collection: one(collections, { fields: [collectionRecipes.collectionId], references: [collections.id] }),
|
||||
recipe: one(recipes, { fields: [collectionRecipes.recipeId], references: [recipes.id] }),
|
||||
}));
|
||||
|
||||
export const collectionMemberRoleEnum = pgEnum("collection_member_role", ["viewer", "editor"]);
|
||||
|
||||
export const collectionMembers = pgTable("collection_members", {
|
||||
id: text("id").primaryKey(),
|
||||
collectionId: text("collection_id").notNull().references(() => collections.id, { onDelete: "cascade" }),
|
||||
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
role: collectionMemberRoleEnum("role").notNull().default("viewer"),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const commentReactionTypeEnum = pgEnum("comment_reaction_type", ["like", "love", "laugh", "wow", "sad", "fire"]);
|
||||
|
||||
export const commentReactions = pgTable("comment_reactions", {
|
||||
id: text("id").primaryKey(),
|
||||
commentId: text("comment_id").notNull().references(() => comments.id, { onDelete: "cascade" }),
|
||||
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
type: commentReactionTypeEnum("type").notNull(),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
}, (t) => [
|
||||
index("comment_reactions_comment_idx").on(t.commentId),
|
||||
]);
|
||||
|
||||
export const collectionMembersRelations = relations(collectionMembers, ({ one }) => ({
|
||||
collection: one(collections, { fields: [collectionMembers.collectionId], references: [collections.id] }),
|
||||
user: one(users, { fields: [collectionMembers.userId], references: [users.id] }),
|
||||
}));
|
||||
|
||||
export const commentReactionsRelations = relations(commentReactions, ({ one }) => ({
|
||||
comment: one(comments, { fields: [commentReactions.commentId], references: [comments.id] }),
|
||||
user: one(users, { fields: [commentReactions.userId], references: [users.id] }),
|
||||
}));
|
||||
@@ -0,0 +1,55 @@
|
||||
import {
|
||||
pgTable,
|
||||
text,
|
||||
timestamp,
|
||||
integer,
|
||||
boolean,
|
||||
index,
|
||||
unique,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { relations } from "drizzle-orm";
|
||||
import { users, tierEnum } from "./users";
|
||||
|
||||
export const tierDefinitions = pgTable("tier_definitions", {
|
||||
tier: tierEnum("tier").primaryKey(),
|
||||
maxRecipes: integer("max_recipes").notNull(),
|
||||
aiCallsPerMonth: integer("ai_calls_per_month").notNull(),
|
||||
storageMb: integer("storage_mb").notNull(),
|
||||
maxPublicRecipes: integer("max_public_recipes").notNull(),
|
||||
});
|
||||
|
||||
export const userUsage = pgTable("user_usage", {
|
||||
id: text("id").primaryKey(),
|
||||
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
month: text("month").notNull(),
|
||||
aiCallsUsed: integer("ai_calls_used").notNull().default(0),
|
||||
recipeCount: integer("recipe_count").notNull().default(0),
|
||||
storageUsedMb: integer("storage_used_mb").notNull().default(0),
|
||||
}, (t) => [
|
||||
index("user_usage_user_month_idx").on(t.userId, t.month),
|
||||
unique("user_usage_user_month_uniq").on(t.userId, t.month),
|
||||
]);
|
||||
|
||||
export const auditLogs = pgTable("audit_logs", {
|
||||
id: text("id").primaryKey(),
|
||||
userId: text("user_id").references(() => users.id, { onDelete: "set null" }),
|
||||
action: text("action").notNull(),
|
||||
targetType: text("target_type"),
|
||||
targetId: text("target_id"),
|
||||
metadata: text("metadata"),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
}, (t) => [
|
||||
index("audit_logs_created_idx").on(t.createdAt),
|
||||
]);
|
||||
|
||||
export const siteSettings = pgTable("site_settings", {
|
||||
key: text("key").primaryKey(),
|
||||
value: text("value"),
|
||||
isSecret: boolean("is_secret").notNull().default(false),
|
||||
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
||||
updatedById: text("updated_by_id").references(() => users.id, { onDelete: "set null" }),
|
||||
});
|
||||
|
||||
export const userUsageRelations = relations(userUsage, ({ one }) => ({
|
||||
user: one(users, { fields: [userUsage.userId], references: [users.id] }),
|
||||
}));
|
||||
@@ -0,0 +1,150 @@
|
||||
import {
|
||||
pgTable,
|
||||
text,
|
||||
timestamp,
|
||||
boolean,
|
||||
integer,
|
||||
pgEnum,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { relations } from "drizzle-orm";
|
||||
|
||||
export const userRoleEnum = pgEnum("user_role", ["user", "moderator", "admin"]);
|
||||
export const tierEnum = pgEnum("tier", ["free", "pro"]);
|
||||
export const unitPrefEnum = pgEnum("unit_pref", ["metric", "imperial"]);
|
||||
|
||||
export const users = pgTable("users", {
|
||||
id: text("id").primaryKey(),
|
||||
email: text("email").notNull().unique(),
|
||||
emailVerified: boolean("email_verified").notNull().default(false),
|
||||
name: text("name").notNull(),
|
||||
avatarUrl: text("avatar_url"),
|
||||
bio: text("bio"),
|
||||
username: text("username").unique(),
|
||||
role: userRoleEnum("role").notNull().default("user"),
|
||||
tier: tierEnum("tier").notNull().default("free"),
|
||||
unitPref: unitPrefEnum("unit_pref").notNull().default("metric"),
|
||||
locale: text("locale").notNull().default("en"),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
||||
});
|
||||
|
||||
// Better Auth requires these tables
|
||||
export const sessions = pgTable("sessions", {
|
||||
id: text("id").primaryKey(),
|
||||
expiresAt: timestamp("expires_at").notNull(),
|
||||
token: text("token").notNull().unique(),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
||||
ipAddress: text("ip_address"),
|
||||
userAgent: text("user_agent"),
|
||||
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
});
|
||||
|
||||
export const accounts = pgTable("accounts", {
|
||||
id: text("id").primaryKey(),
|
||||
accountId: text("account_id").notNull(),
|
||||
providerId: text("provider_id").notNull(),
|
||||
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
accessToken: text("access_token"),
|
||||
refreshToken: text("refresh_token"),
|
||||
idToken: text("id_token"),
|
||||
accessTokenExpiresAt: timestamp("access_token_expires_at"),
|
||||
refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
|
||||
scope: text("scope"),
|
||||
password: text("password"),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const verifications = pgTable("verifications", {
|
||||
id: text("id").primaryKey(),
|
||||
identifier: text("identifier").notNull(),
|
||||
value: text("value").notNull(),
|
||||
expiresAt: timestamp("expires_at").notNull(),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const userFollows = pgTable("user_follows", {
|
||||
followerId: text("follower_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
followingId: text("following_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const apiKeys = pgTable("api_keys", {
|
||||
id: text("id").primaryKey(),
|
||||
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
name: text("name").notNull(),
|
||||
keyHash: text("key_hash").notNull().unique(),
|
||||
lastUsedAt: timestamp("last_used_at"),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const userAiKeys = pgTable("user_ai_keys", {
|
||||
id: text("id").primaryKey(),
|
||||
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
provider: text("provider").notNull(),
|
||||
encryptedKey: text("encrypted_key").notNull(),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const usersRelations = relations(users, ({ many }) => ({
|
||||
sessions: many(sessions),
|
||||
accounts: many(accounts),
|
||||
followers: many(userFollows, { relationName: "following" }),
|
||||
following: many(userFollows, { relationName: "follower" }),
|
||||
apiKeys: many(apiKeys),
|
||||
aiKeys: many(userAiKeys),
|
||||
}));
|
||||
|
||||
export const userAiKeysRelations = relations(userAiKeys, ({ one }) => ({
|
||||
user: one(users, { fields: [userAiKeys.userId], references: [users.id] }),
|
||||
}));
|
||||
|
||||
export const userFollowsRelations = relations(userFollows, ({ one }) => ({
|
||||
follower: one(users, { fields: [userFollows.followerId], references: [users.id], relationName: "follower" }),
|
||||
following: one(users, { fields: [userFollows.followingId], references: [users.id], relationName: "following" }),
|
||||
}));
|
||||
|
||||
export const pushSubscriptions = pgTable("push_subscriptions", {
|
||||
id: text("id").primaryKey(),
|
||||
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
endpoint: text("endpoint").notNull().unique(),
|
||||
p256dh: text("p256dh").notNull(),
|
||||
auth: text("auth").notNull(),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const userNutritionGoals = pgTable("user_nutrition_goals", {
|
||||
id: text("id").primaryKey(),
|
||||
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }).unique(),
|
||||
caloriesKcal: integer("calories_kcal"),
|
||||
proteinG: integer("protein_g"),
|
||||
carbsG: integer("carbs_g"),
|
||||
fatG: integer("fat_g"),
|
||||
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const pushSubscriptionsRelations = relations(pushSubscriptions, ({ one }) => ({
|
||||
user: one(users, { fields: [pushSubscriptions.userId], references: [users.id] }),
|
||||
}));
|
||||
|
||||
export const userNutritionGoalsRelations = relations(userNutritionGoals, ({ one }) => ({
|
||||
user: one(users, { fields: [userNutritionGoals.userId], references: [users.id] }),
|
||||
}));
|
||||
|
||||
export const userModelPrefs = pgTable("user_model_prefs", {
|
||||
id: text("id").primaryKey(),
|
||||
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }).unique(),
|
||||
textProvider: text("text_provider"),
|
||||
textModel: text("text_model"),
|
||||
visionProvider: text("vision_provider"),
|
||||
visionModel: text("vision_model"),
|
||||
mealPlanProvider: text("meal_plan_provider"),
|
||||
mealPlanModel: text("meal_plan_model"),
|
||||
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const userModelPrefsRelations = relations(userModelPrefs, ({ one }) => ({
|
||||
user: one(users, { fields: [userModelPrefs.userId], references: [users.id] }),
|
||||
}));
|
||||
@@ -0,0 +1,33 @@
|
||||
import { pgTable, text, boolean, timestamp, integer, jsonb } from "drizzle-orm/pg-core";
|
||||
import { relations } from "drizzle-orm";
|
||||
import { users } from "./users";
|
||||
|
||||
export const webhooks = pgTable("webhooks", {
|
||||
id: text("id").primaryKey(),
|
||||
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
url: text("url").notNull(),
|
||||
events: text("events").array().notNull().default([]),
|
||||
secret: text("secret").notNull(),
|
||||
active: boolean("active").notNull().default(true),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const webhookDeliveries = pgTable("webhook_deliveries", {
|
||||
id: text("id").primaryKey(),
|
||||
webhookId: text("webhook_id").notNull().references(() => webhooks.id, { onDelete: "cascade" }),
|
||||
event: text("event").notNull(),
|
||||
payload: jsonb("payload"),
|
||||
statusCode: integer("status_code"),
|
||||
success: boolean("success").notNull().default(false),
|
||||
attempts: integer("attempts").notNull().default(0),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const webhooksRelations = relations(webhooks, ({ one, many }) => ({
|
||||
user: one(users, { fields: [webhooks.userId], references: [users.id] }),
|
||||
deliveries: many(webhookDeliveries),
|
||||
}));
|
||||
|
||||
export const webhookDeliveriesRelations = relations(webhookDeliveries, ({ one }) => ({
|
||||
webhook: one(webhooks, { fields: [webhookDeliveries.webhookId], references: [webhooks.id] }),
|
||||
}));
|
||||
@@ -0,0 +1,33 @@
|
||||
import { db } from "./client";
|
||||
import { tierDefinitions } from "./schema";
|
||||
|
||||
async function seed() {
|
||||
console.log("Seeding tier definitions...");
|
||||
await db
|
||||
.insert(tierDefinitions)
|
||||
.values([
|
||||
{
|
||||
tier: "free",
|
||||
maxRecipes: 50,
|
||||
aiCallsPerMonth: 10,
|
||||
storageMb: 500,
|
||||
maxPublicRecipes: 5,
|
||||
},
|
||||
{
|
||||
tier: "pro",
|
||||
maxRecipes: 99999,
|
||||
aiCallsPerMonth: 500,
|
||||
storageMb: 10000,
|
||||
maxPublicRecipes: 99999,
|
||||
},
|
||||
])
|
||||
.onConflictDoNothing();
|
||||
|
||||
console.log("Seed complete.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
seed().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Reference in New Issue
Block a user