mirror of
https://github.com/diced/zipline.git
synced 2025-12-12 07:40:45 -08:00
feat: initial drizzle setup
This commit is contained in:
13
drizzle.config.ts
Normal file
13
drizzle.config.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import 'dotenv/config';
|
||||
import { defineConfig } from 'drizzle-kit';
|
||||
|
||||
export default defineConfig({
|
||||
dialect: 'postgresql',
|
||||
out: './src/drizzle',
|
||||
schema: './src/drizzle/schema.ts',
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL as string,
|
||||
},
|
||||
verbose: true,
|
||||
strict: true,
|
||||
});
|
||||
287
src/drizzle/0000_bouncy_mantis.sql
Normal file
287
src/drizzle/0000_bouncy_mantis.sql
Normal file
@@ -0,0 +1,287 @@
|
||||
CREATE TYPE "public"."IncompleteFileStatus" AS ENUM('PENDING', 'PROCESSING', 'COMPLETE', 'FAILED');--> statement-breakpoint
|
||||
CREATE TYPE "public"."OAuthProviderType" AS ENUM('DISCORD', 'GOOGLE', 'GITHUB', 'OIDC');--> statement-breakpoint
|
||||
CREATE TYPE "public"."Role" AS ENUM('USER', 'ADMIN', 'SUPERADMIN');--> statement-breakpoint
|
||||
CREATE TYPE "public"."UserFilesQuota" AS ENUM('BY_BYTES', 'BY_FILES');--> statement-breakpoint
|
||||
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "Zipline" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
"updatedAt" timestamp(3) NOT NULL,
|
||||
"firstSetup" boolean DEFAULT true NOT NULL,
|
||||
"coreReturnHttpsUrls" boolean DEFAULT false NOT NULL,
|
||||
"coreDefaultDomain" text,
|
||||
"coreTempDirectory" text NOT NULL,
|
||||
"chunksEnabled" boolean DEFAULT true NOT NULL,
|
||||
"chunksMax" text DEFAULT '95mb' NOT NULL,
|
||||
"chunksSize" text DEFAULT '25mb' NOT NULL,
|
||||
"tasksDeleteInterval" text DEFAULT '30m' NOT NULL,
|
||||
"tasksClearInvitesInterval" text DEFAULT '30m' NOT NULL,
|
||||
"tasksMaxViewsInterval" text DEFAULT '30m' NOT NULL,
|
||||
"tasksThumbnailsInterval" text DEFAULT '30m' NOT NULL,
|
||||
"tasksMetricsInterval" text DEFAULT '30m' NOT NULL,
|
||||
"filesRoute" text DEFAULT '/u' NOT NULL,
|
||||
"filesLength" integer DEFAULT 6 NOT NULL,
|
||||
"filesDefaultFormat" text DEFAULT 'random' NOT NULL,
|
||||
"filesDisabledExtensions" text[],
|
||||
"filesMaxFileSize" text DEFAULT '100mb' NOT NULL,
|
||||
"filesDefaultExpiration" text,
|
||||
"filesAssumeMimetypes" boolean DEFAULT false NOT NULL,
|
||||
"filesDefaultDateFormat" text DEFAULT 'YYYY-MM-DD_HH:mm:ss' NOT NULL,
|
||||
"filesRemoveGpsMetadata" boolean DEFAULT false NOT NULL,
|
||||
"urlsRoute" text DEFAULT '/go' NOT NULL,
|
||||
"urlsLength" integer DEFAULT 6 NOT NULL,
|
||||
"featuresImageCompression" boolean DEFAULT true NOT NULL,
|
||||
"featuresRobotsTxt" boolean DEFAULT true NOT NULL,
|
||||
"featuresHealthcheck" boolean DEFAULT true NOT NULL,
|
||||
"featuresUserRegistration" boolean DEFAULT false NOT NULL,
|
||||
"featuresOauthRegistration" boolean DEFAULT false NOT NULL,
|
||||
"featuresDeleteOnMaxViews" boolean DEFAULT true NOT NULL,
|
||||
"featuresThumbnailsEnabled" boolean DEFAULT true NOT NULL,
|
||||
"featuresThumbnailsNumberThreads" integer DEFAULT 4 NOT NULL,
|
||||
"featuresMetricsEnabled" boolean DEFAULT true NOT NULL,
|
||||
"featuresMetricsAdminOnly" boolean DEFAULT false NOT NULL,
|
||||
"featuresMetricsShowUserSpecific" boolean DEFAULT true NOT NULL,
|
||||
"invitesEnabled" boolean DEFAULT true NOT NULL,
|
||||
"invitesLength" integer DEFAULT 6 NOT NULL,
|
||||
"websiteTitle" text DEFAULT 'Zipline' NOT NULL,
|
||||
"websiteTitleLogo" text,
|
||||
"websiteExternalLinks" jsonb DEFAULT '[{"url":"https://github.com/diced/zipline","name":"GitHub"},{"url":"https://zipline.diced.sh/","name":"Documentation"}]'::jsonb NOT NULL,
|
||||
"websiteLoginBackground" text,
|
||||
"websiteDefaultAvatar" text,
|
||||
"websiteTos" text,
|
||||
"websiteThemeDefault" text DEFAULT 'system' NOT NULL,
|
||||
"websiteThemeDark" text DEFAULT 'builtin:dark_gray' NOT NULL,
|
||||
"websiteThemeLight" text DEFAULT 'builtin:light_gray' NOT NULL,
|
||||
"oauthBypassLocalLogin" boolean DEFAULT false NOT NULL,
|
||||
"oauthLoginOnly" boolean DEFAULT false NOT NULL,
|
||||
"oauthDiscordClientId" text,
|
||||
"oauthDiscordClientSecret" text,
|
||||
"oauthDiscordRedirectUri" text,
|
||||
"oauthGoogleClientId" text,
|
||||
"oauthGoogleClientSecret" text,
|
||||
"oauthGoogleRedirectUri" text,
|
||||
"oauthGithubClientId" text,
|
||||
"oauthGithubClientSecret" text,
|
||||
"oauthGithubRedirectUri" text,
|
||||
"oauthOidcClientId" text,
|
||||
"oauthOidcClientSecret" text,
|
||||
"oauthOidcAuthorizeUrl" text,
|
||||
"oauthOidcTokenUrl" text,
|
||||
"oauthOidcUserinfoUrl" text,
|
||||
"oauthOidcRedirectUri" text,
|
||||
"mfaTotpEnabled" boolean DEFAULT false NOT NULL,
|
||||
"mfaTotpIssuer" text DEFAULT 'Zipline' NOT NULL,
|
||||
"mfaPasskeys" boolean DEFAULT false NOT NULL,
|
||||
"ratelimitEnabled" boolean DEFAULT true NOT NULL,
|
||||
"ratelimitMax" integer DEFAULT 10 NOT NULL,
|
||||
"ratelimitWindow" integer,
|
||||
"ratelimitAdminBypass" boolean DEFAULT true NOT NULL,
|
||||
"ratelimitAllowList" text[],
|
||||
"httpWebhookOnUpload" text,
|
||||
"httpWebhookOnShorten" text,
|
||||
"discordWebhookUrl" text,
|
||||
"discordUsername" text,
|
||||
"discordAvatarUrl" text,
|
||||
"discordOnUploadWebhookUrl" text,
|
||||
"discordOnUploadUsername" text,
|
||||
"discordOnUploadAvatarUrl" text,
|
||||
"discordOnUploadContent" text,
|
||||
"discordOnUploadEmbed" jsonb,
|
||||
"discordOnShortenWebhookUrl" text,
|
||||
"discordOnShortenUsername" text,
|
||||
"discordOnShortenAvatarUrl" text,
|
||||
"discordOnShortenContent" text,
|
||||
"discordOnShortenEmbed" jsonb,
|
||||
"pwaEnabled" boolean DEFAULT false NOT NULL,
|
||||
"pwaTitle" text DEFAULT 'Zipline' NOT NULL,
|
||||
"pwaShortName" text DEFAULT 'Zipline' NOT NULL,
|
||||
"pwaDescription" text DEFAULT 'Zipline' NOT NULL,
|
||||
"pwaThemeColor" text DEFAULT '#000000' NOT NULL,
|
||||
"pwaBackgroundColor" text DEFAULT '#000000' NOT NULL,
|
||||
"websiteLoginBackgroundBlur" boolean DEFAULT true NOT NULL,
|
||||
"filesRandomWordsNumAdjectives" integer DEFAULT 2 NOT NULL,
|
||||
"filesRandomWordsSeparator" text DEFAULT '-' NOT NULL,
|
||||
"featuresVersionAPI" text DEFAULT 'https://zipline-version.diced.sh' NOT NULL,
|
||||
"featuresVersionChecking" boolean DEFAULT true NOT NULL,
|
||||
"oauthDiscordAllowedIds" text[] DEFAULT '{"RAY"}',
|
||||
"oauthDiscordDeniedIds" text[] DEFAULT '{"RAY"}',
|
||||
"domains" text[] DEFAULT '{"RAY"}',
|
||||
"filesDefaultCompressionFormat" text DEFAULT 'jpg',
|
||||
"featuresThumbnailsFormat" text DEFAULT 'jpg' NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "Metric" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
"updatedAt" timestamp(3) NOT NULL,
|
||||
"data" jsonb NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "Url" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
"updatedAt" timestamp(3) NOT NULL,
|
||||
"code" text NOT NULL,
|
||||
"vanity" text,
|
||||
"destination" text NOT NULL,
|
||||
"views" integer DEFAULT 0 NOT NULL,
|
||||
"maxViews" integer,
|
||||
"password" text,
|
||||
"userId" text,
|
||||
"enabled" boolean DEFAULT true NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "Folder" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
"updatedAt" timestamp(3) NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"public" boolean DEFAULT false NOT NULL,
|
||||
"userId" text NOT NULL,
|
||||
"allowUploads" boolean DEFAULT false NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "User" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
"updatedAt" timestamp(3) NOT NULL,
|
||||
"username" text NOT NULL,
|
||||
"password" text,
|
||||
"avatar" text,
|
||||
"token" text NOT NULL,
|
||||
"role" "Role" DEFAULT 'USER' NOT NULL,
|
||||
"view" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
"totpSecret" text,
|
||||
"sessions" text[]
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "Export" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
"updatedAt" timestamp(3) NOT NULL,
|
||||
"completed" boolean DEFAULT false NOT NULL,
|
||||
"path" text NOT NULL,
|
||||
"files" integer NOT NULL,
|
||||
"size" text NOT NULL,
|
||||
"userId" text NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "UserQuota" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
"updatedAt" timestamp(3) NOT NULL,
|
||||
"filesQuota" "UserFilesQuota" NOT NULL,
|
||||
"maxBytes" text,
|
||||
"maxFiles" integer,
|
||||
"maxUrls" integer,
|
||||
"userId" text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "UserPasskey" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
"updatedAt" timestamp(3) NOT NULL,
|
||||
"lastUsed" timestamp(3),
|
||||
"name" text NOT NULL,
|
||||
"reg" jsonb NOT NULL,
|
||||
"userId" text NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "OAuthProvider" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
"updatedAt" timestamp(3) NOT NULL,
|
||||
"userId" text NOT NULL,
|
||||
"provider" "OAuthProviderType" NOT NULL,
|
||||
"username" text NOT NULL,
|
||||
"accessToken" text NOT NULL,
|
||||
"refreshToken" text,
|
||||
"oauthId" text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "File" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
"updatedAt" timestamp(3) NOT NULL,
|
||||
"deletesAt" timestamp(3),
|
||||
"name" text NOT NULL,
|
||||
"originalName" text,
|
||||
"size" bigint NOT NULL,
|
||||
"type" text NOT NULL,
|
||||
"views" integer DEFAULT 0 NOT NULL,
|
||||
"maxViews" integer,
|
||||
"favorite" boolean DEFAULT false NOT NULL,
|
||||
"password" text,
|
||||
"userId" text,
|
||||
"folderId" text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "Thumbnail" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
"updatedAt" timestamp(3) NOT NULL,
|
||||
"path" text NOT NULL,
|
||||
"fileId" text NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "IncompleteFile" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
"updatedAt" timestamp(3) NOT NULL,
|
||||
"status" "IncompleteFileStatus" NOT NULL,
|
||||
"chunksTotal" integer NOT NULL,
|
||||
"chunksComplete" integer NOT NULL,
|
||||
"metadata" jsonb NOT NULL,
|
||||
"userId" text NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "Tag" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
"updatedAt" timestamp(3) NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"color" text NOT NULL,
|
||||
"userId" text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "Invite" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
"updatedAt" timestamp(3) NOT NULL,
|
||||
"expiresAt" timestamp(3),
|
||||
"code" text NOT NULL,
|
||||
"uses" integer DEFAULT 0 NOT NULL,
|
||||
"maxUses" integer,
|
||||
"inviterId" text NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "_FileToTag" (
|
||||
"A" text NOT NULL,
|
||||
"B" text NOT NULL,
|
||||
CONSTRAINT "_FileToTag_AB_pkey" PRIMARY KEY("A","B")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "Url" ADD CONSTRAINT "Url_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "Folder" ADD CONSTRAINT "Folder_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "Export" ADD CONSTRAINT "Export_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "UserQuota" ADD CONSTRAINT "UserQuota_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "UserPasskey" ADD CONSTRAINT "UserPasskey_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "OAuthProvider" ADD CONSTRAINT "OAuthProvider_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE restrict ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "File" ADD CONSTRAINT "File_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "File" ADD CONSTRAINT "File_folderId_fkey" FOREIGN KEY ("folderId") REFERENCES "public"."Folder"("id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "Thumbnail" ADD CONSTRAINT "Thumbnail_fileId_fkey" FOREIGN KEY ("fileId") REFERENCES "public"."File"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "IncompleteFile" ADD CONSTRAINT "IncompleteFile_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "Tag" ADD CONSTRAINT "Tag_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "Invite" ADD CONSTRAINT "Invite_inviterId_fkey" FOREIGN KEY ("inviterId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "_FileToTag" ADD CONSTRAINT "_FileToTag_A_fkey" FOREIGN KEY ("A") REFERENCES "public"."File"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "_FileToTag" ADD CONSTRAINT "_FileToTag_B_fkey" FOREIGN KEY ("B") REFERENCES "public"."Tag"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "Url_code_vanity_key" ON "Url" USING btree ("code" text_ops,"vanity" text_ops);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "User_token_key" ON "User" USING btree ("token" text_ops);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "User_username_key" ON "User" USING btree ("username" text_ops);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "UserQuota_userId_key" ON "UserQuota" USING btree ("userId" text_ops);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "OAuthProvider_provider_oauthId_key" ON "OAuthProvider" USING btree ("provider" text_ops,"oauthId" text_ops);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "Thumbnail_fileId_key" ON "Thumbnail" USING btree ("fileId" text_ops);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "Tag_name_key" ON "Tag" USING btree ("name" text_ops);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "Invite_code_key" ON "Invite" USING btree ("code" text_ops);--> statement-breakpoint
|
||||
CREATE INDEX "_FileToTag_B_index" ON "_FileToTag" USING btree ("B" text_ops);
|
||||
2037
src/drizzle/meta/0000_snapshot.json
Normal file
2037
src/drizzle/meta/0000_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
20
src/drizzle/meta/_journal.json
Normal file
20
src/drizzle/meta/_journal.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1756926875085,
|
||||
"tag": "0000_bouncy_mantis",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "7",
|
||||
"when": 1756931651183,
|
||||
"tag": "0001_next_red_ghost",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
125
src/drizzle/relations.ts
Normal file
125
src/drizzle/relations.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { relations } from 'drizzle-orm/relations';
|
||||
import {
|
||||
user,
|
||||
url,
|
||||
folder,
|
||||
exportTable,
|
||||
userQuota,
|
||||
userPasskey,
|
||||
oauthProvider,
|
||||
file,
|
||||
thumbnail,
|
||||
incompleteFile,
|
||||
tag,
|
||||
invite,
|
||||
fileToTag,
|
||||
} from './schema';
|
||||
|
||||
export const urlRelations = relations(url, ({ one }) => ({
|
||||
user: one(user, {
|
||||
fields: [url.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const userRelations = relations(user, ({ many }) => ({
|
||||
urls: many(url),
|
||||
folders: many(folder),
|
||||
exports: many(exportTable),
|
||||
userQuotas: many(userQuota),
|
||||
userPasskeys: many(userPasskey),
|
||||
oauthProviders: many(oauthProvider),
|
||||
files: many(file),
|
||||
incompleteFiles: many(incompleteFile),
|
||||
tags: many(tag),
|
||||
invites: many(invite),
|
||||
}));
|
||||
|
||||
export const folderRelations = relations(folder, ({ one, many }) => ({
|
||||
user: one(user, {
|
||||
fields: [folder.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
files: many(file),
|
||||
}));
|
||||
|
||||
export const exportRelations = relations(exportTable, ({ one }) => ({
|
||||
user: one(user, {
|
||||
fields: [exportTable.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const userQuotaRelations = relations(userQuota, ({ one }) => ({
|
||||
user: one(user, {
|
||||
fields: [userQuota.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const userPasskeyRelations = relations(userPasskey, ({ one }) => ({
|
||||
user: one(user, {
|
||||
fields: [userPasskey.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const oauthProviderRelations = relations(oauthProvider, ({ one }) => ({
|
||||
user: one(user, {
|
||||
fields: [oauthProvider.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const fileRelations = relations(file, ({ one, many }) => ({
|
||||
user: one(user, {
|
||||
fields: [file.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
folder: one(folder, {
|
||||
fields: [file.folderId],
|
||||
references: [folder.id],
|
||||
}),
|
||||
thumbnails: many(thumbnail),
|
||||
fileToTags: many(fileToTag),
|
||||
}));
|
||||
|
||||
export const thumbnailRelations = relations(thumbnail, ({ one }) => ({
|
||||
file: one(file, {
|
||||
fields: [thumbnail.fileId],
|
||||
references: [file.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const incompleteFileRelations = relations(incompleteFile, ({ one }) => ({
|
||||
user: one(user, {
|
||||
fields: [incompleteFile.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const tagRelations = relations(tag, ({ one, many }) => ({
|
||||
user: one(user, {
|
||||
fields: [tag.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
fileToTags: many(fileToTag),
|
||||
}));
|
||||
|
||||
export const inviteRelations = relations(invite, ({ one }) => ({
|
||||
user: one(user, {
|
||||
fields: [invite.inviterId],
|
||||
references: [user.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const fileToTagRelations = relations(fileToTag, ({ one }) => ({
|
||||
file: one(file, {
|
||||
fields: [fileToTag.a],
|
||||
references: [file.id],
|
||||
}),
|
||||
tag: one(tag, {
|
||||
fields: [fileToTag.b],
|
||||
references: [tag.id],
|
||||
}),
|
||||
}));
|
||||
497
src/drizzle/schema.ts
Normal file
497
src/drizzle/schema.ts
Normal file
@@ -0,0 +1,497 @@
|
||||
import {
|
||||
pgTable,
|
||||
timestamp,
|
||||
text,
|
||||
integer,
|
||||
boolean,
|
||||
jsonb,
|
||||
uniqueIndex,
|
||||
foreignKey,
|
||||
bigint,
|
||||
index,
|
||||
primaryKey,
|
||||
pgEnum,
|
||||
} from 'drizzle-orm/pg-core';
|
||||
import { sql } from 'drizzle-orm';
|
||||
|
||||
export const incompleteFileStatus = pgEnum('IncompleteFileStatus', [
|
||||
'PENDING',
|
||||
'PROCESSING',
|
||||
'COMPLETE',
|
||||
'FAILED',
|
||||
]);
|
||||
export const oauthProviderType = pgEnum('OAuthProviderType', ['DISCORD', 'GOOGLE', 'GITHUB', 'OIDC']);
|
||||
export const role = pgEnum('Role', ['USER', 'ADMIN', 'SUPERADMIN']);
|
||||
export const userFilesQuota = pgEnum('UserFilesQuota', ['BY_BYTES', 'BY_FILES']);
|
||||
|
||||
export const zipline = pgTable('Zipline', {
|
||||
id: text().primaryKey().notNull(),
|
||||
createdAt: timestamp({ precision: 3, mode: 'string' })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull(),
|
||||
updatedAt: timestamp({ precision: 3, mode: 'string' }).notNull(),
|
||||
firstSetup: boolean().default(true).notNull(),
|
||||
coreReturnHttpsUrls: boolean().default(false).notNull(),
|
||||
coreDefaultDomain: text(),
|
||||
coreTempDirectory: text().notNull(),
|
||||
chunksEnabled: boolean().default(true).notNull(),
|
||||
chunksMax: text().default('95mb').notNull(),
|
||||
chunksSize: text().default('25mb').notNull(),
|
||||
tasksDeleteInterval: text().default('30m').notNull(),
|
||||
tasksClearInvitesInterval: text().default('30m').notNull(),
|
||||
tasksMaxViewsInterval: text().default('30m').notNull(),
|
||||
tasksThumbnailsInterval: text().default('30m').notNull(),
|
||||
tasksMetricsInterval: text().default('30m').notNull(),
|
||||
filesRoute: text().default('/u').notNull(),
|
||||
filesLength: integer().default(6).notNull(),
|
||||
filesDefaultFormat: text().default('random').notNull(),
|
||||
filesDisabledExtensions: text().array(),
|
||||
filesMaxFileSize: text().default('100mb').notNull(),
|
||||
filesDefaultExpiration: text(),
|
||||
filesAssumeMimetypes: boolean().default(false).notNull(),
|
||||
filesDefaultDateFormat: text().default('YYYY-MM-DD_HH:mm:ss').notNull(),
|
||||
filesRemoveGpsMetadata: boolean().default(false).notNull(),
|
||||
urlsRoute: text().default('/go').notNull(),
|
||||
urlsLength: integer().default(6).notNull(),
|
||||
featuresImageCompression: boolean().default(true).notNull(),
|
||||
featuresRobotsTxt: boolean().default(true).notNull(),
|
||||
featuresHealthcheck: boolean().default(true).notNull(),
|
||||
featuresUserRegistration: boolean().default(false).notNull(),
|
||||
featuresOauthRegistration: boolean().default(false).notNull(),
|
||||
featuresDeleteOnMaxViews: boolean().default(true).notNull(),
|
||||
featuresThumbnailsEnabled: boolean().default(true).notNull(),
|
||||
featuresThumbnailsNumberThreads: integer().default(4).notNull(),
|
||||
featuresMetricsEnabled: boolean().default(true).notNull(),
|
||||
featuresMetricsAdminOnly: boolean().default(false).notNull(),
|
||||
featuresMetricsShowUserSpecific: boolean().default(true).notNull(),
|
||||
invitesEnabled: boolean().default(true).notNull(),
|
||||
invitesLength: integer().default(6).notNull(),
|
||||
websiteTitle: text().default('Zipline').notNull(),
|
||||
websiteTitleLogo: text(),
|
||||
websiteExternalLinks: jsonb()
|
||||
.default([
|
||||
{ url: 'https://github.com/diced/zipline', name: 'GitHub' },
|
||||
{ url: 'https://zipline.diced.sh/', name: 'Documentation' },
|
||||
])
|
||||
.notNull(),
|
||||
websiteLoginBackground: text(),
|
||||
websiteDefaultAvatar: text(),
|
||||
websiteTos: text(),
|
||||
websiteThemeDefault: text().default('system').notNull(),
|
||||
websiteThemeDark: text().default('builtin:dark_gray').notNull(),
|
||||
websiteThemeLight: text().default('builtin:light_gray').notNull(),
|
||||
oauthBypassLocalLogin: boolean().default(false).notNull(),
|
||||
oauthLoginOnly: boolean().default(false).notNull(),
|
||||
oauthDiscordClientId: text(),
|
||||
oauthDiscordClientSecret: text(),
|
||||
oauthDiscordRedirectUri: text(),
|
||||
oauthGoogleClientId: text(),
|
||||
oauthGoogleClientSecret: text(),
|
||||
oauthGoogleRedirectUri: text(),
|
||||
oauthGithubClientId: text(),
|
||||
oauthGithubClientSecret: text(),
|
||||
oauthGithubRedirectUri: text(),
|
||||
oauthOidcClientId: text(),
|
||||
oauthOidcClientSecret: text(),
|
||||
oauthOidcAuthorizeUrl: text(),
|
||||
oauthOidcTokenUrl: text(),
|
||||
oauthOidcUserinfoUrl: text(),
|
||||
oauthOidcRedirectUri: text(),
|
||||
mfaTotpEnabled: boolean().default(false).notNull(),
|
||||
mfaTotpIssuer: text().default('Zipline').notNull(),
|
||||
mfaPasskeys: boolean().default(false).notNull(),
|
||||
ratelimitEnabled: boolean().default(true).notNull(),
|
||||
ratelimitMax: integer().default(10).notNull(),
|
||||
ratelimitWindow: integer(),
|
||||
ratelimitAdminBypass: boolean().default(true).notNull(),
|
||||
ratelimitAllowList: text().array(),
|
||||
httpWebhookOnUpload: text(),
|
||||
httpWebhookOnShorten: text(),
|
||||
discordWebhookUrl: text(),
|
||||
discordUsername: text(),
|
||||
discordAvatarUrl: text(),
|
||||
discordOnUploadWebhookUrl: text(),
|
||||
discordOnUploadUsername: text(),
|
||||
discordOnUploadAvatarUrl: text(),
|
||||
discordOnUploadContent: text(),
|
||||
discordOnUploadEmbed: jsonb(),
|
||||
discordOnShortenWebhookUrl: text(),
|
||||
discordOnShortenUsername: text(),
|
||||
discordOnShortenAvatarUrl: text(),
|
||||
discordOnShortenContent: text(),
|
||||
discordOnShortenEmbed: jsonb(),
|
||||
pwaEnabled: boolean().default(false).notNull(),
|
||||
pwaTitle: text().default('Zipline').notNull(),
|
||||
pwaShortName: text().default('Zipline').notNull(),
|
||||
pwaDescription: text().default('Zipline').notNull(),
|
||||
pwaThemeColor: text().default('#000000').notNull(),
|
||||
pwaBackgroundColor: text().default('#000000').notNull(),
|
||||
websiteLoginBackgroundBlur: boolean().default(true).notNull(),
|
||||
filesRandomWordsNumAdjectives: integer().default(2).notNull(),
|
||||
filesRandomWordsSeparator: text().default('-').notNull(),
|
||||
featuresVersionAPI: text().default('https://zipline-version.diced.sh').notNull(),
|
||||
featuresVersionChecking: boolean().default(true).notNull(),
|
||||
oauthDiscordAllowedIds: text().array().default(['RAY']),
|
||||
oauthDiscordDeniedIds: text().array().default(['RAY']),
|
||||
domains: text().array().default(['RAY']),
|
||||
filesDefaultCompressionFormat: text().default('jpg'),
|
||||
featuresThumbnailsFormat: text().default('jpg').notNull(),
|
||||
});
|
||||
|
||||
export const metric = pgTable('Metric', {
|
||||
id: text().primaryKey().notNull(),
|
||||
createdAt: timestamp({ precision: 3, mode: 'string' })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull(),
|
||||
updatedAt: timestamp({ precision: 3, mode: 'string' }).notNull(),
|
||||
data: jsonb().notNull(),
|
||||
});
|
||||
|
||||
export const url = pgTable(
|
||||
'Url',
|
||||
{
|
||||
id: text().primaryKey().notNull(),
|
||||
createdAt: timestamp({ precision: 3, mode: 'string' })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull(),
|
||||
updatedAt: timestamp({ precision: 3, mode: 'string' }).notNull(),
|
||||
code: text().notNull(),
|
||||
vanity: text(),
|
||||
destination: text().notNull(),
|
||||
views: integer().default(0).notNull(),
|
||||
maxViews: integer(),
|
||||
password: text(),
|
||||
userId: text(),
|
||||
enabled: boolean().default(true).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex('Url_code_vanity_key').using(
|
||||
'btree',
|
||||
table.code.asc().nullsLast().op('text_ops'),
|
||||
table.vanity.asc().nullsLast().op('text_ops'),
|
||||
),
|
||||
foreignKey({
|
||||
columns: [table.userId],
|
||||
foreignColumns: [user.id],
|
||||
name: 'Url_userId_fkey',
|
||||
})
|
||||
.onUpdate('cascade')
|
||||
.onDelete('set null'),
|
||||
],
|
||||
);
|
||||
|
||||
export const folder = pgTable(
|
||||
'Folder',
|
||||
{
|
||||
id: text().primaryKey().notNull(),
|
||||
createdAt: timestamp({ precision: 3, mode: 'string' })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull(),
|
||||
updatedAt: timestamp({ precision: 3, mode: 'string' }).notNull(),
|
||||
name: text().notNull(),
|
||||
public: boolean().default(false).notNull(),
|
||||
userId: text().notNull(),
|
||||
allowUploads: boolean().default(false).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
foreignKey({
|
||||
columns: [table.userId],
|
||||
foreignColumns: [user.id],
|
||||
name: 'Folder_userId_fkey',
|
||||
})
|
||||
.onUpdate('cascade')
|
||||
.onDelete('cascade'),
|
||||
],
|
||||
);
|
||||
|
||||
export const user = pgTable(
|
||||
'User',
|
||||
{
|
||||
id: text().primaryKey().notNull(),
|
||||
createdAt: timestamp({ precision: 3, mode: 'string' })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull(),
|
||||
updatedAt: timestamp({ precision: 3, mode: 'string' }).notNull(),
|
||||
username: text().notNull(),
|
||||
password: text(),
|
||||
avatar: text(),
|
||||
token: text().notNull(),
|
||||
role: role().default('USER').notNull(),
|
||||
view: jsonb().default({}).notNull(),
|
||||
totpSecret: text(),
|
||||
sessions: text().array(),
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex('User_token_key').using('btree', table.token.asc().nullsLast().op('text_ops')),
|
||||
uniqueIndex('User_username_key').using('btree', table.username.asc().nullsLast().op('text_ops')),
|
||||
],
|
||||
);
|
||||
|
||||
export const exportTable = pgTable(
|
||||
'Export',
|
||||
{
|
||||
id: text().primaryKey().notNull(),
|
||||
createdAt: timestamp({ precision: 3, mode: 'string' })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull(),
|
||||
updatedAt: timestamp({ precision: 3, mode: 'string' }).notNull(),
|
||||
completed: boolean().default(false).notNull(),
|
||||
path: text().notNull(),
|
||||
files: integer().notNull(),
|
||||
size: text().notNull(),
|
||||
userId: text().notNull(),
|
||||
},
|
||||
(table) => [
|
||||
foreignKey({
|
||||
columns: [table.userId],
|
||||
foreignColumns: [user.id],
|
||||
name: 'Export_userId_fkey',
|
||||
})
|
||||
.onUpdate('cascade')
|
||||
.onDelete('cascade'),
|
||||
],
|
||||
);
|
||||
|
||||
export const userQuota = pgTable(
|
||||
'UserQuota',
|
||||
{
|
||||
id: text().primaryKey().notNull(),
|
||||
createdAt: timestamp({ precision: 3, mode: 'string' })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull(),
|
||||
updatedAt: timestamp({ precision: 3, mode: 'string' }).notNull(),
|
||||
filesQuota: userFilesQuota().notNull(),
|
||||
maxBytes: text(),
|
||||
maxFiles: integer(),
|
||||
maxUrls: integer(),
|
||||
userId: text(),
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex('UserQuota_userId_key').using('btree', table.userId.asc().nullsLast().op('text_ops')),
|
||||
foreignKey({
|
||||
columns: [table.userId],
|
||||
foreignColumns: [user.id],
|
||||
name: 'UserQuota_userId_fkey',
|
||||
})
|
||||
.onUpdate('cascade')
|
||||
.onDelete('cascade'),
|
||||
],
|
||||
);
|
||||
|
||||
export const userPasskey = pgTable(
|
||||
'UserPasskey',
|
||||
{
|
||||
id: text().primaryKey().notNull(),
|
||||
createdAt: timestamp({ precision: 3, mode: 'string' })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull(),
|
||||
updatedAt: timestamp({ precision: 3, mode: 'string' }).notNull(),
|
||||
lastUsed: timestamp({ precision: 3, mode: 'string' }),
|
||||
name: text().notNull(),
|
||||
reg: jsonb().notNull(),
|
||||
userId: text().notNull(),
|
||||
},
|
||||
(table) => [
|
||||
foreignKey({
|
||||
columns: [table.userId],
|
||||
foreignColumns: [user.id],
|
||||
name: 'UserPasskey_userId_fkey',
|
||||
})
|
||||
.onUpdate('cascade')
|
||||
.onDelete('cascade'),
|
||||
],
|
||||
);
|
||||
|
||||
export const oauthProvider = pgTable(
|
||||
'OAuthProvider',
|
||||
{
|
||||
id: text().primaryKey().notNull(),
|
||||
createdAt: timestamp({ precision: 3, mode: 'string' })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull(),
|
||||
updatedAt: timestamp({ precision: 3, mode: 'string' }).notNull(),
|
||||
userId: text().notNull(),
|
||||
provider: oauthProviderType().notNull(),
|
||||
username: text().notNull(),
|
||||
accessToken: text().notNull(),
|
||||
refreshToken: text(),
|
||||
oauthId: text(),
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex('OAuthProvider_provider_oauthId_key').using(
|
||||
'btree',
|
||||
table.provider.asc().nullsLast().op('text_ops'),
|
||||
table.oauthId.asc().nullsLast().op('text_ops'),
|
||||
),
|
||||
foreignKey({
|
||||
columns: [table.userId],
|
||||
foreignColumns: [user.id],
|
||||
name: 'OAuthProvider_userId_fkey',
|
||||
})
|
||||
.onUpdate('cascade')
|
||||
.onDelete('restrict'),
|
||||
],
|
||||
);
|
||||
|
||||
export const file = pgTable(
|
||||
'File',
|
||||
{
|
||||
id: text().primaryKey().notNull(),
|
||||
createdAt: timestamp({ precision: 3, mode: 'string' })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull(),
|
||||
updatedAt: timestamp({ precision: 3, mode: 'string' }).notNull(),
|
||||
deletesAt: timestamp({ precision: 3, mode: 'string' }),
|
||||
name: text().notNull(),
|
||||
originalName: text(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
size: bigint({ mode: 'number' }).notNull(),
|
||||
type: text().notNull(),
|
||||
views: integer().default(0).notNull(),
|
||||
maxViews: integer(),
|
||||
favorite: boolean().default(false).notNull(),
|
||||
password: text(),
|
||||
userId: text(),
|
||||
folderId: text(),
|
||||
},
|
||||
(table) => [
|
||||
foreignKey({
|
||||
columns: [table.userId],
|
||||
foreignColumns: [user.id],
|
||||
name: 'File_userId_fkey',
|
||||
})
|
||||
.onUpdate('cascade')
|
||||
.onDelete('set null'),
|
||||
foreignKey({
|
||||
columns: [table.folderId],
|
||||
foreignColumns: [folder.id],
|
||||
name: 'File_folderId_fkey',
|
||||
})
|
||||
.onUpdate('cascade')
|
||||
.onDelete('set null'),
|
||||
],
|
||||
);
|
||||
|
||||
export const thumbnail = pgTable(
|
||||
'Thumbnail',
|
||||
{
|
||||
id: text().primaryKey().notNull(),
|
||||
createdAt: timestamp({ precision: 3, mode: 'string' })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull(),
|
||||
updatedAt: timestamp({ precision: 3, mode: 'string' }).notNull(),
|
||||
path: text().notNull(),
|
||||
fileId: text().notNull(),
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex('Thumbnail_fileId_key').using('btree', table.fileId.asc().nullsLast().op('text_ops')),
|
||||
foreignKey({
|
||||
columns: [table.fileId],
|
||||
foreignColumns: [file.id],
|
||||
name: 'Thumbnail_fileId_fkey',
|
||||
})
|
||||
.onUpdate('cascade')
|
||||
.onDelete('cascade'),
|
||||
],
|
||||
);
|
||||
|
||||
export const incompleteFile = pgTable(
|
||||
'IncompleteFile',
|
||||
{
|
||||
id: text().primaryKey().notNull(),
|
||||
createdAt: timestamp({ precision: 3, mode: 'string' })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull(),
|
||||
updatedAt: timestamp({ precision: 3, mode: 'string' }).notNull(),
|
||||
status: incompleteFileStatus().notNull(),
|
||||
chunksTotal: integer().notNull(),
|
||||
chunksComplete: integer().notNull(),
|
||||
metadata: jsonb().notNull(),
|
||||
userId: text().notNull(),
|
||||
},
|
||||
(table) => [
|
||||
foreignKey({
|
||||
columns: [table.userId],
|
||||
foreignColumns: [user.id],
|
||||
name: 'IncompleteFile_userId_fkey',
|
||||
})
|
||||
.onUpdate('cascade')
|
||||
.onDelete('cascade'),
|
||||
],
|
||||
);
|
||||
|
||||
export const tag = pgTable(
|
||||
'Tag',
|
||||
{
|
||||
id: text().primaryKey().notNull(),
|
||||
createdAt: timestamp({ precision: 3, mode: 'string' })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull(),
|
||||
updatedAt: timestamp({ precision: 3, mode: 'string' }).notNull(),
|
||||
name: text().notNull(),
|
||||
color: text().notNull(),
|
||||
userId: text(),
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex('Tag_name_key').using('btree', table.name.asc().nullsLast().op('text_ops')),
|
||||
foreignKey({
|
||||
columns: [table.userId],
|
||||
foreignColumns: [user.id],
|
||||
name: 'Tag_userId_fkey',
|
||||
})
|
||||
.onUpdate('cascade')
|
||||
.onDelete('set null'),
|
||||
],
|
||||
);
|
||||
|
||||
export const invite = pgTable(
|
||||
'Invite',
|
||||
{
|
||||
id: text().primaryKey().notNull(),
|
||||
createdAt: timestamp({ precision: 3, mode: 'string' })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull(),
|
||||
updatedAt: timestamp({ precision: 3, mode: 'string' }).notNull(),
|
||||
expiresAt: timestamp({ precision: 3, mode: 'string' }),
|
||||
code: text().notNull(),
|
||||
uses: integer().default(0).notNull(),
|
||||
maxUses: integer(),
|
||||
inviterId: text().notNull(),
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex('Invite_code_key').using('btree', table.code.asc().nullsLast().op('text_ops')),
|
||||
foreignKey({
|
||||
columns: [table.inviterId],
|
||||
foreignColumns: [user.id],
|
||||
name: 'Invite_inviterId_fkey',
|
||||
})
|
||||
.onUpdate('cascade')
|
||||
.onDelete('cascade'),
|
||||
],
|
||||
);
|
||||
|
||||
export const fileToTag = pgTable(
|
||||
'_FileToTag',
|
||||
{
|
||||
a: text('A').notNull(),
|
||||
b: text('B').notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index().using('btree', table.b.asc().nullsLast().op('text_ops')),
|
||||
foreignKey({
|
||||
columns: [table.a],
|
||||
foreignColumns: [file.id],
|
||||
name: '_FileToTag_A_fkey',
|
||||
})
|
||||
.onUpdate('cascade')
|
||||
.onDelete('cascade'),
|
||||
foreignKey({
|
||||
columns: [table.b],
|
||||
foreignColumns: [tag.id],
|
||||
name: '_FileToTag_B_fkey',
|
||||
})
|
||||
.onUpdate('cascade')
|
||||
.onDelete('cascade'),
|
||||
primaryKey({ columns: [table.a, table.b], name: '_FileToTag_AB_pkey' }),
|
||||
],
|
||||
);
|
||||
87
src/lib/db/migration/drizzle.ts
Normal file
87
src/lib/db/migration/drizzle.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { migrate } from 'drizzle-orm/node-postgres/migrator';
|
||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||
import pg from 'pg';
|
||||
import { join } from 'path';
|
||||
import { log } from '@/lib/logger';
|
||||
|
||||
const logger = log('db').c('drizzle');
|
||||
|
||||
async function drizzleBootstrap(client: pg.Client) {
|
||||
await client.query('CREATE SCHEMA IF NOT EXISTS "drizzle"');
|
||||
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS "drizzle"."__drizzle_migrations" (
|
||||
id SERIAL PRIMARY KEY,
|
||||
hash text NOT NULL,
|
||||
created_at numeric
|
||||
)
|
||||
`);
|
||||
}
|
||||
|
||||
async function migrateExistingPrisma(client: pg.Client) {
|
||||
// check if there is a _prisma_migrations table
|
||||
// if there is then we continue with prisma -> drizzle.
|
||||
|
||||
const resPrisma = await client.query(`
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = '_prisma_migrations'
|
||||
)
|
||||
`);
|
||||
|
||||
const existsPrisma = resPrisma.rows[0]?.exists;
|
||||
if (!existsPrisma) {
|
||||
logger.debug('no existing prisma migrations found, skipping prisma -> drizzle migration step');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug('existing prisma migrations found, migrating to drizzle');
|
||||
|
||||
// at this point, there should already be a __drizzle_migrations table
|
||||
// now looking for the first migration so we can manually insert it if needed.
|
||||
|
||||
const firstMigration = 1756926875085;
|
||||
|
||||
const res = await client.query(
|
||||
`
|
||||
SELECT COUNT(*) FROM drizzle.__drizzle_migrations WHERE created_at = $1
|
||||
`,
|
||||
[firstMigration],
|
||||
);
|
||||
|
||||
const count = parseInt(res.rows[0]?.count || '0', 10);
|
||||
|
||||
logger.debug('finding existing first migrations', { count });
|
||||
|
||||
if (count === 0) {
|
||||
logger.debug('inserting first migration manually');
|
||||
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO drizzle.__drizzle_migrations (created_at, hash)
|
||||
VALUES ($1, $2)
|
||||
`,
|
||||
[firstMigration, 'manual'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function runDrizzleMigrations() {
|
||||
const client = new pg.Client({ connectionString: process.env.DATABASE_URL });
|
||||
await client.connect();
|
||||
const db = drizzle(client);
|
||||
|
||||
// ensure drizzle migrations table exists
|
||||
await drizzleBootstrap(client);
|
||||
|
||||
// migrate from prisma to drizzle
|
||||
await migrateExistingPrisma(client);
|
||||
|
||||
// now we can run migrations with drizzle
|
||||
await migrate(db, {
|
||||
migrationsFolder: join(process.cwd(), 'src', 'drizzle'),
|
||||
});
|
||||
|
||||
logger.info('migrations complete');
|
||||
}
|
||||
Reference in New Issue
Block a user