Strip Songs2VID to a payment-free self-hosted OSS core.
Remove Stripe/billing/pricing/marketing, simplify schema and entitlements for unlimited self-host use, and keep auth, encode, and YouTube upload.
This commit is contained in:
@@ -1,90 +1,88 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "Plan" AS ENUM ('FREE', 'PREMIUM');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "Privacy" AS ENUM ('PUBLIC', 'PRIVATE', 'UNLISTED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "JobStatus" AS ENUM ('PENDING', 'PROCESSING', 'COMPLETED', 'FAILED', 'PARTIAL');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "JobItemStatus" AS ENUM ('PENDING', 'ENCODING', 'UPLOADING', 'COMPLETED', 'FAILED');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"name" TEXT,
|
||||
"image" TEXT,
|
||||
"plan" "Plan" NOT NULL DEFAULT 'FREE',
|
||||
"videosUsed" INTEGER NOT NULL DEFAULT 0,
|
||||
"quotaResetAt" TIMESTAMP(3) NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||
"id" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"name" TEXT,
|
||||
"image" TEXT,
|
||||
"createdVideoCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"apiKeyHash" TEXT,
|
||||
"apiKeyPrefix" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "YouTubeConnection" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"accessToken" TEXT NOT NULL,
|
||||
"refreshToken" TEXT NOT NULL,
|
||||
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||
"channelId" TEXT NOT NULL,
|
||||
"channelTitle" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "YouTubeConnection_pkey" PRIMARY KEY ("id")
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"accessToken" TEXT NOT NULL,
|
||||
"refreshToken" TEXT NOT NULL,
|
||||
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||
"channelId" TEXT NOT NULL,
|
||||
"channelTitle" TEXT NOT NULL,
|
||||
CONSTRAINT "YouTubeConnection_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Job" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"status" "JobStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"imagePath" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"completedAt" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "Job_pkey" PRIMARY KEY ("id")
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"status" "JobStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"imagePath" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"completedAt" TIMESTAMP(3),
|
||||
CONSTRAINT "Job_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "JobItem" (
|
||||
"id" TEXT NOT NULL,
|
||||
"jobId" TEXT NOT NULL,
|
||||
"audioPath" TEXT NOT NULL,
|
||||
"audioFilename" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"description" TEXT NOT NULL DEFAULT '',
|
||||
"tags" TEXT NOT NULL DEFAULT '',
|
||||
"privacy" "Privacy" NOT NULL DEFAULT 'PUBLIC',
|
||||
"categoryId" TEXT NOT NULL DEFAULT '10',
|
||||
"resolution" TEXT NOT NULL DEFAULT '1280x720',
|
||||
"notifySubscribers" BOOLEAN NOT NULL DEFAULT true,
|
||||
"madeForKids" BOOLEAN NOT NULL DEFAULT false,
|
||||
"embeddable" BOOLEAN NOT NULL DEFAULT true,
|
||||
"creativeCommons" BOOLEAN NOT NULL DEFAULT false,
|
||||
"includeWatermark" BOOLEAN NOT NULL DEFAULT true,
|
||||
"status" "JobItemStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"outputPath" TEXT,
|
||||
"youtubeVideoId" TEXT,
|
||||
"error" TEXT,
|
||||
|
||||
CONSTRAINT "JobItem_pkey" PRIMARY KEY ("id")
|
||||
"id" TEXT NOT NULL,
|
||||
"jobId" TEXT NOT NULL,
|
||||
"audioPath" TEXT NOT NULL,
|
||||
"audioFilename" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"description" TEXT NOT NULL DEFAULT '',
|
||||
"tags" TEXT NOT NULL DEFAULT '',
|
||||
"privacy" "Privacy" NOT NULL DEFAULT 'PUBLIC',
|
||||
"categoryId" TEXT NOT NULL DEFAULT '10',
|
||||
"resolution" TEXT NOT NULL DEFAULT '1280x720',
|
||||
"notifySubscribers" BOOLEAN NOT NULL DEFAULT true,
|
||||
"madeForKids" BOOLEAN NOT NULL DEFAULT false,
|
||||
"embeddable" BOOLEAN NOT NULL DEFAULT true,
|
||||
"creativeCommons" BOOLEAN NOT NULL DEFAULT false,
|
||||
"includeWatermark" BOOLEAN NOT NULL DEFAULT false,
|
||||
"itemImagePath" TEXT,
|
||||
"watermarkMode" TEXT NOT NULL DEFAULT 'none',
|
||||
"watermarkText" TEXT,
|
||||
"watermarkLogoPath" TEXT,
|
||||
"watermarkFontKey" TEXT,
|
||||
"watermarkFontPath" TEXT,
|
||||
"watermarkPosition" TEXT NOT NULL DEFAULT 'bottom-right',
|
||||
"watermarkOffsetX" INTEGER NOT NULL DEFAULT 20,
|
||||
"watermarkOffsetY" INTEGER NOT NULL DEFAULT 20,
|
||||
"artist" TEXT,
|
||||
"songTitle" TEXT,
|
||||
"layoutTemplate" TEXT,
|
||||
"blurAmount" INTEGER NOT NULL DEFAULT 55,
|
||||
"blurOpacity" INTEGER NOT NULL DEFAULT 100,
|
||||
"textPadding" INTEGER NOT NULL DEFAULT 48,
|
||||
"titleArtistGap" INTEGER NOT NULL DEFAULT 10,
|
||||
"textOffsetX" INTEGER NOT NULL DEFAULT 0,
|
||||
"textOffsetY" INTEGER NOT NULL DEFAULT 0,
|
||||
"playlistId" TEXT,
|
||||
"status" "JobItemStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"outputPath" TEXT,
|
||||
"youtubeVideoId" TEXT,
|
||||
"error" TEXT,
|
||||
CONSTRAINT "JobItem_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_apiKeyHash_key" ON "User"("apiKeyHash");
|
||||
CREATE UNIQUE INDEX "YouTubeConnection_userId_key" ON "YouTubeConnection"("userId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "YouTubeConnection" ADD CONSTRAINT "YouTubeConnection_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Job" ADD CONSTRAINT "Job_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "JobItem" ADD CONSTRAINT "JobItem_jobId_fkey" FOREIGN KEY ("jobId") REFERENCES "Job"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "YouTubeConnection" ADD CONSTRAINT "YouTubeConnection_userId_fkey"
|
||||
FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "Job" ADD CONSTRAINT "Job_userId_fkey"
|
||||
FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "JobItem" ADD CONSTRAINT "JobItem_jobId_fkey"
|
||||
FOREIGN KEY ("jobId") REFERENCES "Job"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "cardLast4" TEXT,
|
||||
ADD COLUMN "subscribedAt" TIMESTAMP(3);
|
||||
@@ -1,21 +0,0 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "QuotaExtensionRequestStatus" AS ENUM ('PENDING', 'APPROVED', 'REJECTED');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "bonusQuota" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "QuotaExtensionRequest" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"status" "QuotaExtensionRequestStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"message" TEXT NOT NULL DEFAULT '',
|
||||
"requestedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"processedAt" TIMESTAMP(3),
|
||||
"adminNote" TEXT,
|
||||
|
||||
CONSTRAINT "QuotaExtensionRequest_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "QuotaExtensionRequest" ADD CONSTRAINT "QuotaExtensionRequest_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -1,6 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "apiKeyHash" TEXT,
|
||||
ADD COLUMN "apiKeyPrefix" TEXT;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_apiKeyHash_key" ON "User"("apiKeyHash");
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "JobItem" ADD COLUMN "playlistId" TEXT;
|
||||
@@ -1,8 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "apiRateLimitBonus" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "QuotaExtensionKind" AS ENUM ('VIDEO_QUOTA', 'API_RATE_LIMIT');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "QuotaExtensionRequest" ADD COLUMN "kind" "QuotaExtensionKind" NOT NULL DEFAULT 'VIDEO_QUOTA';
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "JobItem" ADD COLUMN "songTitle" TEXT;
|
||||
@@ -1,43 +0,0 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "FooterBadge" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"embedHtml" TEXT,
|
||||
"imageUrl" TEXT,
|
||||
"linkUrl" TEXT,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "FooterBadge_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "FooterBadge_isActive_sortOrder_idx" ON "FooterBadge"("isActive", "sortOrder");
|
||||
|
||||
-- Seed current Product Hunt + LaunchBuff badges
|
||||
INSERT INTO "FooterBadge" ("id", "name", "embedHtml", "imageUrl", "linkUrl", "isActive", "sortOrder", "createdAt", "updatedAt")
|
||||
VALUES
|
||||
(
|
||||
'seed_producthunt',
|
||||
'Product Hunt',
|
||||
'<a href="https://www.producthunt.com/products/songs2vid?embed=true&utm_source=badge-featured&utm_medium=badge&utm_campaign=badge-songs2vid" target="_blank" rel="noopener noreferrer"><img alt="Songs2VID - Bulk render audio & artworks into videos in seconds | Product Hunt" width="250" height="54" src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=1206681&theme=dark&t=1785422871345" /></a>',
|
||||
'https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=1206681&theme=dark&t=1785422871345',
|
||||
'https://www.producthunt.com/products/songs2vid?embed=true&utm_source=badge-featured&utm_medium=badge&utm_campaign=badge-songs2vid',
|
||||
true,
|
||||
0,
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP
|
||||
),
|
||||
(
|
||||
'seed_launchbuff',
|
||||
'LaunchBuff',
|
||||
'<a href="https://launchbuff.com" target="_blank" rel="noopener noreferrer" title="Featured on LaunchBuff"><img src="https://launchbuff.com/badge-featured-dark.svg" alt="Featured on LaunchBuff" width="256" height="80" /></a>',
|
||||
'https://launchbuff.com/badge-featured-dark.svg',
|
||||
'https://launchbuff.com',
|
||||
true,
|
||||
1,
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP
|
||||
);
|
||||
@@ -1,10 +0,0 @@
|
||||
-- AlterEnum
|
||||
CREATE TYPE "SubscriptionStatus" AS ENUM ('free', 'pro');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "subscriptionStatus" "SubscriptionStatus" NOT NULL DEFAULT 'free';
|
||||
ALTER TABLE "User" ADD COLUMN "createdVideoCount" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
-- Backfill from existing Plan
|
||||
UPDATE "User" SET "subscriptionStatus" = 'pro' WHERE "plan" = 'PREMIUM';
|
||||
UPDATE "User" SET "subscriptionStatus" = 'free' WHERE "plan" = 'FREE';
|
||||
+12
-124
@@ -7,17 +7,6 @@ datasource db {
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
enum Plan {
|
||||
FREE
|
||||
PREMIUM
|
||||
}
|
||||
|
||||
/// Public-facing subscription tier (kept in sync with `plan`: FREE→free, PREMIUM→pro)
|
||||
enum SubscriptionStatus {
|
||||
free
|
||||
pro
|
||||
}
|
||||
|
||||
enum Privacy {
|
||||
PUBLIC
|
||||
PRIVATE
|
||||
@@ -40,86 +29,17 @@ enum JobItemStatus {
|
||||
FAILED
|
||||
}
|
||||
|
||||
enum QuotaExtensionRequestStatus {
|
||||
PENDING
|
||||
APPROVED
|
||||
REJECTED
|
||||
}
|
||||
|
||||
enum QuotaExtensionKind {
|
||||
VIDEO_QUOTA
|
||||
API_RATE_LIMIT
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
email String @unique
|
||||
name String?
|
||||
image String?
|
||||
/// FREE = free tier; PREMIUM = Pro (€5/mo, formerly starter_5eur)
|
||||
plan Plan @default(FREE)
|
||||
/// Mirrors plan for product/UI (`free` | `pro`). Prefer `plan` for entitlements.
|
||||
subscriptionStatus SubscriptionStatus @default(free)
|
||||
/// Lifetime count of successfully completed video renders (not monthly quota).
|
||||
createdVideoCount Int @default(0)
|
||||
/// Credits consumed in the current billing cycle (credits_used)
|
||||
videosUsed Int @default(0)
|
||||
/// Monthly allocation for the current cycle (10 free / 50 pro)
|
||||
monthlyCredits Int @default(10)
|
||||
quotaResetAt DateTime
|
||||
cardLast4 String?
|
||||
subscribedAt DateTime?
|
||||
bonusQuota Int @default(0)
|
||||
apiRateLimitBonus Int @default(0)
|
||||
/// Purchased extras that never expire (maps to legacy "videoCredits" column)
|
||||
extraCredits Int @default(0) @map("videoCredits")
|
||||
/// Free-tier one-time +15 top-up already purchased
|
||||
freeTopUpPurchased Boolean @default(false)
|
||||
stripeCustomerId String?
|
||||
stripeSubscriptionId String?
|
||||
apiKeyHash String? @unique
|
||||
apiKeyPrefix String?
|
||||
youtubeConnection YouTubeConnection?
|
||||
jobs Job[]
|
||||
quotaExtensionRequests QuotaExtensionRequest[]
|
||||
creditPurchases CreditPurchase[]
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
enum CreditPurchaseStatus {
|
||||
PENDING
|
||||
COMPLETED
|
||||
FAILED
|
||||
}
|
||||
|
||||
model CreditPurchase {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
credits Int
|
||||
amountCents Int
|
||||
status CreditPurchaseStatus @default(PENDING)
|
||||
stripeSessionId String? @unique
|
||||
stripePaymentIntentId String?
|
||||
createdAt DateTime @default(now())
|
||||
completedAt DateTime?
|
||||
}
|
||||
|
||||
enum JobItemBilling {
|
||||
QUOTA
|
||||
CREDIT
|
||||
}
|
||||
|
||||
model QuotaExtensionRequest {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
kind QuotaExtensionKind @default(VIDEO_QUOTA)
|
||||
status QuotaExtensionRequestStatus @default(PENDING)
|
||||
message String @default("")
|
||||
requestedAt DateTime @default(now())
|
||||
processedAt DateTime?
|
||||
adminNote String?
|
||||
id String @id @default(cuid())
|
||||
email String @unique
|
||||
name String?
|
||||
image String?
|
||||
createdVideoCount Int @default(0)
|
||||
apiKeyHash String? @unique
|
||||
apiKeyPrefix String?
|
||||
youtubeConnection YouTubeConnection?
|
||||
jobs Job[]
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model YouTubeConnection {
|
||||
@@ -160,60 +80,28 @@ model JobItem {
|
||||
madeForKids Boolean @default(false)
|
||||
embeddable Boolean @default(true)
|
||||
creativeCommons Boolean @default(false)
|
||||
includeWatermark Boolean @default(true)
|
||||
/// Optional per-item cover (Pro). Falls back to Job.imagePath when null.
|
||||
includeWatermark Boolean @default(false)
|
||||
itemImagePath String?
|
||||
/// none | default | text | logo custom modes require Pro
|
||||
watermarkMode String @default("default")
|
||||
watermarkMode String @default("none")
|
||||
watermarkText String?
|
||||
watermarkLogoPath String?
|
||||
/// curated key | custom | system
|
||||
watermarkFontKey String?
|
||||
/// uploaded .ttf/.otf when fontKey=custom
|
||||
watermarkFontPath String?
|
||||
/// top-left | top-right | bottom-left | bottom-right | center
|
||||
watermarkPosition String @default("bottom-right")
|
||||
watermarkOffsetX Int @default(20)
|
||||
watermarkOffsetY Int @default(20)
|
||||
/// Optional artist line for art-track layouts (Pro, on-video only)
|
||||
artist String?
|
||||
/// Song title burned into art-track layouts (Pro, on-video only)
|
||||
songTitle String?
|
||||
/// COVER_LEFT_TEXT_RIGHT | COVER_TOP_TEXT_BOTTOM | COVER_RIGHT_TEXT_LEFT | CENTERED_COMPACT | null=classic
|
||||
layoutTemplate String?
|
||||
/// 0–100 Gaussian / boxblur intensity (Pro art-track)
|
||||
blurAmount Int @default(55)
|
||||
/// 0–100 visibility of blurred fill vs black
|
||||
blurOpacity Int @default(100)
|
||||
/// Padding around cover + text in art-track layouts
|
||||
textPadding Int @default(48)
|
||||
/// Extra gap between title and artist lines (px)
|
||||
titleArtistGap Int @default(10)
|
||||
/// Fine-tune text block within template (-120..120)
|
||||
textOffsetX Int @default(0)
|
||||
textOffsetY Int @default(0)
|
||||
playlistId String?
|
||||
billingSource JobItemBilling @default(QUOTA)
|
||||
status JobItemStatus @default(PENDING)
|
||||
outputPath String?
|
||||
youtubeVideoId String?
|
||||
error String?
|
||||
}
|
||||
|
||||
/// Site footer badge / launch-directory embeds (managed via /admin)
|
||||
model FooterBadge {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
/// Raw paste from Product Hunt / LaunchBuff / etc. (preferred source)
|
||||
embedHtml String? @db.Text
|
||||
/// Parsed or manually set image URL (used when embedHtml is empty)
|
||||
imageUrl String?
|
||||
/// Parsed or manually set destination URL
|
||||
linkUrl String?
|
||||
isActive Boolean @default(true)
|
||||
sortOrder Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([isActive, sortOrder])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user