Initial commit: PodcastYes — AI podcast platform
This commit is contained in:
@@ -0,0 +1,545 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "EpisodeStatus" AS ENUM ('DRAFT', 'QUEUED', 'SCRIPTING', 'SYNTHESIZING', 'STITCHING', 'ART', 'SAVING', 'READY', 'FAILED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "EpisodeFormat" AS ENUM ('SOLO', 'INTERVIEW', 'MULTI_HOST');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "user" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"emailVerified" BOOLEAN NOT NULL DEFAULT false,
|
||||
"image" TEXT,
|
||||
"role" TEXT DEFAULT 'user',
|
||||
"banned" BOOLEAN DEFAULT false,
|
||||
"banReason" TEXT,
|
||||
"banExpires" TIMESTAMP(3),
|
||||
"stripeCustomerId" TEXT,
|
||||
"paypalPayerId" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "user_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "session" (
|
||||
"id" TEXT NOT NULL,
|
||||
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||
"token" TEXT NOT NULL,
|
||||
"ipAddress" TEXT,
|
||||
"userAgent" TEXT,
|
||||
"activeOrganizationId" TEXT,
|
||||
"impersonatedBy" TEXT,
|
||||
"userId" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "session_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "account" (
|
||||
"id" TEXT NOT NULL,
|
||||
"accountId" TEXT NOT NULL,
|
||||
"providerId" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"accessToken" TEXT,
|
||||
"refreshToken" TEXT,
|
||||
"idToken" TEXT,
|
||||
"accessTokenExpiresAt" TIMESTAMP(3),
|
||||
"refreshTokenExpiresAt" TIMESTAMP(3),
|
||||
"scope" TEXT,
|
||||
"password" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "account_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "verification" (
|
||||
"id" TEXT NOT NULL,
|
||||
"identifier" TEXT NOT NULL,
|
||||
"value" TEXT NOT NULL,
|
||||
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "verification_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "organization" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"slug" TEXT,
|
||||
"logo" TEXT,
|
||||
"metadata" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "organization_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "member" (
|
||||
"id" TEXT NOT NULL,
|
||||
"organizationId" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"teamId" TEXT,
|
||||
"role" TEXT NOT NULL DEFAULT 'member',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "member_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "invitation" (
|
||||
"id" TEXT NOT NULL,
|
||||
"organizationId" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"role" TEXT,
|
||||
"teamId" TEXT,
|
||||
"status" TEXT NOT NULL DEFAULT 'pending',
|
||||
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||
"inviterId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "invitation_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "team" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"organizationId" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "team_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "org_branding" (
|
||||
"organizationId" TEXT NOT NULL,
|
||||
"brandName" TEXT,
|
||||
"primaryColor" TEXT,
|
||||
"logoUrl" TEXT,
|
||||
"customDomain" TEXT,
|
||||
"removePoweredBy" BOOLEAN NOT NULL DEFAULT false,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "org_branding_pkey" PRIMARY KEY ("organizationId")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "subscription" (
|
||||
"id" TEXT NOT NULL,
|
||||
"plan" TEXT NOT NULL,
|
||||
"referenceId" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL DEFAULT 'active',
|
||||
"provider" TEXT NOT NULL DEFAULT 'stripe',
|
||||
"billingInterval" TEXT,
|
||||
"seats" INTEGER,
|
||||
"stripeCustomerId" TEXT,
|
||||
"stripeSubscriptionId" TEXT,
|
||||
"paypalSubscriptionId" TEXT,
|
||||
"paypalPlanId" TEXT,
|
||||
"periodStart" TIMESTAMP(3),
|
||||
"periodEnd" TIMESTAMP(3),
|
||||
"cancelAtPeriodEnd" BOOLEAN DEFAULT false,
|
||||
"trialStart" TIMESTAMP(3),
|
||||
"trialEnd" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "subscription_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "plan" (
|
||||
"id" TEXT NOT NULL,
|
||||
"key" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"priceMonthly" INTEGER NOT NULL DEFAULT 0,
|
||||
"priceYearly" INTEGER NOT NULL DEFAULT 0,
|
||||
"stripePriceIdMonthly" TEXT,
|
||||
"stripePriceIdYearly" TEXT,
|
||||
"paypalPlanIdMonthly" TEXT,
|
||||
"paypalPlanIdYearly" TEXT,
|
||||
"limits" JSONB NOT NULL,
|
||||
"features" JSONB NOT NULL,
|
||||
"isPublic" BOOLEAN NOT NULL DEFAULT true,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "plan_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "usage_record" (
|
||||
"id" TEXT NOT NULL,
|
||||
"ownerId" TEXT NOT NULL,
|
||||
"ownerType" TEXT NOT NULL,
|
||||
"periodKey" TEXT NOT NULL,
|
||||
"metric" TEXT NOT NULL,
|
||||
"count" INTEGER NOT NULL DEFAULT 0,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "usage_record_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "episode" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"organizationId" TEXT,
|
||||
"seriesId" TEXT,
|
||||
"title" TEXT NOT NULL,
|
||||
"topic" TEXT NOT NULL,
|
||||
"tone" TEXT NOT NULL,
|
||||
"format" "EpisodeFormat" NOT NULL DEFAULT 'SOLO',
|
||||
"language" TEXT NOT NULL DEFAULT 'en',
|
||||
"targetLengthMin" INTEGER NOT NULL DEFAULT 5,
|
||||
"audience" TEXT,
|
||||
"status" "EpisodeStatus" NOT NULL DEFAULT 'DRAFT',
|
||||
"stage" TEXT,
|
||||
"errorMessage" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "episode_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "script" (
|
||||
"id" TEXT NOT NULL,
|
||||
"episodeId" TEXT NOT NULL,
|
||||
"content" JSONB NOT NULL,
|
||||
"model" TEXT NOT NULL DEFAULT 'gpt-4o',
|
||||
"version" INTEGER NOT NULL DEFAULT 1,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "script_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "speaker_config" (
|
||||
"id" TEXT NOT NULL,
|
||||
"episodeId" TEXT NOT NULL,
|
||||
"speakerKey" TEXT NOT NULL,
|
||||
"displayName" TEXT NOT NULL,
|
||||
"elevenVoiceId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "speaker_config_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "audio_asset" (
|
||||
"id" TEXT NOT NULL,
|
||||
"episodeId" TEXT NOT NULL,
|
||||
"storageKey" TEXT NOT NULL,
|
||||
"durationSec" INTEGER,
|
||||
"sizeBytes" INTEGER,
|
||||
"format" TEXT NOT NULL DEFAULT 'mp3',
|
||||
"segments" JSONB,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "audio_asset_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "cover_art" (
|
||||
"id" TEXT NOT NULL,
|
||||
"episodeId" TEXT NOT NULL,
|
||||
"storageKey" TEXT NOT NULL,
|
||||
"prompt" TEXT NOT NULL,
|
||||
"model" TEXT NOT NULL DEFAULT 'dall-e-3',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "cover_art_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "repurposed_content" (
|
||||
"id" TEXT NOT NULL,
|
||||
"episodeId" TEXT NOT NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
"content" JSONB NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "repurposed_content_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "series" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"organizationId" TEXT,
|
||||
"title" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"plannedCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"plan" JSONB,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "series_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "generation_job" (
|
||||
"id" TEXT NOT NULL,
|
||||
"episodeId" TEXT NOT NULL,
|
||||
"pgBossJobId" TEXT,
|
||||
"type" TEXT NOT NULL DEFAULT 'full',
|
||||
"status" TEXT NOT NULL DEFAULT 'queued',
|
||||
"stage" TEXT,
|
||||
"attempts" INTEGER NOT NULL DEFAULT 0,
|
||||
"costEstimate" JSONB,
|
||||
"error" TEXT,
|
||||
"startedAt" TIMESTAMP(3),
|
||||
"finishedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "generation_job_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "api_key" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"hashedKey" TEXT NOT NULL,
|
||||
"prefix" TEXT NOT NULL,
|
||||
"lastUsedAt" TIMESTAMP(3),
|
||||
"revokedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "api_key_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "audit_log" (
|
||||
"id" TEXT NOT NULL,
|
||||
"actorId" TEXT,
|
||||
"actorType" TEXT NOT NULL DEFAULT 'user',
|
||||
"action" TEXT NOT NULL,
|
||||
"target" TEXT,
|
||||
"metadata" JSONB,
|
||||
"ip" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "audit_log_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "feature_flag" (
|
||||
"key" TEXT NOT NULL,
|
||||
"enabled" BOOLEAN NOT NULL DEFAULT false,
|
||||
"rolloutPct" INTEGER NOT NULL DEFAULT 0,
|
||||
"metadata" JSONB,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "feature_flag_pkey" PRIMARY KEY ("key")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ai_cost_log" (
|
||||
"id" TEXT NOT NULL,
|
||||
"provider" TEXT NOT NULL,
|
||||
"operation" TEXT NOT NULL,
|
||||
"episodeId" TEXT,
|
||||
"userId" TEXT,
|
||||
"units" INTEGER NOT NULL DEFAULT 0,
|
||||
"costUsd" DECIMAL(10,4) NOT NULL DEFAULT 0,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "ai_cost_log_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "content_flag" (
|
||||
"id" TEXT NOT NULL,
|
||||
"episodeId" TEXT NOT NULL,
|
||||
"reason" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL DEFAULT 'open',
|
||||
"reviewedBy" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "content_flag_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "user_email_key" ON "user"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "session_token_key" ON "session"("token");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "session_userId_idx" ON "session"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "account_userId_idx" ON "account"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "verification_identifier_idx" ON "verification"("identifier");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "organization_slug_key" ON "organization"("slug");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "member_userId_idx" ON "member"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "member_organizationId_userId_key" ON "member"("organizationId", "userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "invitation_organizationId_idx" ON "invitation"("organizationId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "team_organizationId_idx" ON "team"("organizationId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "org_branding_customDomain_key" ON "org_branding"("customDomain");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "subscription_referenceId_idx" ON "subscription"("referenceId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "subscription_stripeSubscriptionId_idx" ON "subscription"("stripeSubscriptionId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "subscription_paypalSubscriptionId_idx" ON "subscription"("paypalSubscriptionId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "plan_key_key" ON "plan"("key");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "usage_record_ownerId_periodKey_idx" ON "usage_record"("ownerId", "periodKey");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "usage_record_ownerId_periodKey_metric_key" ON "usage_record"("ownerId", "periodKey", "metric");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "episode_userId_idx" ON "episode"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "episode_organizationId_idx" ON "episode"("organizationId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "episode_seriesId_idx" ON "episode"("seriesId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "episode_status_idx" ON "episode"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "script_episodeId_key" ON "script"("episodeId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "speaker_config_episodeId_speakerKey_key" ON "speaker_config"("episodeId", "speakerKey");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "audio_asset_episodeId_key" ON "audio_asset"("episodeId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "cover_art_episodeId_key" ON "cover_art"("episodeId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "repurposed_content_episodeId_idx" ON "repurposed_content"("episodeId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "series_userId_idx" ON "series"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "generation_job_episodeId_idx" ON "generation_job"("episodeId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "generation_job_status_idx" ON "generation_job"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "api_key_hashedKey_key" ON "api_key"("hashedKey");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "api_key_userId_idx" ON "api_key"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "audit_log_action_idx" ON "audit_log"("action");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "audit_log_createdAt_idx" ON "audit_log"("createdAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ai_cost_log_provider_createdAt_idx" ON "ai_cost_log"("provider", "createdAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ai_cost_log_userId_idx" ON "ai_cost_log"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "content_flag_status_idx" ON "content_flag"("status");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "session" ADD CONSTRAINT "session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "account" ADD CONSTRAINT "account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "member" ADD CONSTRAINT "member_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "member" ADD CONSTRAINT "member_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "invitation" ADD CONSTRAINT "invitation_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "invitation" ADD CONSTRAINT "invitation_inviterId_fkey" FOREIGN KEY ("inviterId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "team" ADD CONSTRAINT "team_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "org_branding" ADD CONSTRAINT "org_branding_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "usage_record" ADD CONSTRAINT "usage_owner_user_fk" FOREIGN KEY ("ownerId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "episode" ADD CONSTRAINT "episode_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "episode" ADD CONSTRAINT "episode_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organization"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "episode" ADD CONSTRAINT "episode_seriesId_fkey" FOREIGN KEY ("seriesId") REFERENCES "series"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "script" ADD CONSTRAINT "script_episodeId_fkey" FOREIGN KEY ("episodeId") REFERENCES "episode"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "speaker_config" ADD CONSTRAINT "speaker_config_episodeId_fkey" FOREIGN KEY ("episodeId") REFERENCES "episode"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "audio_asset" ADD CONSTRAINT "audio_asset_episodeId_fkey" FOREIGN KEY ("episodeId") REFERENCES "episode"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "cover_art" ADD CONSTRAINT "cover_art_episodeId_fkey" FOREIGN KEY ("episodeId") REFERENCES "episode"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "repurposed_content" ADD CONSTRAINT "repurposed_content_episodeId_fkey" FOREIGN KEY ("episodeId") REFERENCES "episode"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "series" ADD CONSTRAINT "series_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "series" ADD CONSTRAINT "series_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organization"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "generation_job" ADD CONSTRAINT "generation_job_episodeId_fkey" FOREIGN KEY ("episodeId") REFERENCES "episode"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "api_key" ADD CONSTRAINT "api_key_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "audit_log" ADD CONSTRAINT "audit_log_actorId_fkey" FOREIGN KEY ("actorId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "content_flag" ADD CONSTRAINT "content_flag_episodeId_fkey" FOREIGN KEY ("episodeId") REFERENCES "episode"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "postgresql"
|
||||
@@ -0,0 +1,492 @@
|
||||
// PodcastYes data model.
|
||||
// Better Auth owns the auth/org/subscription tables (field names follow its conventions);
|
||||
// we augment User/Session/Subscription and add the podcast + admin domain.
|
||||
// pg-boss manages its own `pgboss.*` schema separately — not modeled here.
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
// ─────────────────────────── Better Auth: core ───────────────────────────
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
email String @unique
|
||||
emailVerified Boolean @default(false)
|
||||
image String?
|
||||
|
||||
// admin plugin
|
||||
role String? @default("user") // "user" | "admin"
|
||||
banned Boolean? @default(false)
|
||||
banReason String?
|
||||
banExpires DateTime?
|
||||
|
||||
// app fields
|
||||
stripeCustomerId String?
|
||||
paypalPayerId String?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
sessions Session[]
|
||||
accounts Account[]
|
||||
memberships Member[]
|
||||
sentInvitations Invitation[] @relation("SentInvitations")
|
||||
episodes Episode[]
|
||||
series Series[]
|
||||
apiKeys ApiKey[]
|
||||
usageRecords UsageRecord[]
|
||||
auditLogs AuditLog[] @relation("ActorLogs")
|
||||
|
||||
@@map("user")
|
||||
}
|
||||
|
||||
model Session {
|
||||
id String @id @default(cuid())
|
||||
expiresAt DateTime
|
||||
token String @unique
|
||||
ipAddress String?
|
||||
userAgent String?
|
||||
|
||||
// organization plugin
|
||||
activeOrganizationId String?
|
||||
// admin plugin (impersonation)
|
||||
impersonatedBy String?
|
||||
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([userId])
|
||||
@@map("session")
|
||||
}
|
||||
|
||||
model Account {
|
||||
id String @id @default(cuid())
|
||||
accountId String
|
||||
providerId String
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
accessToken String?
|
||||
refreshToken String?
|
||||
idToken String?
|
||||
accessTokenExpiresAt DateTime?
|
||||
refreshTokenExpiresAt DateTime?
|
||||
scope String?
|
||||
password String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([userId])
|
||||
@@map("account")
|
||||
}
|
||||
|
||||
model Verification {
|
||||
id String @id @default(cuid())
|
||||
identifier String
|
||||
value String
|
||||
expiresAt DateTime
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([identifier])
|
||||
@@map("verification")
|
||||
}
|
||||
|
||||
// ─────────────────────── Better Auth: organization / team ───────────────────────
|
||||
|
||||
model Organization {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
slug String? @unique
|
||||
logo String?
|
||||
metadata String? // JSON blob (Better Auth stores as string)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
members Member[]
|
||||
invitations Invitation[]
|
||||
teams Team[]
|
||||
episodes Episode[]
|
||||
series Series[]
|
||||
branding OrgBranding?
|
||||
|
||||
@@map("organization")
|
||||
}
|
||||
|
||||
model Member {
|
||||
id String @id @default(cuid())
|
||||
organizationId String
|
||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
teamId String?
|
||||
role String @default("member") // "owner" | "admin" | "member"
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@unique([organizationId, userId])
|
||||
@@index([userId])
|
||||
@@map("member")
|
||||
}
|
||||
|
||||
model Invitation {
|
||||
id String @id @default(cuid())
|
||||
organizationId String
|
||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
email String
|
||||
role String?
|
||||
teamId String?
|
||||
status String @default("pending")
|
||||
expiresAt DateTime
|
||||
inviterId String
|
||||
inviter User @relation("SentInvitations", fields: [inviterId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([organizationId])
|
||||
@@map("invitation")
|
||||
}
|
||||
|
||||
model Team {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
organizationId String
|
||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime? @updatedAt
|
||||
|
||||
@@index([organizationId])
|
||||
@@map("team")
|
||||
}
|
||||
|
||||
/// Agency white-label branding, one-to-one with an organization.
|
||||
model OrgBranding {
|
||||
organizationId String @id
|
||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
brandName String?
|
||||
primaryColor String? // hex
|
||||
logoUrl String?
|
||||
customDomain String? @unique
|
||||
removePoweredBy Boolean @default(false)
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("org_branding")
|
||||
}
|
||||
|
||||
// ─────────────────────────── Billing ───────────────────────────
|
||||
|
||||
/// One internal subscription model. Better Auth's Stripe plugin populates the Stripe fields;
|
||||
/// the custom PayPal webhook handler populates the PayPal fields. `provider` discriminates.
|
||||
model Subscription {
|
||||
id String @id @default(cuid())
|
||||
plan String // "free" | "creator" | "pro" | "agency"
|
||||
referenceId String // user.id (individual) OR organization.id (Agency)
|
||||
status String @default("active") // active | trialing | past_due | canceled | paused
|
||||
|
||||
provider String @default("stripe") // "stripe" | "paypal"
|
||||
billingInterval String? // "month" | "year"
|
||||
seats Int?
|
||||
|
||||
// Stripe
|
||||
stripeCustomerId String?
|
||||
stripeSubscriptionId String?
|
||||
// PayPal
|
||||
paypalSubscriptionId String?
|
||||
paypalPlanId String?
|
||||
|
||||
periodStart DateTime?
|
||||
periodEnd DateTime?
|
||||
cancelAtPeriodEnd Boolean? @default(false)
|
||||
trialStart DateTime?
|
||||
trialEnd DateTime?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([referenceId])
|
||||
@@index([stripeSubscriptionId])
|
||||
@@index([paypalSubscriptionId])
|
||||
@@map("subscription")
|
||||
}
|
||||
|
||||
/// Optional DB mirror of the code-defined plan catalog (lib/billing/plans.ts) so admins can
|
||||
/// tune prices/limits without a deploy. `limits` is the PlanLimits JSON.
|
||||
model Plan {
|
||||
id String @id @default(cuid())
|
||||
key String @unique // "free" | "creator" | "pro" | "agency"
|
||||
name String
|
||||
priceMonthly Int @default(0) // cents
|
||||
priceYearly Int @default(0)
|
||||
stripePriceIdMonthly String?
|
||||
stripePriceIdYearly String?
|
||||
paypalPlanIdMonthly String?
|
||||
paypalPlanIdYearly String?
|
||||
limits Json
|
||||
features Json
|
||||
isPublic Boolean @default(true)
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("plan")
|
||||
}
|
||||
|
||||
/// Monthly usage counters, one row per (subject, period, metric).
|
||||
model UsageRecord {
|
||||
id String @id @default(cuid())
|
||||
ownerId String // user.id or organization.id (the billing subject)
|
||||
ownerType String // "user" | "organization"
|
||||
periodKey String // "2026-06"
|
||||
metric String // "script" | "audio" | "art" | "repurpose"
|
||||
count Int @default(0)
|
||||
user User? @relation(fields: [ownerId], references: [id], onDelete: Cascade, map: "usage_owner_user_fk")
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([ownerId, periodKey, metric])
|
||||
@@index([ownerId, periodKey])
|
||||
@@map("usage_record")
|
||||
}
|
||||
|
||||
// ─────────────────────────── Podcast domain ───────────────────────────
|
||||
|
||||
enum EpisodeStatus {
|
||||
DRAFT
|
||||
QUEUED
|
||||
SCRIPTING
|
||||
SYNTHESIZING
|
||||
STITCHING
|
||||
ART
|
||||
SAVING
|
||||
READY
|
||||
FAILED
|
||||
}
|
||||
|
||||
enum EpisodeFormat {
|
||||
SOLO
|
||||
INTERVIEW
|
||||
MULTI_HOST
|
||||
}
|
||||
|
||||
model Episode {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
organizationId String?
|
||||
organization Organization? @relation(fields: [organizationId], references: [id], onDelete: SetNull)
|
||||
seriesId String?
|
||||
series Series? @relation(fields: [seriesId], references: [id], onDelete: SetNull)
|
||||
|
||||
title String
|
||||
topic String @db.Text
|
||||
tone String
|
||||
format EpisodeFormat @default(SOLO)
|
||||
language String @default("en") // ISO code
|
||||
targetLengthMin Int @default(5)
|
||||
audience String?
|
||||
|
||||
status EpisodeStatus @default(DRAFT)
|
||||
stage String? // human-readable current step
|
||||
errorMessage String?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
script Script?
|
||||
speakers SpeakerConfig[]
|
||||
audioAsset AudioAsset?
|
||||
coverArt CoverArt?
|
||||
repurposed RepurposedContent[]
|
||||
jobs GenerationJob[]
|
||||
flags ContentFlag[]
|
||||
|
||||
@@index([userId])
|
||||
@@index([organizationId])
|
||||
@@index([seriesId])
|
||||
@@index([status])
|
||||
@@map("episode")
|
||||
}
|
||||
|
||||
model Script {
|
||||
id String @id @default(cuid())
|
||||
episodeId String @unique
|
||||
episode Episode @relation(fields: [episodeId], references: [id], onDelete: Cascade)
|
||||
/// Structured: [{ speakerKey, text, sectionId, title }]
|
||||
content Json
|
||||
model String @default("gpt-4o")
|
||||
version Int @default(1)
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("script")
|
||||
}
|
||||
|
||||
/// Maps a script speaker ("host", "guest", "cohost") to an ElevenLabs voice.
|
||||
model SpeakerConfig {
|
||||
id String @id @default(cuid())
|
||||
episodeId String
|
||||
episode Episode @relation(fields: [episodeId], references: [id], onDelete: Cascade)
|
||||
speakerKey String
|
||||
displayName String
|
||||
elevenVoiceId String
|
||||
|
||||
@@unique([episodeId, speakerKey])
|
||||
@@map("speaker_config")
|
||||
}
|
||||
|
||||
model AudioAsset {
|
||||
id String @id @default(cuid())
|
||||
episodeId String @unique
|
||||
episode Episode @relation(fields: [episodeId], references: [id], onDelete: Cascade)
|
||||
storageKey String // relative key, e.g. "mp3/<episodeId>.mp3"
|
||||
durationSec Int?
|
||||
sizeBytes Int?
|
||||
format String @default("mp3")
|
||||
/// Segment manifest for partial regeneration.
|
||||
segments Json?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@map("audio_asset")
|
||||
}
|
||||
|
||||
model CoverArt {
|
||||
id String @id @default(cuid())
|
||||
episodeId String @unique
|
||||
episode Episode @relation(fields: [episodeId], references: [id], onDelete: Cascade)
|
||||
storageKey String // relative key, e.g. "art/<episodeId>.png"
|
||||
prompt String @db.Text
|
||||
model String @default("dall-e-3")
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@map("cover_art")
|
||||
}
|
||||
|
||||
model RepurposedContent {
|
||||
id String @id @default(cuid())
|
||||
episodeId String
|
||||
episode Episode @relation(fields: [episodeId], references: [id], onDelete: Cascade)
|
||||
type String // "blog" | "social_thread" | "newsletter"
|
||||
content Json
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([episodeId])
|
||||
@@map("repurposed_content")
|
||||
}
|
||||
|
||||
/// Series / season planner (Pro+).
|
||||
model Series {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
organizationId String?
|
||||
organization Organization? @relation(fields: [organizationId], references: [id], onDelete: SetNull)
|
||||
title String
|
||||
description String? @db.Text
|
||||
plannedCount Int @default(0)
|
||||
/// Season plan: [{ title, topic, summary }]
|
||||
plan Json?
|
||||
episodes Episode[]
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([userId])
|
||||
@@map("series")
|
||||
}
|
||||
|
||||
// ─────────────────────────── Jobs / async ───────────────────────────
|
||||
|
||||
model GenerationJob {
|
||||
id String @id @default(cuid())
|
||||
episodeId String
|
||||
episode Episode @relation(fields: [episodeId], references: [id], onDelete: Cascade)
|
||||
pgBossJobId String?
|
||||
type String @default("full") // full | script | audio | art | section | repurpose
|
||||
status String @default("queued") // queued | running | completed | failed
|
||||
stage String?
|
||||
attempts Int @default(0)
|
||||
/// { openaiTokens, elevenChars, dalleImages, costUsd }
|
||||
costEstimate Json?
|
||||
error String?
|
||||
startedAt DateTime?
|
||||
finishedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([episodeId])
|
||||
@@index([status])
|
||||
@@map("generation_job")
|
||||
}
|
||||
|
||||
// ─────────────────────────── Pro API access ───────────────────────────
|
||||
|
||||
/// Custom API keys (we hash + verify ourselves; not the Better Auth apiKey plugin).
|
||||
model ApiKey {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
name String
|
||||
hashedKey String @unique
|
||||
prefix String // display only, e.g. "pky_live_abcd…"
|
||||
lastUsedAt DateTime?
|
||||
revokedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([userId])
|
||||
@@map("api_key")
|
||||
}
|
||||
|
||||
// ─────────────────────────── Admin / ops ───────────────────────────
|
||||
|
||||
model AuditLog {
|
||||
id String @id @default(cuid())
|
||||
actorId String?
|
||||
actor User? @relation("ActorLogs", fields: [actorId], references: [id], onDelete: SetNull)
|
||||
actorType String @default("user") // "user" | "admin" | "system"
|
||||
action String // "user.ban", "subscription.refund", "flag.toggle", …
|
||||
target String?
|
||||
metadata Json?
|
||||
ip String?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([action])
|
||||
@@index([createdAt])
|
||||
@@map("audit_log")
|
||||
}
|
||||
|
||||
model FeatureFlag {
|
||||
key String @id
|
||||
enabled Boolean @default(false)
|
||||
rolloutPct Int @default(0)
|
||||
metadata Json?
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("feature_flag")
|
||||
}
|
||||
|
||||
/// AI usage + cost monitoring.
|
||||
model AiCostLog {
|
||||
id String @id @default(cuid())
|
||||
provider String // "openai" | "elevenlabs"
|
||||
operation String // "script" | "audio" | "art" | "repurpose"
|
||||
episodeId String?
|
||||
userId String?
|
||||
units Int @default(0) // tokens | chars | images
|
||||
costUsd Decimal @default(0) @db.Decimal(10, 4)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([provider, createdAt])
|
||||
@@index([userId])
|
||||
@@map("ai_cost_log")
|
||||
}
|
||||
|
||||
/// Content moderation queue.
|
||||
model ContentFlag {
|
||||
id String @id @default(cuid())
|
||||
episodeId String
|
||||
episode Episode @relation(fields: [episodeId], references: [id], onDelete: Cascade)
|
||||
reason String
|
||||
status String @default("open") // open | reviewed | removed
|
||||
reviewedBy String?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([status])
|
||||
@@map("content_flag")
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// Seed the Plan catalog (admin-tunable mirror of lib/billing/plans.ts).
|
||||
// Run: npm run db:seed
|
||||
import "dotenv/config";
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { PLANS, PLAN_ORDER } from "@/lib/billing/plans";
|
||||
|
||||
async function main() {
|
||||
for (const key of PLAN_ORDER) {
|
||||
const plan = PLANS[key];
|
||||
await prisma.plan.upsert({
|
||||
where: { key },
|
||||
create: {
|
||||
key,
|
||||
name: plan.name,
|
||||
priceMonthly: plan.priceMonthly,
|
||||
priceYearly: plan.priceYearly,
|
||||
limits: plan.limits as unknown as Prisma.InputJsonValue,
|
||||
features: plan.features as unknown as Prisma.InputJsonValue,
|
||||
},
|
||||
update: {
|
||||
name: plan.name,
|
||||
priceMonthly: plan.priceMonthly,
|
||||
priceYearly: plan.priceYearly,
|
||||
limits: plan.limits as unknown as Prisma.InputJsonValue,
|
||||
features: plan.features as unknown as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
console.log(`✓ Seeded plan: ${plan.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((err) => {
|
||||
console.error("Seed failed:", err.message ?? err);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(() => prisma.$disconnect());
|
||||
Reference in New Issue
Block a user