diff --git a/.env.example b/.env.example
index 28fa9d4de..d643c1194 100644
--- a/.env.example
+++ b/.env.example
@@ -34,6 +34,10 @@ DB_URL='FULL_PATH_TO_YOUR_SQLITE_DB_FILE'
# DB_PASSWORD='password'
# DB_NAME='name-of-database'
+# The following is an example on how to use the node-postgres driver:
+# DB_DRIVER='node-postgres'
+# DB_URL='postgres://user:password@host:port/database'
+
# The below path can be used to store trusted certificates, it is not required and can be left empty.
# If it is empty, it will default to `/appdata/trusted-certificates` in production.
# If it is used, please use the full path to the directory where the certificates are stored.
diff --git a/.run/db_migration_postgresql_generate.run copy.xml b/.run/db_migration_postgresql_generate.run copy.xml
new file mode 100644
index 000000000..4bc02bdba
--- /dev/null
+++ b/.run/db_migration_postgresql_generate.run copy.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/development/development.docker-compose.yml b/development/development.docker-compose.yml
index 0ee173260..f19ed787d 100644
--- a/development/development.docker-compose.yml
+++ b/development/development.docker-compose.yml
@@ -26,5 +26,29 @@ services:
volumes:
- mysql_data:/var/lib/mysql
+ postgresql:
+ image: postgres
+ restart: always
+ # set shared memory limit when using docker compose
+ shm_size: 128mb
+ # or set shared memory limit when deploy via swarm stack
+ #volumes:
+ # - type: tmpfs
+ # target: /dev/shm
+ # tmpfs:
+ # size: 134217728 # 128*2^20 bytes = 128Mb
+ environment:
+ POSTGRES_PASSWORD: homarr
+ POSTGRES_USER: homarr
+ POSTGRES_DB: homarrdb
+ PGDATA: /var/lib/postgresql/data/pgdata
+ volumes:
+ - postgresql_data:/var/lib/postgresql/data
+ ports:
+ - 5432:5432
+ # if already run PostgreSQL, change port number to use container's service
+ # - 2345:5432
+
volumes:
- mysql_data:
\ No newline at end of file
+ mysql_data:
+ postgresql_data:
\ No newline at end of file
diff --git a/package.json b/package.json
index ec9f3e9f4..73be82e66 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,8 @@
"cli": "pnpm with-env tsx packages/cli/index.ts",
"db:migration:mysql:generate": "pnpm -F db migration:mysql:generate",
"db:migration:mysql:run": "pnpm -F db migration:mysql:run",
+ "db:migration:postgresql:generate": "pnpm -F db migration:postgresql:generate",
+ "db:migration:postgresql:run": "pnpm -F db migration:postgresql:run",
"db:migration:sqlite:generate": "pnpm -F db migration:sqlite:generate",
"db:migration:sqlite:run": "pnpm -F db migration:sqlite:run",
"db:push": "pnpm -F db push:sqlite",
diff --git a/packages/db/collection.ts b/packages/db/collection.ts
index 1bcbb9316..6a44b44c2 100644
--- a/packages/db/collection.ts
+++ b/packages/db/collection.ts
@@ -2,7 +2,7 @@ import type { InferInsertModel } from "drizzle-orm";
import { objectEntries } from "@homarr/common";
-import type { HomarrDatabase, HomarrDatabaseMysql } from "./driver";
+import type { HomarrDatabase, HomarrDatabaseMysql, HomarrDatabasePostgresql } from "./driver";
import { env } from "./env";
import * as schema from "./schema";
@@ -10,6 +10,14 @@ type TableKey = {
[K in keyof typeof schema]: (typeof schema)[K] extends { _: { brand: "Table" } } ? K : never;
}[keyof typeof schema];
+export function isMysql(): boolean {
+ return env.DB_DRIVER === "mysql2";
+}
+
+export function isPostgresql(): boolean {
+ return env.DB_DRIVER === "node-postgres";
+}
+
export const createDbInsertCollectionForTransaction = (
tablesInInsertOrder: TTableKey[],
) => {
@@ -35,8 +43,10 @@ export const createDbInsertCollectionForTransaction = {
- await db.transaction(async (transaction) => {
+ // We allow any database that supports async passed here but then fallback to mysql to prevent typescript errors
+ insertAllAsync: async (db: HomarrDatabaseMysql | HomarrDatabasePostgresql) => {
+ const innerDb = db as HomarrDatabaseMysql;
+ await innerDb.transaction(async (transaction) => {
for (const [key, values] of objectEntries(context)) {
if (values.length >= 1) {
// Below is actually the mysqlSchema when the driver is mysql
@@ -56,12 +66,18 @@ export const createDbInsertCollectionWithoutTransaction = {
- if (env.DB_DRIVER !== "mysql2") {
- insertAll(db);
- return;
+ switch (env.DB_DRIVER) {
+ case "mysql2":
+ case "node-postgres":
+ // For mysql2 and node-postgres, we can use the async insertAllAsync method
+ await insertAllAsync(db as unknown as HomarrDatabaseMysql | HomarrDatabasePostgresql);
+ return;
+ default:
+ // For better-sqlite3, we need to use the synchronous insertAll method
+ // default assumes better-sqlite3. It's original implementation.
+ insertAll(db);
+ break;
}
-
- await insertAllAsync(db as unknown as HomarrDatabaseMysql);
},
};
};
diff --git a/packages/db/configs/postgresql.config.ts b/packages/db/configs/postgresql.config.ts
new file mode 100644
index 000000000..a37115dfa
--- /dev/null
+++ b/packages/db/configs/postgresql.config.ts
@@ -0,0 +1,20 @@
+import type { Config } from "drizzle-kit";
+
+import { env } from "../env";
+
+export default {
+ dialect: "postgresql",
+ schema: "./schema",
+ casing: "snake_case",
+
+ dbCredentials: env.DB_URL
+ ? { url: env.DB_URL }
+ : {
+ host: env.DB_HOST,
+ port: env.DB_PORT,
+ user: env.DB_USER,
+ password: env.DB_PASSWORD,
+ database: env.DB_NAME,
+ },
+ out: "./migrations/postgresql",
+} satisfies Config;
diff --git a/packages/db/driver.ts b/packages/db/driver.ts
index c5159fd9d..9ed5c899f 100644
--- a/packages/db/driver.ts
+++ b/packages/db/driver.ts
@@ -5,17 +5,22 @@ import type { BetterSQLite3Database } from "drizzle-orm/better-sqlite3";
import { drizzle as drizzleSqlite } from "drizzle-orm/better-sqlite3";
import type { MySql2Database } from "drizzle-orm/mysql2";
import { drizzle as drizzleMysql } from "drizzle-orm/mysql2";
+import type { NodePgDatabase } from "drizzle-orm/node-postgres";
+import { drizzle as drizzlePg } from "drizzle-orm/node-postgres";
import type { Pool as MysqlConnectionPool } from "mysql2";
import mysql from "mysql2";
+import { Pool as PostgresPool } from "pg";
import { logger } from "@homarr/log";
import { env } from "./env";
import * as mysqlSchema from "./schema/mysql";
+import * as pgSchema from "./schema/postgresql";
import * as sqliteSchema from "./schema/sqlite";
export type HomarrDatabase = BetterSQLite3Database;
export type HomarrDatabaseMysql = MySql2Database;
+export type HomarrDatabasePostgresql = NodePgDatabase;
const init = () => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
@@ -24,6 +29,9 @@ const init = () => {
case "mysql2":
initMySQL2();
break;
+ case "node-postgres":
+ initNodePostgres();
+ break;
default:
initBetterSqlite();
break;
@@ -31,7 +39,7 @@ const init = () => {
}
};
-export let connection: BetterSqlite3Connection | MysqlConnectionPool;
+export let connection: BetterSqlite3Connection | MysqlConnectionPool | PostgresPool;
export let database: HomarrDatabase;
class WinstonDrizzleLogger implements Logger {
@@ -73,4 +81,33 @@ const initMySQL2 = () => {
}) as unknown as HomarrDatabase;
};
+const initNodePostgres = () => {
+ if (!env.DB_HOST) {
+ connection = new PostgresPool({
+ connectionString: env.DB_URL,
+ max: 0,
+ idleTimeoutMillis: 60000,
+ allowExitOnIdle: false,
+ });
+ } else {
+ connection = new PostgresPool({
+ host: env.DB_HOST,
+ database: env.DB_NAME,
+ port: env.DB_PORT,
+ user: env.DB_USER,
+ password: env.DB_PASSWORD,
+ max: 0,
+ idleTimeoutMillis: 60000,
+ allowExitOnIdle: false,
+ });
+ }
+
+ database = drizzlePg({
+ logger: new WinstonDrizzleLogger(),
+ schema: pgSchema,
+ casing: "snake_case",
+ client: connection,
+ }) as unknown as HomarrDatabase;
+};
+
init();
diff --git a/packages/db/env.ts b/packages/db/env.ts
index f02c35b8e..9d8932a7d 100644
--- a/packages/db/env.ts
+++ b/packages/db/env.ts
@@ -6,6 +6,7 @@ import { createEnv } from "@homarr/core/infrastructure/env";
const drivers = {
betterSqlite3: "better-sqlite3",
mysql2: "mysql2",
+ nodePostgres: "node-postgres",
} as const;
const isDriver = (driver: (typeof drivers)[keyof typeof drivers]) => process.env.DB_DRIVER === driver;
@@ -21,7 +22,7 @@ export const env = createEnv({
*/
server: {
DB_DRIVER: z
- .union([z.literal(drivers.betterSqlite3), z.literal(drivers.mysql2)], {
+ .union([z.literal(drivers.betterSqlite3), z.literal(drivers.mysql2), z.literal(drivers.nodePostgres)], {
message: `Invalid database driver, supported are ${Object.keys(drivers).join(", ")}`,
})
.default(drivers.betterSqlite3),
@@ -42,7 +43,7 @@ export const env = createEnv({
.regex(/\d+/)
.transform(Number)
.refine((number) => number >= 1)
- .default(3306),
+ .default(isDriver(drivers.mysql2) ? 3306 : 5432),
DB_USER: z.string(),
DB_PASSWORD: z.string(),
DB_NAME: z.string(),
diff --git a/packages/db/index.ts b/packages/db/index.ts
index dd98d8a1b..b796d2fa4 100644
--- a/packages/db/index.ts
+++ b/packages/db/index.ts
@@ -7,6 +7,6 @@ export * from "drizzle-orm";
export const db = database;
export type Database = typeof db;
-export type { HomarrDatabaseMysql } from "./driver";
+export type { HomarrDatabaseMysql, HomarrDatabasePostgresql } from "./driver";
export { handleDiffrentDbDriverOperationsAsync as handleTransactionsAsync } from "./transactions";
diff --git a/packages/db/migrations/postgresql/0000_initial.sql b/packages/db/migrations/postgresql/0000_initial.sql
new file mode 100644
index 000000000..fd4d20cbe
--- /dev/null
+++ b/packages/db/migrations/postgresql/0000_initial.sql
@@ -0,0 +1,322 @@
+CREATE TABLE "account" (
+ "user_id" varchar(64) NOT NULL,
+ "type" text NOT NULL,
+ "provider" varchar(64) NOT NULL,
+ "provider_account_id" varchar(64) NOT NULL,
+ "refresh_token" text,
+ "access_token" text,
+ "expires_at" integer,
+ "token_type" text,
+ "scope" text,
+ "id_token" text,
+ "session_state" text,
+ CONSTRAINT "account_provider_provider_account_id_pk" PRIMARY KEY("provider","provider_account_id")
+);
+--> statement-breakpoint
+CREATE TABLE "apiKey" (
+ "id" varchar(64) PRIMARY KEY NOT NULL,
+ "api_key" text NOT NULL,
+ "salt" text NOT NULL,
+ "user_id" varchar(64) NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE "app" (
+ "id" varchar(64) PRIMARY KEY NOT NULL,
+ "name" text NOT NULL,
+ "description" text,
+ "icon_url" text NOT NULL,
+ "href" text,
+ "ping_url" text
+);
+--> statement-breakpoint
+CREATE TABLE "boardGroupPermission" (
+ "board_id" varchar(64) NOT NULL,
+ "group_id" varchar(64) NOT NULL,
+ "permission" varchar(128) NOT NULL,
+ CONSTRAINT "boardGroupPermission_board_id_group_id_permission_pk" PRIMARY KEY("board_id","group_id","permission")
+);
+--> statement-breakpoint
+CREATE TABLE "boardUserPermission" (
+ "board_id" varchar(64) NOT NULL,
+ "user_id" varchar(64) NOT NULL,
+ "permission" varchar(128) NOT NULL,
+ CONSTRAINT "boardUserPermission_board_id_user_id_permission_pk" PRIMARY KEY("board_id","user_id","permission")
+);
+--> statement-breakpoint
+CREATE TABLE "board" (
+ "id" varchar(64) PRIMARY KEY NOT NULL,
+ "name" varchar(256) NOT NULL,
+ "is_public" boolean DEFAULT false NOT NULL,
+ "creator_id" varchar(64),
+ "page_title" text,
+ "meta_title" text,
+ "logo_image_url" text,
+ "favicon_image_url" text,
+ "background_image_url" text,
+ "background_image_attachment" text DEFAULT 'fixed' NOT NULL,
+ "background_image_repeat" text DEFAULT 'no-repeat' NOT NULL,
+ "background_image_size" text DEFAULT 'cover' NOT NULL,
+ "primary_color" text DEFAULT '#fa5252' NOT NULL,
+ "secondary_color" text DEFAULT '#fd7e14' NOT NULL,
+ "opacity" integer DEFAULT 100 NOT NULL,
+ "custom_css" text,
+ "icon_color" text,
+ "item_radius" text DEFAULT 'lg' NOT NULL,
+ "disable_status" boolean DEFAULT false NOT NULL,
+ CONSTRAINT "board_name_unique" UNIQUE("name")
+);
+--> statement-breakpoint
+CREATE TABLE "cron_job_configuration" (
+ "name" varchar(256) PRIMARY KEY NOT NULL,
+ "cron_expression" varchar(32) NOT NULL,
+ "is_enabled" boolean DEFAULT true NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE "groupMember" (
+ "group_id" varchar(64) NOT NULL,
+ "user_id" varchar(64) NOT NULL,
+ CONSTRAINT "groupMember_group_id_user_id_pk" PRIMARY KEY("group_id","user_id")
+);
+--> statement-breakpoint
+CREATE TABLE "groupPermission" (
+ "group_id" varchar(64) NOT NULL,
+ "permission" text NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE "group" (
+ "id" varchar(64) PRIMARY KEY NOT NULL,
+ "name" varchar(64) NOT NULL,
+ "owner_id" varchar(64),
+ "home_board_id" varchar(64),
+ "mobile_home_board_id" varchar(64),
+ "position" smallint NOT NULL,
+ CONSTRAINT "group_name_unique" UNIQUE("name")
+);
+--> statement-breakpoint
+CREATE TABLE "iconRepository" (
+ "id" varchar(64) PRIMARY KEY NOT NULL,
+ "slug" varchar(150) NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE "icon" (
+ "id" varchar(64) PRIMARY KEY NOT NULL,
+ "name" varchar(250) NOT NULL,
+ "url" text NOT NULL,
+ "checksum" text NOT NULL,
+ "icon_repository_id" varchar(64) NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE "integrationGroupPermissions" (
+ "integration_id" varchar(64) NOT NULL,
+ "group_id" varchar(64) NOT NULL,
+ "permission" varchar(128) NOT NULL,
+ CONSTRAINT "integration_group_permission__pk" PRIMARY KEY("integration_id","group_id","permission")
+);
+--> statement-breakpoint
+CREATE TABLE "integration_item" (
+ "item_id" varchar(64) NOT NULL,
+ "integration_id" varchar(64) NOT NULL,
+ CONSTRAINT "integration_item_item_id_integration_id_pk" PRIMARY KEY("item_id","integration_id")
+);
+--> statement-breakpoint
+CREATE TABLE "integrationSecret" (
+ "kind" varchar(16) NOT NULL,
+ "value" text NOT NULL,
+ "updated_at" timestamp NOT NULL,
+ "integration_id" varchar(64) NOT NULL,
+ CONSTRAINT "integrationSecret_integration_id_kind_pk" PRIMARY KEY("integration_id","kind")
+);
+--> statement-breakpoint
+CREATE TABLE "integrationUserPermission" (
+ "integration_id" varchar(64) NOT NULL,
+ "user_id" varchar(64) NOT NULL,
+ "permission" varchar(128) NOT NULL,
+ CONSTRAINT "integrationUserPermission_integration_id_user_id_permission_pk" PRIMARY KEY("integration_id","user_id","permission")
+);
+--> statement-breakpoint
+CREATE TABLE "integration" (
+ "id" varchar(64) PRIMARY KEY NOT NULL,
+ "name" text NOT NULL,
+ "url" text NOT NULL,
+ "kind" varchar(128) NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE "invite" (
+ "id" varchar(64) PRIMARY KEY NOT NULL,
+ "token" varchar(512) NOT NULL,
+ "expiration_date" timestamp NOT NULL,
+ "creator_id" varchar(64) NOT NULL,
+ CONSTRAINT "invite_token_unique" UNIQUE("token")
+);
+--> statement-breakpoint
+CREATE TABLE "item_layout" (
+ "item_id" varchar(64) NOT NULL,
+ "section_id" varchar(64) NOT NULL,
+ "layout_id" varchar(64) NOT NULL,
+ "x_offset" integer NOT NULL,
+ "y_offset" integer NOT NULL,
+ "width" integer NOT NULL,
+ "height" integer NOT NULL,
+ CONSTRAINT "item_layout_item_id_section_id_layout_id_pk" PRIMARY KEY("item_id","section_id","layout_id")
+);
+--> statement-breakpoint
+CREATE TABLE "item" (
+ "id" varchar(64) PRIMARY KEY NOT NULL,
+ "board_id" varchar(64) NOT NULL,
+ "kind" text NOT NULL,
+ "options" text DEFAULT '{"json": {}}' NOT NULL,
+ "advanced_options" text DEFAULT '{"json": {}}' NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE "layout" (
+ "id" varchar(64) PRIMARY KEY NOT NULL,
+ "name" varchar(32) NOT NULL,
+ "board_id" varchar(64) NOT NULL,
+ "column_count" smallint NOT NULL,
+ "breakpoint" smallint DEFAULT 0 NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE "media" (
+ "id" varchar(64) PRIMARY KEY NOT NULL,
+ "name" varchar(512) NOT NULL,
+ "content" "bytea" NOT NULL,
+ "content_type" text NOT NULL,
+ "size" integer NOT NULL,
+ "created_at" timestamp DEFAULT now() NOT NULL,
+ "creator_id" varchar(64)
+);
+--> statement-breakpoint
+CREATE TABLE "onboarding" (
+ "id" varchar(64) PRIMARY KEY NOT NULL,
+ "step" varchar(64) NOT NULL,
+ "previous_step" varchar(64)
+);
+--> statement-breakpoint
+CREATE TABLE "search_engine" (
+ "id" varchar(64) PRIMARY KEY NOT NULL,
+ "icon_url" text NOT NULL,
+ "name" varchar(64) NOT NULL,
+ "short" varchar(8) NOT NULL,
+ "description" text,
+ "url_template" text,
+ "type" varchar(64) DEFAULT 'generic' NOT NULL,
+ "integration_id" varchar(64),
+ CONSTRAINT "search_engine_short_unique" UNIQUE("short")
+);
+--> statement-breakpoint
+CREATE TABLE "section_collapse_state" (
+ "user_id" varchar(64) NOT NULL,
+ "section_id" varchar(64) NOT NULL,
+ "collapsed" boolean DEFAULT false NOT NULL,
+ CONSTRAINT "section_collapse_state_user_id_section_id_pk" PRIMARY KEY("user_id","section_id")
+);
+--> statement-breakpoint
+CREATE TABLE "section_layout" (
+ "section_id" varchar(64) NOT NULL,
+ "layout_id" varchar(64) NOT NULL,
+ "parent_section_id" varchar(64),
+ "x_offset" integer NOT NULL,
+ "y_offset" integer NOT NULL,
+ "width" integer NOT NULL,
+ "height" integer NOT NULL,
+ CONSTRAINT "section_layout_section_id_layout_id_pk" PRIMARY KEY("section_id","layout_id")
+);
+--> statement-breakpoint
+CREATE TABLE "section" (
+ "id" varchar(64) PRIMARY KEY NOT NULL,
+ "board_id" varchar(64) NOT NULL,
+ "kind" text NOT NULL,
+ "x_offset" integer,
+ "y_offset" integer,
+ "name" text,
+ "options" text DEFAULT '{"json": {}}'
+);
+--> statement-breakpoint
+CREATE TABLE "serverSetting" (
+ "setting_key" varchar(64) PRIMARY KEY NOT NULL,
+ "value" text DEFAULT '{"json": {}}' NOT NULL,
+ CONSTRAINT "serverSetting_settingKey_unique" UNIQUE("setting_key")
+);
+--> statement-breakpoint
+CREATE TABLE "session" (
+ "session_token" varchar(512) PRIMARY KEY NOT NULL,
+ "user_id" varchar(64) NOT NULL,
+ "expires" timestamp NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE "trusted_certificate_hostname" (
+ "hostname" varchar(256) NOT NULL,
+ "thumbprint" varchar(128) NOT NULL,
+ "certificate" text NOT NULL,
+ CONSTRAINT "trusted_certificate_hostname_hostname_thumbprint_pk" PRIMARY KEY("hostname","thumbprint")
+);
+--> statement-breakpoint
+CREATE TABLE "user" (
+ "id" varchar(64) PRIMARY KEY NOT NULL,
+ "name" text,
+ "email" text,
+ "email_verified" timestamp,
+ "image" text,
+ "password" text,
+ "salt" text,
+ "provider" varchar(64) DEFAULT 'credentials' NOT NULL,
+ "home_board_id" varchar(64),
+ "mobile_home_board_id" varchar(64),
+ "default_search_engine_id" varchar(64),
+ "open_search_in_new_tab" boolean DEFAULT false NOT NULL,
+ "color_scheme" varchar(5) DEFAULT 'dark' NOT NULL,
+ "first_day_of_week" smallint DEFAULT 1 NOT NULL,
+ "ping_icons_enabled" boolean DEFAULT false NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE "verificationToken" (
+ "identifier" varchar(64) NOT NULL,
+ "token" varchar(512) NOT NULL,
+ "expires" timestamp NOT NULL,
+ CONSTRAINT "verificationToken_identifier_token_pk" PRIMARY KEY("identifier","token")
+);
+--> statement-breakpoint
+ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "apiKey" ADD CONSTRAINT "apiKey_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "boardGroupPermission" ADD CONSTRAINT "boardGroupPermission_board_id_board_id_fk" FOREIGN KEY ("board_id") REFERENCES "public"."board"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "boardGroupPermission" ADD CONSTRAINT "boardGroupPermission_group_id_group_id_fk" FOREIGN KEY ("group_id") REFERENCES "public"."group"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "boardUserPermission" ADD CONSTRAINT "boardUserPermission_board_id_board_id_fk" FOREIGN KEY ("board_id") REFERENCES "public"."board"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "boardUserPermission" ADD CONSTRAINT "boardUserPermission_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "board" ADD CONSTRAINT "board_creator_id_user_id_fk" FOREIGN KEY ("creator_id") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "groupMember" ADD CONSTRAINT "groupMember_group_id_group_id_fk" FOREIGN KEY ("group_id") REFERENCES "public"."group"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "groupMember" ADD CONSTRAINT "groupMember_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "groupPermission" ADD CONSTRAINT "groupPermission_group_id_group_id_fk" FOREIGN KEY ("group_id") REFERENCES "public"."group"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "group" ADD CONSTRAINT "group_owner_id_user_id_fk" FOREIGN KEY ("owner_id") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "group" ADD CONSTRAINT "group_home_board_id_board_id_fk" FOREIGN KEY ("home_board_id") REFERENCES "public"."board"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "group" ADD CONSTRAINT "group_mobile_home_board_id_board_id_fk" FOREIGN KEY ("mobile_home_board_id") REFERENCES "public"."board"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "icon" ADD CONSTRAINT "icon_icon_repository_id_iconRepository_id_fk" FOREIGN KEY ("icon_repository_id") REFERENCES "public"."iconRepository"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "integrationGroupPermissions" ADD CONSTRAINT "integrationGroupPermissions_integration_id_integration_id_fk" FOREIGN KEY ("integration_id") REFERENCES "public"."integration"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "integrationGroupPermissions" ADD CONSTRAINT "integrationGroupPermissions_group_id_group_id_fk" FOREIGN KEY ("group_id") REFERENCES "public"."group"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "integration_item" ADD CONSTRAINT "integration_item_item_id_item_id_fk" FOREIGN KEY ("item_id") REFERENCES "public"."item"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "integration_item" ADD CONSTRAINT "integration_item_integration_id_integration_id_fk" FOREIGN KEY ("integration_id") REFERENCES "public"."integration"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "integrationSecret" ADD CONSTRAINT "integrationSecret_integration_id_integration_id_fk" FOREIGN KEY ("integration_id") REFERENCES "public"."integration"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "integrationUserPermission" ADD CONSTRAINT "integrationUserPermission_integration_id_integration_id_fk" FOREIGN KEY ("integration_id") REFERENCES "public"."integration"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "integrationUserPermission" ADD CONSTRAINT "integrationUserPermission_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "invite" ADD CONSTRAINT "invite_creator_id_user_id_fk" FOREIGN KEY ("creator_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "item_layout" ADD CONSTRAINT "item_layout_item_id_item_id_fk" FOREIGN KEY ("item_id") REFERENCES "public"."item"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "item_layout" ADD CONSTRAINT "item_layout_section_id_section_id_fk" FOREIGN KEY ("section_id") REFERENCES "public"."section"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "item_layout" ADD CONSTRAINT "item_layout_layout_id_layout_id_fk" FOREIGN KEY ("layout_id") REFERENCES "public"."layout"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "item" ADD CONSTRAINT "item_board_id_board_id_fk" FOREIGN KEY ("board_id") REFERENCES "public"."board"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "layout" ADD CONSTRAINT "layout_board_id_board_id_fk" FOREIGN KEY ("board_id") REFERENCES "public"."board"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "media" ADD CONSTRAINT "media_creator_id_user_id_fk" FOREIGN KEY ("creator_id") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "search_engine" ADD CONSTRAINT "search_engine_integration_id_integration_id_fk" FOREIGN KEY ("integration_id") REFERENCES "public"."integration"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "section_collapse_state" ADD CONSTRAINT "section_collapse_state_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "section_collapse_state" ADD CONSTRAINT "section_collapse_state_section_id_section_id_fk" FOREIGN KEY ("section_id") REFERENCES "public"."section"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "section_layout" ADD CONSTRAINT "section_layout_section_id_section_id_fk" FOREIGN KEY ("section_id") REFERENCES "public"."section"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "section_layout" ADD CONSTRAINT "section_layout_layout_id_layout_id_fk" FOREIGN KEY ("layout_id") REFERENCES "public"."layout"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "section_layout" ADD CONSTRAINT "section_layout_parent_section_id_section_id_fk" FOREIGN KEY ("parent_section_id") REFERENCES "public"."section"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "section" ADD CONSTRAINT "section_board_id_board_id_fk" FOREIGN KEY ("board_id") REFERENCES "public"."board"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "user" ADD CONSTRAINT "user_home_board_id_board_id_fk" FOREIGN KEY ("home_board_id") REFERENCES "public"."board"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "user" ADD CONSTRAINT "user_mobile_home_board_id_board_id_fk" FOREIGN KEY ("mobile_home_board_id") REFERENCES "public"."board"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "user" ADD CONSTRAINT "user_default_search_engine_id_search_engine_id_fk" FOREIGN KEY ("default_search_engine_id") REFERENCES "public"."search_engine"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
+CREATE INDEX "userId_idx" ON "account" USING btree ("user_id");--> statement-breakpoint
+CREATE INDEX "integration_secret__kind_idx" ON "integrationSecret" USING btree ("kind");--> statement-breakpoint
+CREATE INDEX "integration_secret__updated_at_idx" ON "integrationSecret" USING btree ("updated_at");--> statement-breakpoint
+CREATE INDEX "integration__kind_idx" ON "integration" USING btree ("kind");--> statement-breakpoint
+CREATE INDEX "user_id_idx" ON "session" USING btree ("user_id");
\ No newline at end of file
diff --git a/packages/db/migrations/postgresql/meta/0000_snapshot.json b/packages/db/migrations/postgresql/meta/0000_snapshot.json
new file mode 100644
index 000000000..1e2688b84
--- /dev/null
+++ b/packages/db/migrations/postgresql/meta/0000_snapshot.json
@@ -0,0 +1,1991 @@
+{
+ "id": "7ee22b38-c9db-4844-a3ff-bf8e1f4c71e8",
+ "prevId": "00000000-0000-0000-0000-000000000000",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.account": {
+ "name": "account",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider": {
+ "name": "provider",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_account_id": {
+ "name": "provider_account_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "token_type": {
+ "name": "token_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "id_token": {
+ "name": "id_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "session_state": {
+ "name": "session_state",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "userId_idx": {
+ "name": "userId_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "account_user_id_user_id_fk": {
+ "name": "account_user_id_user_id_fk",
+ "tableFrom": "account",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "account_provider_provider_account_id_pk": {
+ "name": "account_provider_provider_account_id_pk",
+ "columns": ["provider", "provider_account_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.apiKey": {
+ "name": "apiKey",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "api_key": {
+ "name": "api_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "salt": {
+ "name": "salt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "apiKey_user_id_user_id_fk": {
+ "name": "apiKey_user_id_user_id_fk",
+ "tableFrom": "apiKey",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.app": {
+ "name": "app",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "icon_url": {
+ "name": "icon_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "href": {
+ "name": "href",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ping_url": {
+ "name": "ping_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.boardGroupPermission": {
+ "name": "boardGroupPermission",
+ "schema": "",
+ "columns": {
+ "board_id": {
+ "name": "board_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "group_id": {
+ "name": "group_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "permission": {
+ "name": "permission",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "boardGroupPermission_board_id_board_id_fk": {
+ "name": "boardGroupPermission_board_id_board_id_fk",
+ "tableFrom": "boardGroupPermission",
+ "tableTo": "board",
+ "columnsFrom": ["board_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "boardGroupPermission_group_id_group_id_fk": {
+ "name": "boardGroupPermission_group_id_group_id_fk",
+ "tableFrom": "boardGroupPermission",
+ "tableTo": "group",
+ "columnsFrom": ["group_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "boardGroupPermission_board_id_group_id_permission_pk": {
+ "name": "boardGroupPermission_board_id_group_id_permission_pk",
+ "columns": ["board_id", "group_id", "permission"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.boardUserPermission": {
+ "name": "boardUserPermission",
+ "schema": "",
+ "columns": {
+ "board_id": {
+ "name": "board_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "permission": {
+ "name": "permission",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "boardUserPermission_board_id_board_id_fk": {
+ "name": "boardUserPermission_board_id_board_id_fk",
+ "tableFrom": "boardUserPermission",
+ "tableTo": "board",
+ "columnsFrom": ["board_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "boardUserPermission_user_id_user_id_fk": {
+ "name": "boardUserPermission_user_id_user_id_fk",
+ "tableFrom": "boardUserPermission",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "boardUserPermission_board_id_user_id_permission_pk": {
+ "name": "boardUserPermission_board_id_user_id_permission_pk",
+ "columns": ["board_id", "user_id", "permission"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.board": {
+ "name": "board",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(256)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_public": {
+ "name": "is_public",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "creator_id": {
+ "name": "creator_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "page_title": {
+ "name": "page_title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "meta_title": {
+ "name": "meta_title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "logo_image_url": {
+ "name": "logo_image_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "favicon_image_url": {
+ "name": "favicon_image_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "background_image_url": {
+ "name": "background_image_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "background_image_attachment": {
+ "name": "background_image_attachment",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'fixed'"
+ },
+ "background_image_repeat": {
+ "name": "background_image_repeat",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'no-repeat'"
+ },
+ "background_image_size": {
+ "name": "background_image_size",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'cover'"
+ },
+ "primary_color": {
+ "name": "primary_color",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'#fa5252'"
+ },
+ "secondary_color": {
+ "name": "secondary_color",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'#fd7e14'"
+ },
+ "opacity": {
+ "name": "opacity",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 100
+ },
+ "custom_css": {
+ "name": "custom_css",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "icon_color": {
+ "name": "icon_color",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "item_radius": {
+ "name": "item_radius",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'lg'"
+ },
+ "disable_status": {
+ "name": "disable_status",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "board_creator_id_user_id_fk": {
+ "name": "board_creator_id_user_id_fk",
+ "tableFrom": "board",
+ "tableTo": "user",
+ "columnsFrom": ["creator_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "board_name_unique": {
+ "name": "board_name_unique",
+ "nullsNotDistinct": false,
+ "columns": ["name"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.cron_job_configuration": {
+ "name": "cron_job_configuration",
+ "schema": "",
+ "columns": {
+ "name": {
+ "name": "name",
+ "type": "varchar(256)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "cron_expression": {
+ "name": "cron_expression",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_enabled": {
+ "name": "is_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.groupMember": {
+ "name": "groupMember",
+ "schema": "",
+ "columns": {
+ "group_id": {
+ "name": "group_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "groupMember_group_id_group_id_fk": {
+ "name": "groupMember_group_id_group_id_fk",
+ "tableFrom": "groupMember",
+ "tableTo": "group",
+ "columnsFrom": ["group_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "groupMember_user_id_user_id_fk": {
+ "name": "groupMember_user_id_user_id_fk",
+ "tableFrom": "groupMember",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "groupMember_group_id_user_id_pk": {
+ "name": "groupMember_group_id_user_id_pk",
+ "columns": ["group_id", "user_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.groupPermission": {
+ "name": "groupPermission",
+ "schema": "",
+ "columns": {
+ "group_id": {
+ "name": "group_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "permission": {
+ "name": "permission",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "groupPermission_group_id_group_id_fk": {
+ "name": "groupPermission_group_id_group_id_fk",
+ "tableFrom": "groupPermission",
+ "tableTo": "group",
+ "columnsFrom": ["group_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.group": {
+ "name": "group",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "owner_id": {
+ "name": "owner_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "home_board_id": {
+ "name": "home_board_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "mobile_home_board_id": {
+ "name": "mobile_home_board_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "position": {
+ "name": "position",
+ "type": "smallint",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "group_owner_id_user_id_fk": {
+ "name": "group_owner_id_user_id_fk",
+ "tableFrom": "group",
+ "tableTo": "user",
+ "columnsFrom": ["owner_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "group_home_board_id_board_id_fk": {
+ "name": "group_home_board_id_board_id_fk",
+ "tableFrom": "group",
+ "tableTo": "board",
+ "columnsFrom": ["home_board_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "group_mobile_home_board_id_board_id_fk": {
+ "name": "group_mobile_home_board_id_board_id_fk",
+ "tableFrom": "group",
+ "tableTo": "board",
+ "columnsFrom": ["mobile_home_board_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "group_name_unique": {
+ "name": "group_name_unique",
+ "nullsNotDistinct": false,
+ "columns": ["name"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.iconRepository": {
+ "name": "iconRepository",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "slug": {
+ "name": "slug",
+ "type": "varchar(150)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.icon": {
+ "name": "icon",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(250)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "checksum": {
+ "name": "checksum",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "icon_repository_id": {
+ "name": "icon_repository_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "icon_icon_repository_id_iconRepository_id_fk": {
+ "name": "icon_icon_repository_id_iconRepository_id_fk",
+ "tableFrom": "icon",
+ "tableTo": "iconRepository",
+ "columnsFrom": ["icon_repository_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.integrationGroupPermissions": {
+ "name": "integrationGroupPermissions",
+ "schema": "",
+ "columns": {
+ "integration_id": {
+ "name": "integration_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "group_id": {
+ "name": "group_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "permission": {
+ "name": "permission",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "integrationGroupPermissions_integration_id_integration_id_fk": {
+ "name": "integrationGroupPermissions_integration_id_integration_id_fk",
+ "tableFrom": "integrationGroupPermissions",
+ "tableTo": "integration",
+ "columnsFrom": ["integration_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "integrationGroupPermissions_group_id_group_id_fk": {
+ "name": "integrationGroupPermissions_group_id_group_id_fk",
+ "tableFrom": "integrationGroupPermissions",
+ "tableTo": "group",
+ "columnsFrom": ["group_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "integration_group_permission__pk": {
+ "name": "integration_group_permission__pk",
+ "columns": ["integration_id", "group_id", "permission"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.integration_item": {
+ "name": "integration_item",
+ "schema": "",
+ "columns": {
+ "item_id": {
+ "name": "item_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "integration_id": {
+ "name": "integration_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "integration_item_item_id_item_id_fk": {
+ "name": "integration_item_item_id_item_id_fk",
+ "tableFrom": "integration_item",
+ "tableTo": "item",
+ "columnsFrom": ["item_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "integration_item_integration_id_integration_id_fk": {
+ "name": "integration_item_integration_id_integration_id_fk",
+ "tableFrom": "integration_item",
+ "tableTo": "integration",
+ "columnsFrom": ["integration_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "integration_item_item_id_integration_id_pk": {
+ "name": "integration_item_item_id_integration_id_pk",
+ "columns": ["item_id", "integration_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.integrationSecret": {
+ "name": "integrationSecret",
+ "schema": "",
+ "columns": {
+ "kind": {
+ "name": "kind",
+ "type": "varchar(16)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "integration_id": {
+ "name": "integration_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "integration_secret__kind_idx": {
+ "name": "integration_secret__kind_idx",
+ "columns": [
+ {
+ "expression": "kind",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "integration_secret__updated_at_idx": {
+ "name": "integration_secret__updated_at_idx",
+ "columns": [
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "integrationSecret_integration_id_integration_id_fk": {
+ "name": "integrationSecret_integration_id_integration_id_fk",
+ "tableFrom": "integrationSecret",
+ "tableTo": "integration",
+ "columnsFrom": ["integration_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "integrationSecret_integration_id_kind_pk": {
+ "name": "integrationSecret_integration_id_kind_pk",
+ "columns": ["integration_id", "kind"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.integrationUserPermission": {
+ "name": "integrationUserPermission",
+ "schema": "",
+ "columns": {
+ "integration_id": {
+ "name": "integration_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "permission": {
+ "name": "permission",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "integrationUserPermission_integration_id_integration_id_fk": {
+ "name": "integrationUserPermission_integration_id_integration_id_fk",
+ "tableFrom": "integrationUserPermission",
+ "tableTo": "integration",
+ "columnsFrom": ["integration_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "integrationUserPermission_user_id_user_id_fk": {
+ "name": "integrationUserPermission_user_id_user_id_fk",
+ "tableFrom": "integrationUserPermission",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "integrationUserPermission_integration_id_user_id_permission_pk": {
+ "name": "integrationUserPermission_integration_id_user_id_permission_pk",
+ "columns": ["integration_id", "user_id", "permission"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.integration": {
+ "name": "integration",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "kind": {
+ "name": "kind",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "integration__kind_idx": {
+ "name": "integration__kind_idx",
+ "columns": [
+ {
+ "expression": "kind",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.invite": {
+ "name": "invite",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "varchar(512)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expiration_date": {
+ "name": "expiration_date",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "creator_id": {
+ "name": "creator_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "invite_creator_id_user_id_fk": {
+ "name": "invite_creator_id_user_id_fk",
+ "tableFrom": "invite",
+ "tableTo": "user",
+ "columnsFrom": ["creator_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "invite_token_unique": {
+ "name": "invite_token_unique",
+ "nullsNotDistinct": false,
+ "columns": ["token"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.item_layout": {
+ "name": "item_layout",
+ "schema": "",
+ "columns": {
+ "item_id": {
+ "name": "item_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "section_id": {
+ "name": "section_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "layout_id": {
+ "name": "layout_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "x_offset": {
+ "name": "x_offset",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "y_offset": {
+ "name": "y_offset",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "width": {
+ "name": "width",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "height": {
+ "name": "height",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "item_layout_item_id_item_id_fk": {
+ "name": "item_layout_item_id_item_id_fk",
+ "tableFrom": "item_layout",
+ "tableTo": "item",
+ "columnsFrom": ["item_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "item_layout_section_id_section_id_fk": {
+ "name": "item_layout_section_id_section_id_fk",
+ "tableFrom": "item_layout",
+ "tableTo": "section",
+ "columnsFrom": ["section_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "item_layout_layout_id_layout_id_fk": {
+ "name": "item_layout_layout_id_layout_id_fk",
+ "tableFrom": "item_layout",
+ "tableTo": "layout",
+ "columnsFrom": ["layout_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "item_layout_item_id_section_id_layout_id_pk": {
+ "name": "item_layout_item_id_section_id_layout_id_pk",
+ "columns": ["item_id", "section_id", "layout_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.item": {
+ "name": "item",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "board_id": {
+ "name": "board_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "kind": {
+ "name": "kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "options": {
+ "name": "options",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{\"json\": {}}'"
+ },
+ "advanced_options": {
+ "name": "advanced_options",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{\"json\": {}}'"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "item_board_id_board_id_fk": {
+ "name": "item_board_id_board_id_fk",
+ "tableFrom": "item",
+ "tableTo": "board",
+ "columnsFrom": ["board_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.layout": {
+ "name": "layout",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "board_id": {
+ "name": "board_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "column_count": {
+ "name": "column_count",
+ "type": "smallint",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "breakpoint": {
+ "name": "breakpoint",
+ "type": "smallint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "layout_board_id_board_id_fk": {
+ "name": "layout_board_id_board_id_fk",
+ "tableFrom": "layout",
+ "tableTo": "board",
+ "columnsFrom": ["board_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.media": {
+ "name": "media",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(512)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content": {
+ "name": "content",
+ "type": "bytea",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content_type": {
+ "name": "content_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "size": {
+ "name": "size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "creator_id": {
+ "name": "creator_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "media_creator_id_user_id_fk": {
+ "name": "media_creator_id_user_id_fk",
+ "tableFrom": "media",
+ "tableTo": "user",
+ "columnsFrom": ["creator_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.onboarding": {
+ "name": "onboarding",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "step": {
+ "name": "step",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "previous_step": {
+ "name": "previous_step",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.search_engine": {
+ "name": "search_engine",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "icon_url": {
+ "name": "icon_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "short": {
+ "name": "short",
+ "type": "varchar(8)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "url_template": {
+ "name": "url_template",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "type": {
+ "name": "type",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'generic'"
+ },
+ "integration_id": {
+ "name": "integration_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "search_engine_integration_id_integration_id_fk": {
+ "name": "search_engine_integration_id_integration_id_fk",
+ "tableFrom": "search_engine",
+ "tableTo": "integration",
+ "columnsFrom": ["integration_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "search_engine_short_unique": {
+ "name": "search_engine_short_unique",
+ "nullsNotDistinct": false,
+ "columns": ["short"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.section_collapse_state": {
+ "name": "section_collapse_state",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "section_id": {
+ "name": "section_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "collapsed": {
+ "name": "collapsed",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "section_collapse_state_user_id_user_id_fk": {
+ "name": "section_collapse_state_user_id_user_id_fk",
+ "tableFrom": "section_collapse_state",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "section_collapse_state_section_id_section_id_fk": {
+ "name": "section_collapse_state_section_id_section_id_fk",
+ "tableFrom": "section_collapse_state",
+ "tableTo": "section",
+ "columnsFrom": ["section_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "section_collapse_state_user_id_section_id_pk": {
+ "name": "section_collapse_state_user_id_section_id_pk",
+ "columns": ["user_id", "section_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.section_layout": {
+ "name": "section_layout",
+ "schema": "",
+ "columns": {
+ "section_id": {
+ "name": "section_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "layout_id": {
+ "name": "layout_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "parent_section_id": {
+ "name": "parent_section_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "x_offset": {
+ "name": "x_offset",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "y_offset": {
+ "name": "y_offset",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "width": {
+ "name": "width",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "height": {
+ "name": "height",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "section_layout_section_id_section_id_fk": {
+ "name": "section_layout_section_id_section_id_fk",
+ "tableFrom": "section_layout",
+ "tableTo": "section",
+ "columnsFrom": ["section_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "section_layout_layout_id_layout_id_fk": {
+ "name": "section_layout_layout_id_layout_id_fk",
+ "tableFrom": "section_layout",
+ "tableTo": "layout",
+ "columnsFrom": ["layout_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "section_layout_parent_section_id_section_id_fk": {
+ "name": "section_layout_parent_section_id_section_id_fk",
+ "tableFrom": "section_layout",
+ "tableTo": "section",
+ "columnsFrom": ["parent_section_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "section_layout_section_id_layout_id_pk": {
+ "name": "section_layout_section_id_layout_id_pk",
+ "columns": ["section_id", "layout_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.section": {
+ "name": "section",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "board_id": {
+ "name": "board_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "kind": {
+ "name": "kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "x_offset": {
+ "name": "x_offset",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "y_offset": {
+ "name": "y_offset",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "options": {
+ "name": "options",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{\"json\": {}}'"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "section_board_id_board_id_fk": {
+ "name": "section_board_id_board_id_fk",
+ "tableFrom": "section",
+ "tableTo": "board",
+ "columnsFrom": ["board_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.serverSetting": {
+ "name": "serverSetting",
+ "schema": "",
+ "columns": {
+ "setting_key": {
+ "name": "setting_key",
+ "type": "varchar(64)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{\"json\": {}}'"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "serverSetting_settingKey_unique": {
+ "name": "serverSetting_settingKey_unique",
+ "nullsNotDistinct": false,
+ "columns": ["setting_key"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.session": {
+ "name": "session",
+ "schema": "",
+ "columns": {
+ "session_token": {
+ "name": "session_token",
+ "type": "varchar(512)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires": {
+ "name": "expires",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "user_id_idx": {
+ "name": "user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "session_user_id_user_id_fk": {
+ "name": "session_user_id_user_id_fk",
+ "tableFrom": "session",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.trusted_certificate_hostname": {
+ "name": "trusted_certificate_hostname",
+ "schema": "",
+ "columns": {
+ "hostname": {
+ "name": "hostname",
+ "type": "varchar(256)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "thumbprint": {
+ "name": "thumbprint",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "certificate": {
+ "name": "certificate",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "trusted_certificate_hostname_hostname_thumbprint_pk": {
+ "name": "trusted_certificate_hostname_hostname_thumbprint_pk",
+ "columns": ["hostname", "thumbprint"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user": {
+ "name": "user",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(64)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "email_verified": {
+ "name": "email_verified",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "image": {
+ "name": "image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "salt": {
+ "name": "salt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provider": {
+ "name": "provider",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'credentials'"
+ },
+ "home_board_id": {
+ "name": "home_board_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "mobile_home_board_id": {
+ "name": "mobile_home_board_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "default_search_engine_id": {
+ "name": "default_search_engine_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "open_search_in_new_tab": {
+ "name": "open_search_in_new_tab",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "color_scheme": {
+ "name": "color_scheme",
+ "type": "varchar(5)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'dark'"
+ },
+ "first_day_of_week": {
+ "name": "first_day_of_week",
+ "type": "smallint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "ping_icons_enabled": {
+ "name": "ping_icons_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "user_home_board_id_board_id_fk": {
+ "name": "user_home_board_id_board_id_fk",
+ "tableFrom": "user",
+ "tableTo": "board",
+ "columnsFrom": ["home_board_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "user_mobile_home_board_id_board_id_fk": {
+ "name": "user_mobile_home_board_id_board_id_fk",
+ "tableFrom": "user",
+ "tableTo": "board",
+ "columnsFrom": ["mobile_home_board_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "user_default_search_engine_id_search_engine_id_fk": {
+ "name": "user_default_search_engine_id_search_engine_id_fk",
+ "tableFrom": "user",
+ "tableTo": "search_engine",
+ "columnsFrom": ["default_search_engine_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.verificationToken": {
+ "name": "verificationToken",
+ "schema": "",
+ "columns": {
+ "identifier": {
+ "name": "identifier",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "varchar(512)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires": {
+ "name": "expires",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "verificationToken_identifier_token_pk": {
+ "name": "verificationToken_identifier_token_pk",
+ "columns": ["identifier", "token"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {},
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
diff --git a/packages/db/migrations/postgresql/meta/_journal.json b/packages/db/migrations/postgresql/meta/_journal.json
new file mode 100644
index 000000000..44e76ff19
--- /dev/null
+++ b/packages/db/migrations/postgresql/meta/_journal.json
@@ -0,0 +1,13 @@
+{
+ "version": "7",
+ "dialect": "postgresql",
+ "entries": [
+ {
+ "idx": 0,
+ "version": "7",
+ "when": 1754853510707,
+ "tag": "0000_initial",
+ "breakpoints": true
+ }
+ ]
+}
diff --git a/packages/db/migrations/postgresql/migrate.ts b/packages/db/migrations/postgresql/migrate.ts
new file mode 100644
index 000000000..6e97f7a1b
--- /dev/null
+++ b/packages/db/migrations/postgresql/migrate.ts
@@ -0,0 +1,45 @@
+import { drizzle } from "drizzle-orm/node-postgres";
+import { migrate } from "drizzle-orm/node-postgres/migrator";
+import { Pool } from "pg";
+
+import type { Database } from "../..";
+import { env } from "../../env";
+import * as pgSchema from "../../schema/postgresql";
+import { applyCustomMigrationsAsync } from "../custom";
+import { seedDataAsync } from "../seed";
+
+const migrationsFolder = process.argv[2] ?? ".";
+
+const migrateAsync = async () => {
+ const pool = new Pool(
+ env.DB_URL
+ ? { connectionString: env.DB_URL }
+ : {
+ host: env.DB_HOST,
+ database: env.DB_NAME,
+ port: env.DB_PORT,
+ user: env.DB_USER,
+ password: env.DB_PASSWORD,
+ },
+ );
+
+ const db = drizzle({
+ schema: pgSchema,
+ casing: "snake_case",
+ client: pool,
+ });
+
+ await migrate(db, { migrationsFolder });
+ await seedDataAsync(db as unknown as Database);
+ await applyCustomMigrationsAsync(db as unknown as Database);
+};
+
+migrateAsync()
+ .then(() => {
+ console.log("Migration complete");
+ process.exit(0);
+ })
+ .catch((err) => {
+ console.log("Migration failed", err);
+ process.exit(1);
+ });
diff --git a/packages/db/migrations/seed.ts b/packages/db/migrations/seed.ts
index 4a1efdafc..e3d16bfdc 100644
--- a/packages/db/migrations/seed.ts
+++ b/packages/db/migrations/seed.ts
@@ -15,9 +15,8 @@ import {
insertServerSettingByKeyAsync,
updateServerSettingByKeyAsync,
} from "../queries/server-setting";
-import { integrations, onboarding, searchEngines } from "../schema";
+import { groups, integrations, onboarding, searchEngines } from "../schema";
import type { Integration } from "../schema";
-import { groups } from "../schema/mysql";
export const seedDataAsync = async (db: Database) => {
await seedEveryoneGroupAsync(db);
diff --git a/packages/db/package.json b/packages/db/package.json
index 8bbae94a6..317d35b25 100644
--- a/packages/db/package.json
+++ b/packages/db/package.json
@@ -16,8 +16,9 @@
"main": "./index.ts",
"types": "./index.ts",
"scripts": {
- "build": "pnpm run build:sqlite && pnpm run build:mysql",
+ "build": "pnpm run build:sqlite && pnpm run build:mysql && pnpm run build:postgresql",
"build:mysql": "esbuild migrations/mysql/migrate.ts --bundle --platform=node --outfile=migrations/mysql/migrate.cjs",
+ "build:postgresql": "esbuild migrations/postgresql/migrate.ts --bundle --platform=node --outfile=migrations/postgresql/migrate.cjs",
"build:sqlite": "esbuild migrations/sqlite/migrate.ts --bundle --platform=node --outfile=migrations/sqlite/migrate.cjs",
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
@@ -26,10 +27,14 @@
"migration:mysql:drop": "pnpm with-env drizzle-kit drop --config ./configs/mysql.config.ts",
"migration:mysql:generate": "pnpm with-env drizzle-kit generate --config ./configs/mysql.config.ts",
"migration:mysql:run": "pnpm with-env drizzle-kit migrate --config ./configs/mysql.config.ts && pnpm run seed && pnpm run migration:custom",
+ "migration:postgresql:drop": "pnpm with-env drizzle-kit drop --config ./configs/postgresql.config.ts",
+ "migration:postgresql:generate": "pnpm with-env drizzle-kit generate --config ./configs/postgresql.config.ts",
+ "migration:postgresql:run": "pnpm with-env drizzle-kit migrate --config ./configs/postgresql.config.ts && pnpm run seed && pnpm run migration:custom",
"migration:sqlite:drop": "pnpm with-env drizzle-kit drop --config ./configs/sqlite.config.ts",
"migration:sqlite:generate": "pnpm with-env drizzle-kit generate --config ./configs/sqlite.config.ts",
"migration:sqlite:run": "pnpm with-env drizzle-kit migrate --config ./configs/sqlite.config.ts && pnpm run seed && pnpm run migration:custom",
"push:mysql": "pnpm with-env drizzle-kit push --config ./configs/mysql.config.ts",
+ "push:postgresql": "pnpm with-env drizzle-kit push --config ./configs/postgresql.config.ts",
"push:sqlite": "pnpm with-env drizzle-kit push --config ./configs/sqlite.config.ts",
"seed": "pnpm with-env tsx ./migrations/run-seed.ts",
"studio": "pnpm with-env drizzle-kit studio --config ./configs/sqlite.config.ts",
@@ -47,12 +52,14 @@
"@mantine/core": "^8.2.5",
"@paralleldrive/cuid2": "^2.2.2",
"@testcontainers/mysql": "^11.5.1",
+ "@testcontainers/postgresql": "^11.4.0",
"better-sqlite3": "^12.2.0",
"dotenv": "^17.2.1",
"drizzle-kit": "^0.31.4",
"drizzle-orm": "^0.44.4",
"drizzle-zod": "^0.8.3",
"mysql2": "3.14.3",
+ "pg": "^8.16.3",
"superjson": "2.2.2"
},
"devDependencies": {
@@ -60,6 +67,7 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/better-sqlite3": "7.6.13",
+ "@types/pg": "^8.15.4",
"dotenv-cli": "^10.0.0",
"esbuild": "^0.25.9",
"eslint": "^9.33.0",
diff --git a/packages/db/schema/index.ts b/packages/db/schema/index.ts
index 1b9d143c2..52a1b9cdb 100644
--- a/packages/db/schema/index.ts
+++ b/packages/db/schema/index.ts
@@ -1,11 +1,20 @@
import type { InferSelectModel } from "drizzle-orm";
+import { env } from "../env";
import * as mysqlSchema from "./mysql";
+import * as pgSchema from "./postgresql";
import * as sqliteSchema from "./sqlite";
+export type PostgreSqlSchema = typeof pgSchema;
+export type MySqlSchema = typeof mysqlSchema;
type Schema = typeof sqliteSchema;
-const schema = process.env.DB_DRIVER === "mysql2" ? (mysqlSchema as unknown as Schema) : sqliteSchema;
+const schema =
+ env.DB_DRIVER === "mysql2"
+ ? (mysqlSchema as unknown as Schema)
+ : env.DB_DRIVER === "node-postgres"
+ ? (pgSchema as unknown as Schema)
+ : sqliteSchema;
// Sadly we can't use export * from here as we have multiple possible exports
export const {
diff --git a/packages/db/schema/mysql.ts b/packages/db/schema/mysql.ts
index a5607c7b5..29ca741a8 100644
--- a/packages/db/schema/mysql.ts
+++ b/packages/db/schema/mysql.ts
@@ -528,6 +528,7 @@ export const userRelations = relations(users, ({ one, many }) => ({
groups: many(groupMembers),
ownedGroups: many(groups),
invites: many(invites),
+ medias: many(medias),
defaultSearchEngine: one(searchEngines, {
fields: [users.defaultSearchEngineId],
references: [searchEngines.id],
diff --git a/packages/db/schema/postgresql.ts b/packages/db/schema/postgresql.ts
new file mode 100644
index 000000000..d84c816e9
--- /dev/null
+++ b/packages/db/schema/postgresql.ts
@@ -0,0 +1,775 @@
+import type { AdapterAccount } from "@auth/core/adapters";
+import type { MantineSize } from "@mantine/core";
+import type { DayOfWeek } from "@mantine/dates";
+import { relations } from "drizzle-orm";
+import type { AnyPgColumn } from "drizzle-orm/pg-core";
+import {
+ boolean,
+ customType,
+ index,
+ integer,
+ pgTable,
+ primaryKey,
+ smallint,
+ text,
+ timestamp,
+ varchar,
+} from "drizzle-orm/pg-core";
+
+import {
+ backgroundImageAttachments,
+ backgroundImageRepeats,
+ backgroundImageSizes,
+ emptySuperJSON,
+} from "@homarr/definitions";
+import type {
+ BackgroundImageAttachment,
+ BackgroundImageRepeat,
+ BackgroundImageSize,
+ BoardPermission,
+ ColorScheme,
+ GroupPermissionKey,
+ IntegrationKind,
+ IntegrationPermission,
+ IntegrationSecretKind,
+ OnboardingStep,
+ SearchEngineType,
+ SectionKind,
+ SupportedAuthProvider,
+ WidgetKind,
+} from "@homarr/definitions";
+
+const customBlob = customType<{ data: Buffer }>({
+ dataType() {
+ return "bytea";
+ },
+});
+
+export const apiKeys = pgTable("apiKey", {
+ id: varchar({ length: 64 }).notNull().primaryKey(),
+ apiKey: text().notNull(),
+ salt: text().notNull(),
+ userId: varchar({ length: 64 })
+ .notNull()
+ .references((): AnyPgColumn => users.id, {
+ onDelete: "cascade",
+ }),
+});
+
+export const users = pgTable("user", {
+ id: varchar({ length: 64 }).notNull().primaryKey(),
+ name: text(),
+ email: text(),
+ emailVerified: timestamp(),
+ image: text(),
+ password: text(),
+ salt: text(),
+ provider: varchar({ length: 64 }).$type().default("credentials").notNull(),
+ homeBoardId: varchar({ length: 64 }).references((): AnyPgColumn => boards.id, {
+ onDelete: "set null",
+ }),
+ mobileHomeBoardId: varchar({ length: 64 }).references((): AnyPgColumn => boards.id, {
+ onDelete: "set null",
+ }),
+ defaultSearchEngineId: varchar({ length: 64 }).references(() => searchEngines.id, {
+ onDelete: "set null",
+ }),
+ openSearchInNewTab: boolean().default(false).notNull(),
+ colorScheme: varchar({ length: 5 }).$type().default("dark").notNull(),
+ firstDayOfWeek: smallint().$type().default(1).notNull(), // Defaults to Monday
+ pingIconsEnabled: boolean().default(false).notNull(),
+});
+
+export const accounts = pgTable(
+ "account",
+ {
+ userId: varchar({ length: 64 })
+ .notNull()
+ .references(() => users.id, { onDelete: "cascade" }),
+ type: text().$type().notNull(),
+ provider: varchar({ length: 64 }).notNull(),
+ providerAccountId: varchar({ length: 64 }).notNull(),
+ refresh_token: text(),
+ access_token: text(),
+ expires_at: integer(),
+ token_type: text(),
+ scope: text(),
+ id_token: text(),
+ session_state: text(),
+ },
+ (account) => ({
+ compoundKey: primaryKey({
+ columns: [account.provider, account.providerAccountId],
+ }),
+ userIdIdx: index("userId_idx").on(account.userId),
+ }),
+);
+
+export const sessions = pgTable(
+ "session",
+ {
+ sessionToken: varchar({ length: 512 }).notNull().primaryKey(),
+ userId: varchar({ length: 64 })
+ .notNull()
+ .references(() => users.id, { onDelete: "cascade" }),
+ expires: timestamp().notNull(),
+ },
+ (session) => ({
+ userIdIdx: index("user_id_idx").on(session.userId),
+ }),
+);
+
+export const verificationTokens = pgTable(
+ "verificationToken",
+ {
+ identifier: varchar({ length: 64 }).notNull(),
+ token: varchar({ length: 512 }).notNull(),
+ expires: timestamp().notNull(),
+ },
+ (verificationToken) => ({
+ compoundKey: primaryKey({
+ columns: [verificationToken.identifier, verificationToken.token],
+ }),
+ }),
+);
+
+export const groupMembers = pgTable(
+ "groupMember",
+ {
+ groupId: varchar({ length: 64 })
+ .notNull()
+ .references(() => groups.id, { onDelete: "cascade" }),
+ userId: varchar({ length: 64 })
+ .notNull()
+ .references(() => users.id, { onDelete: "cascade" }),
+ },
+ (groupMember) => ({
+ compoundKey: primaryKey({
+ columns: [groupMember.groupId, groupMember.userId],
+ }),
+ }),
+);
+
+export const groups = pgTable("group", {
+ id: varchar({ length: 64 }).notNull().primaryKey(),
+ name: varchar({ length: 64 }).unique().notNull(),
+ ownerId: varchar({ length: 64 }).references(() => users.id, {
+ onDelete: "set null",
+ }),
+ homeBoardId: varchar({ length: 64 }).references(() => boards.id, {
+ onDelete: "set null",
+ }),
+ mobileHomeBoardId: varchar({ length: 64 }).references(() => boards.id, {
+ onDelete: "set null",
+ }),
+ position: smallint().notNull(),
+});
+
+export const groupPermissions = pgTable("groupPermission", {
+ groupId: varchar({ length: 64 })
+ .notNull()
+ .references(() => groups.id, { onDelete: "cascade" }),
+ permission: text().$type().notNull(),
+});
+
+export const invites = pgTable("invite", {
+ id: varchar({ length: 64 }).notNull().primaryKey(),
+ token: varchar({ length: 512 }).notNull().unique(),
+ expirationDate: timestamp().notNull(),
+ creatorId: varchar({ length: 64 })
+ .notNull()
+ .references(() => users.id, { onDelete: "cascade" }),
+});
+
+export const medias = pgTable("media", {
+ id: varchar({ length: 64 }).notNull().primaryKey(),
+ name: varchar({ length: 512 }).notNull(),
+ content: customBlob().notNull(),
+ contentType: text().notNull(),
+ size: integer().notNull(),
+ createdAt: timestamp({ mode: "date" }).notNull().defaultNow(),
+ creatorId: varchar({ length: 64 }).references(() => users.id, { onDelete: "set null" }),
+});
+
+export const integrations = pgTable(
+ "integration",
+ {
+ id: varchar({ length: 64 }).notNull().primaryKey(),
+ name: text().notNull(),
+ url: text().notNull(),
+ kind: varchar({ length: 128 }).$type().notNull(),
+ },
+ (integrations) => ({
+ kindIdx: index("integration__kind_idx").on(integrations.kind),
+ }),
+);
+
+export const integrationSecrets = pgTable(
+ "integrationSecret",
+ {
+ kind: varchar({ length: 16 }).$type().notNull(),
+ value: text().$type<`${string}.${string}`>().notNull(),
+ updatedAt: timestamp()
+ .$onUpdateFn(() => new Date())
+ .notNull(),
+ integrationId: varchar({ length: 64 })
+ .notNull()
+ .references(() => integrations.id, { onDelete: "cascade" }),
+ },
+ (integrationSecret) => ({
+ compoundKey: primaryKey({
+ columns: [integrationSecret.integrationId, integrationSecret.kind],
+ }),
+ kindIdx: index("integration_secret__kind_idx").on(integrationSecret.kind),
+ updatedAtIdx: index("integration_secret__updated_at_idx").on(integrationSecret.updatedAt),
+ }),
+);
+
+export const integrationUserPermissions = pgTable(
+ "integrationUserPermission",
+ {
+ integrationId: varchar({ length: 64 })
+ .notNull()
+ .references(() => integrations.id, { onDelete: "cascade" }),
+ userId: varchar({ length: 64 })
+ .notNull()
+ .references(() => users.id, { onDelete: "cascade" }),
+ permission: varchar({ length: 128 }).$type().notNull(),
+ },
+ (table) => ({
+ compoundKey: primaryKey({
+ columns: [table.integrationId, table.userId, table.permission],
+ }),
+ }),
+);
+
+export const integrationGroupPermissions = pgTable(
+ "integrationGroupPermissions",
+ {
+ integrationId: varchar({ length: 64 })
+ .notNull()
+ .references(() => integrations.id, { onDelete: "cascade" }),
+ groupId: varchar({ length: 64 })
+ .notNull()
+ .references(() => groups.id, { onDelete: "cascade" }),
+ permission: varchar({ length: 128 }).$type().notNull(),
+ },
+ (table) => ({
+ compoundKey: primaryKey({
+ columns: [table.integrationId, table.groupId, table.permission],
+ name: "integration_group_permission__pk",
+ }),
+ }),
+);
+
+export const boards = pgTable("board", {
+ id: varchar({ length: 64 }).notNull().primaryKey(),
+ name: varchar({ length: 256 }).unique().notNull(),
+ isPublic: boolean().default(false).notNull(),
+ creatorId: varchar({ length: 64 }).references(() => users.id, {
+ onDelete: "set null",
+ }),
+ pageTitle: text(),
+ metaTitle: text(),
+ logoImageUrl: text(),
+ faviconImageUrl: text(),
+ backgroundImageUrl: text(),
+ backgroundImageAttachment: text()
+ .$type()
+ .default(backgroundImageAttachments.defaultValue)
+ .notNull(),
+ backgroundImageRepeat: text().$type().default(backgroundImageRepeats.defaultValue).notNull(),
+ backgroundImageSize: text().$type().default(backgroundImageSizes.defaultValue).notNull(),
+ primaryColor: text().default("#fa5252").notNull(),
+ secondaryColor: text().default("#fd7e14").notNull(),
+ opacity: integer().default(100).notNull(),
+ customCss: text(),
+ iconColor: text(),
+ itemRadius: text().$type().default("lg").notNull(),
+ disableStatus: boolean().default(false).notNull(),
+});
+
+export const boardUserPermissions = pgTable(
+ "boardUserPermission",
+ {
+ boardId: varchar({ length: 64 })
+ .notNull()
+ .references(() => boards.id, { onDelete: "cascade" }),
+ userId: varchar({ length: 64 })
+ .notNull()
+ .references(() => users.id, { onDelete: "cascade" }),
+ permission: varchar({ length: 128 }).$type().notNull(),
+ },
+ (table) => ({
+ compoundKey: primaryKey({
+ columns: [table.boardId, table.userId, table.permission],
+ }),
+ }),
+);
+
+export const boardGroupPermissions = pgTable(
+ "boardGroupPermission",
+ {
+ boardId: varchar({ length: 64 })
+ .notNull()
+ .references(() => boards.id, { onDelete: "cascade" }),
+ groupId: varchar({ length: 64 })
+ .notNull()
+ .references(() => groups.id, { onDelete: "cascade" }),
+ permission: varchar({ length: 128 }).$type().notNull(),
+ },
+ (table) => ({
+ compoundKey: primaryKey({
+ columns: [table.boardId, table.groupId, table.permission],
+ }),
+ }),
+);
+
+export const layouts = pgTable("layout", {
+ id: varchar({ length: 64 }).notNull().primaryKey(),
+ name: varchar({ length: 32 }).notNull(),
+ boardId: varchar({ length: 64 })
+ .notNull()
+ .references(() => boards.id, { onDelete: "cascade" }),
+ columnCount: smallint().notNull(),
+ breakpoint: smallint().notNull().default(0),
+});
+
+export const itemLayouts = pgTable(
+ "item_layout",
+ {
+ itemId: varchar({ length: 64 })
+ .notNull()
+ .references(() => items.id, { onDelete: "cascade" }),
+ sectionId: varchar({ length: 64 })
+ .notNull()
+ .references(() => sections.id, { onDelete: "cascade" }),
+ layoutId: varchar({ length: 64 })
+ .notNull()
+ .references(() => layouts.id, { onDelete: "cascade" }),
+ xOffset: integer().notNull(),
+ yOffset: integer().notNull(),
+ width: integer().notNull(),
+ height: integer().notNull(),
+ },
+ (table) => ({
+ compoundKey: primaryKey({
+ columns: [table.itemId, table.sectionId, table.layoutId],
+ }),
+ }),
+);
+
+export const sectionLayouts = pgTable(
+ "section_layout",
+ {
+ sectionId: varchar({ length: 64 })
+ .notNull()
+ .references(() => sections.id, { onDelete: "cascade" }),
+ layoutId: varchar({ length: 64 })
+ .notNull()
+ .references(() => layouts.id, { onDelete: "cascade" }),
+ parentSectionId: varchar({ length: 64 }).references((): AnyPgColumn => sections.id, {
+ onDelete: "cascade",
+ }),
+ xOffset: integer().notNull(),
+ yOffset: integer().notNull(),
+ width: integer().notNull(),
+ height: integer().notNull(),
+ },
+ (table) => ({
+ compoundKey: primaryKey({
+ columns: [table.sectionId, table.layoutId],
+ }),
+ }),
+);
+
+export const sections = pgTable("section", {
+ id: varchar({ length: 64 }).notNull().primaryKey(),
+ boardId: varchar({ length: 64 })
+ .notNull()
+ .references(() => boards.id, { onDelete: "cascade" }),
+ kind: text().$type().notNull(),
+ xOffset: integer(),
+ yOffset: integer(),
+ name: text(),
+ options: text().default(emptySuperJSON),
+});
+
+export const sectionCollapseStates = pgTable(
+ "section_collapse_state",
+ {
+ userId: varchar({ length: 64 })
+ .notNull()
+ .references(() => users.id, { onDelete: "cascade" }),
+ sectionId: varchar({ length: 64 })
+ .notNull()
+ .references(() => sections.id, { onDelete: "cascade" }),
+ collapsed: boolean().default(false).notNull(),
+ },
+ (table) => ({
+ compoundKey: primaryKey({
+ columns: [table.userId, table.sectionId],
+ }),
+ }),
+);
+
+export const items = pgTable("item", {
+ id: varchar({ length: 64 }).notNull().primaryKey(),
+ boardId: varchar({ length: 64 })
+ .notNull()
+ .references(() => boards.id, { onDelete: "cascade" }),
+ kind: text().$type().notNull(),
+ options: text().default(emptySuperJSON).notNull(),
+ advancedOptions: text().default(emptySuperJSON).notNull(),
+});
+
+export const apps = pgTable("app", {
+ id: varchar({ length: 64 }).notNull().primaryKey(),
+ name: text().notNull(),
+ description: text(),
+ iconUrl: text().notNull(),
+ href: text(),
+ pingUrl: text(),
+});
+
+export const integrationItems = pgTable(
+ "integration_item",
+ {
+ itemId: varchar({ length: 64 })
+ .notNull()
+ .references(() => items.id, { onDelete: "cascade" }),
+ integrationId: varchar({ length: 64 })
+ .notNull()
+ .references(() => integrations.id, { onDelete: "cascade" }),
+ },
+ (table) => ({
+ compoundKey: primaryKey({
+ columns: [table.itemId, table.integrationId],
+ }),
+ }),
+);
+
+export const icons = pgTable("icon", {
+ id: varchar({ length: 64 }).notNull().primaryKey(),
+ name: varchar({ length: 250 }).notNull(),
+ url: text().notNull(),
+ checksum: text().notNull(),
+ iconRepositoryId: varchar({ length: 64 })
+ .notNull()
+ .references(() => iconRepositories.id, { onDelete: "cascade" }),
+});
+
+export const iconRepositories = pgTable("iconRepository", {
+ id: varchar({ length: 64 }).notNull().primaryKey(),
+ slug: varchar({ length: 150 }).notNull(),
+});
+
+export const serverSettings = pgTable("serverSetting", {
+ settingKey: varchar({ length: 64 }).notNull().unique().primaryKey(),
+ value: text().default(emptySuperJSON).notNull(),
+});
+
+export const apiKeyRelations = relations(apiKeys, ({ one }) => ({
+ user: one(users, {
+ fields: [apiKeys.userId],
+ references: [users.id],
+ }),
+}));
+
+export const searchEngines = pgTable("search_engine", {
+ id: varchar({ length: 64 }).notNull().primaryKey(),
+ iconUrl: text().notNull(),
+ name: varchar({ length: 64 }).notNull(),
+ short: varchar({ length: 8 }).unique().notNull(),
+ description: text(),
+ urlTemplate: text(),
+ type: varchar({ length: 64 }).$type().notNull().default("generic"),
+ integrationId: varchar({ length: 64 }).references(() => integrations.id, { onDelete: "cascade" }),
+});
+
+export const onboarding = pgTable("onboarding", {
+ id: varchar({ length: 64 }).notNull().primaryKey(),
+ step: varchar({ length: 64 }).$type().notNull(),
+ previousStep: varchar({ length: 64 }).$type(),
+});
+
+export const trustedCertificateHostnames = pgTable(
+ "trusted_certificate_hostname",
+ {
+ hostname: varchar({ length: 256 }).notNull(),
+ thumbprint: varchar({ length: 128 }).notNull(),
+ certificate: text().notNull(),
+ },
+ (table) => ({
+ compoundKey: primaryKey({
+ columns: [table.hostname, table.thumbprint],
+ }),
+ }),
+);
+
+export const cronJobConfigurations = pgTable("cron_job_configuration", {
+ name: varchar({ length: 256 }).notNull().primaryKey(),
+ cronExpression: varchar({ length: 32 }).notNull(),
+ isEnabled: boolean().default(true).notNull(),
+});
+
+export const accountRelations = relations(accounts, ({ one }) => ({
+ user: one(users, {
+ fields: [accounts.userId],
+ references: [users.id],
+ }),
+}));
+
+export const userRelations = relations(users, ({ one, many }) => ({
+ accounts: many(accounts),
+ boards: many(boards),
+ boardPermissions: many(boardUserPermissions),
+ groups: many(groupMembers),
+ ownedGroups: many(groups),
+ invites: many(invites),
+ medias: many(medias),
+ defaultSearchEngine: one(searchEngines, {
+ fields: [users.defaultSearchEngineId],
+ references: [searchEngines.id],
+ }),
+}));
+
+export const mediaRelations = relations(medias, ({ one }) => ({
+ creator: one(users, {
+ fields: [medias.creatorId],
+ references: [users.id],
+ }),
+}));
+
+export const iconRelations = relations(icons, ({ one }) => ({
+ repository: one(iconRepositories, {
+ fields: [icons.iconRepositoryId],
+ references: [iconRepositories.id],
+ }),
+}));
+
+export const iconRepositoryRelations = relations(iconRepositories, ({ many }) => ({
+ icons: many(icons),
+}));
+
+export const inviteRelations = relations(invites, ({ one }) => ({
+ creator: one(users, {
+ fields: [invites.creatorId],
+ references: [users.id],
+ }),
+}));
+
+export const sessionRelations = relations(sessions, ({ one }) => ({
+ user: one(users, {
+ fields: [sessions.userId],
+ references: [users.id],
+ }),
+}));
+
+export const groupMemberRelations = relations(groupMembers, ({ one }) => ({
+ group: one(groups, {
+ fields: [groupMembers.groupId],
+ references: [groups.id],
+ }),
+ user: one(users, {
+ fields: [groupMembers.userId],
+ references: [users.id],
+ }),
+}));
+
+export const groupRelations = relations(groups, ({ one, many }) => ({
+ permissions: many(groupPermissions),
+ boardPermissions: many(boardGroupPermissions),
+ members: many(groupMembers),
+ owner: one(users, {
+ fields: [groups.ownerId],
+ references: [users.id],
+ }),
+ homeBoard: one(boards, {
+ fields: [groups.homeBoardId],
+ references: [boards.id],
+ relationName: "groupRelations__board__homeBoardId",
+ }),
+ mobileHomeBoard: one(boards, {
+ fields: [groups.mobileHomeBoardId],
+ references: [boards.id],
+ relationName: "groupRelations__board__mobileHomeBoardId",
+ }),
+}));
+
+export const groupPermissionRelations = relations(groupPermissions, ({ one }) => ({
+ group: one(groups, {
+ fields: [groupPermissions.groupId],
+ references: [groups.id],
+ }),
+}));
+
+export const boardUserPermissionRelations = relations(boardUserPermissions, ({ one }) => ({
+ user: one(users, {
+ fields: [boardUserPermissions.userId],
+ references: [users.id],
+ }),
+ board: one(boards, {
+ fields: [boardUserPermissions.boardId],
+ references: [boards.id],
+ }),
+}));
+
+export const boardGroupPermissionRelations = relations(boardGroupPermissions, ({ one }) => ({
+ group: one(groups, {
+ fields: [boardGroupPermissions.groupId],
+ references: [groups.id],
+ }),
+ board: one(boards, {
+ fields: [boardGroupPermissions.boardId],
+ references: [boards.id],
+ }),
+}));
+
+export const integrationRelations = relations(integrations, ({ many }) => ({
+ secrets: many(integrationSecrets),
+ items: many(integrationItems),
+ userPermissions: many(integrationUserPermissions),
+ groupPermissions: many(integrationGroupPermissions),
+}));
+
+export const integrationUserPermissionRelations = relations(integrationUserPermissions, ({ one }) => ({
+ user: one(users, {
+ fields: [integrationUserPermissions.userId],
+ references: [users.id],
+ }),
+ integration: one(integrations, {
+ fields: [integrationUserPermissions.integrationId],
+ references: [integrations.id],
+ }),
+}));
+
+export const integrationGroupPermissionRelations = relations(integrationGroupPermissions, ({ one }) => ({
+ group: one(groups, {
+ fields: [integrationGroupPermissions.groupId],
+ references: [groups.id],
+ }),
+ integration: one(integrations, {
+ fields: [integrationGroupPermissions.integrationId],
+ references: [integrations.id],
+ }),
+}));
+
+export const integrationSecretRelations = relations(integrationSecrets, ({ one }) => ({
+ integration: one(integrations, {
+ fields: [integrationSecrets.integrationId],
+ references: [integrations.id],
+ }),
+}));
+
+export const boardRelations = relations(boards, ({ many, one }) => ({
+ sections: many(sections),
+ items: many(items),
+ creator: one(users, {
+ fields: [boards.creatorId],
+ references: [users.id],
+ }),
+ userPermissions: many(boardUserPermissions),
+ groupPermissions: many(boardGroupPermissions),
+ layouts: many(layouts),
+ groupHomes: many(groups, {
+ relationName: "groupRelations__board__homeBoardId",
+ }),
+ mobileHomeBoard: many(groups, {
+ relationName: "groupRelations__board__mobileHomeBoardId",
+ }),
+}));
+
+export const sectionRelations = relations(sections, ({ many, one }) => ({
+ board: one(boards, {
+ fields: [sections.boardId],
+ references: [boards.id],
+ }),
+ collapseStates: many(sectionCollapseStates),
+ layouts: many(sectionLayouts, {
+ relationName: "sectionLayoutRelations__section__sectionId",
+ }),
+ children: many(sectionLayouts, {
+ relationName: "sectionLayoutRelations__section__parentSectionId",
+ }),
+}));
+
+export const sectionCollapseStateRelations = relations(sectionCollapseStates, ({ one }) => ({
+ user: one(users, {
+ fields: [sectionCollapseStates.userId],
+ references: [users.id],
+ }),
+ section: one(sections, {
+ fields: [sectionCollapseStates.sectionId],
+ references: [sections.id],
+ }),
+}));
+
+export const itemRelations = relations(items, ({ one, many }) => ({
+ integrations: many(integrationItems),
+ layouts: many(itemLayouts),
+ board: one(boards, {
+ fields: [items.boardId],
+ references: [boards.id],
+ }),
+}));
+
+export const integrationItemRelations = relations(integrationItems, ({ one }) => ({
+ integration: one(integrations, {
+ fields: [integrationItems.integrationId],
+ references: [integrations.id],
+ }),
+ item: one(items, {
+ fields: [integrationItems.itemId],
+ references: [items.id],
+ }),
+}));
+
+export const searchEngineRelations = relations(searchEngines, ({ one, many }) => ({
+ integration: one(integrations, {
+ fields: [searchEngines.integrationId],
+ references: [integrations.id],
+ }),
+ usersWithDefault: many(users),
+}));
+
+export const itemLayoutRelations = relations(itemLayouts, ({ one }) => ({
+ item: one(items, {
+ fields: [itemLayouts.itemId],
+ references: [items.id],
+ }),
+ section: one(sections, {
+ fields: [itemLayouts.sectionId],
+ references: [sections.id],
+ }),
+ layout: one(layouts, {
+ fields: [itemLayouts.layoutId],
+ references: [layouts.id],
+ }),
+}));
+
+export const sectionLayoutRelations = relations(sectionLayouts, ({ one }) => ({
+ section: one(sections, {
+ fields: [sectionLayouts.sectionId],
+ references: [sections.id],
+ relationName: "sectionLayoutRelations__section__sectionId",
+ }),
+ layout: one(layouts, {
+ fields: [sectionLayouts.layoutId],
+ references: [layouts.id],
+ }),
+ parentSection: one(sections, {
+ fields: [sectionLayouts.parentSectionId],
+ references: [sections.id],
+ relationName: "sectionLayoutRelations__section__parentSectionId",
+ }),
+}));
+
+export const layoutRelations = relations(layouts, ({ one, many }) => ({
+ items: many(itemLayouts),
+ sections: many(sectionLayouts),
+ board: one(boards, {
+ fields: [layouts.boardId],
+ references: [boards.id],
+ }),
+}));
diff --git a/packages/db/test/postgresql-migration.spec.ts b/packages/db/test/postgresql-migration.spec.ts
new file mode 100644
index 000000000..40dd28e46
--- /dev/null
+++ b/packages/db/test/postgresql-migration.spec.ts
@@ -0,0 +1,46 @@
+import path from "path";
+import { PostgreSqlContainer } from "@testcontainers/postgresql";
+import { drizzle } from "drizzle-orm/node-postgres";
+import { migrate } from "drizzle-orm/node-postgres/migrator";
+import { Pool } from "pg";
+import { describe, test } from "vitest";
+
+import * as pgSchema from "../schema/postgresql";
+
+describe("PostgreSql Migration", () => {
+ test("should add all tables and keys specified in migration files", async () => {
+ const container = new PostgreSqlContainer("postgres:latest");
+ const postgreSqlContainer = await container.start();
+
+ const pool = new Pool({
+ user: postgreSqlContainer.getUsername(),
+ database: postgreSqlContainer.getDatabase(),
+ password: postgreSqlContainer.getPassword(),
+ port: postgreSqlContainer.getPort(),
+ host: postgreSqlContainer.getHost(),
+ keepAlive: true,
+ max: 0,
+ idleTimeoutMillis: 60000,
+ allowExitOnIdle: false,
+ });
+
+ const database = drizzle({
+ schema: pgSchema,
+ casing: "snake_case",
+ client: pool,
+ });
+
+ // Run migrations and check if it works
+ await migrate(database, {
+ migrationsFolder: path.join(__dirname, "..", "migrations", "postgresql"),
+ });
+
+ // Check if users table exists
+ await database.query.users.findMany();
+
+ // Close the pool to release resources
+ await pool.end();
+ // Stop the container
+ await postgreSqlContainer.stop();
+ }, 40_000);
+});
diff --git a/packages/db/test/schema.spec.ts b/packages/db/test/schema.spec.ts
index 3de1cd5a9..246219547 100644
--- a/packages/db/test/schema.spec.ts
+++ b/packages/db/test/schema.spec.ts
@@ -1,15 +1,17 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import type { Column, InferSelectModel } from "drizzle-orm";
import type { ForeignKey as MysqlForeignKey, MySqlTableWithColumns } from "drizzle-orm/mysql-core";
+import type { PgTableWithColumns, ForeignKey as PostgresqlForeignKey } from "drizzle-orm/pg-core";
import type { ForeignKey as SqliteForeignKey, SQLiteTableWithColumns } from "drizzle-orm/sqlite-core";
import { expect, expectTypeOf, test } from "vitest";
import { objectEntries } from "@homarr/common";
import * as mysqlSchema from "../schema/mysql";
+import * as postgresqlSchema from "../schema/postgresql";
import * as sqliteSchema from "../schema/sqlite";
-// We need the following two types as there is currently no support for Buffer in mysql and
+// We need the following three types as there is currently no support for Buffer in mysql & pg and
// so we use a custom type which results in the config beeing different
type FixedMysqlConfig = {
[key in keyof MysqlConfig]: {
@@ -22,6 +24,22 @@ type FixedMysqlConfig = {
};
};
+type FixedPostgresqlConfig = {
+ [key in keyof PostgreisqlConfig]: {
+ [column in keyof PostgreisqlConfig[key]]: {
+ [property in Exclude<
+ keyof PostgreisqlConfig[key][column],
+ "dataType" | "data"
+ >]: PostgreisqlConfig[key][column][property];
+ } & {
+ dataType: PostgreisqlConfig[key][column]["data"] extends Buffer
+ ? "buffer"
+ : PostgreisqlConfig[key][column]["dataType"];
+ data: PostgreisqlConfig[key][column]["data"] extends Buffer ? Buffer : PostgreisqlConfig[key][column]["data"];
+ };
+ };
+};
+
type FixedSqliteConfig = {
[key in keyof SqliteConfig]: {
[column in keyof SqliteConfig[key]]: {
@@ -117,6 +135,91 @@ test("schemas should match", () => {
});
});
+test("schemas should match for postgresql", () => {
+ expectTypeOf().toEqualTypeOf();
+ expectTypeOf().toEqualTypeOf();
+ expectTypeOf().toEqualTypeOf();
+ expectTypeOf().toEqualTypeOf();
+
+ objectEntries(sqliteSchema).forEach(([tableName, sqliteTable]) => {
+ // keys of sqliteSchema and postgresqlSchema are the same, so we can safely use tableName as key
+ // skipcq: JS-E1007
+ const postgresqlTable = postgresqlSchema[tableName];
+ Object.entries(sqliteTable).forEach(([columnName, sqliteColumn]: [string, object]) => {
+ if (!("isUnique" in sqliteColumn)) return;
+ if (!("uniqueName" in sqliteColumn)) return;
+ if (!("primary" in sqliteColumn)) return;
+
+ const postgresqlColumn = postgresqlTable[columnName as keyof typeof postgresqlTable] as object;
+ if (!("isUnique" in postgresqlColumn)) return;
+ if (!("uniqueName" in postgresqlColumn)) return;
+ if (!("primary" in postgresqlColumn)) return;
+
+ expect(
+ sqliteColumn.isUnique,
+ `expect unique of column ${columnName} in table ${tableName} to be the same for both schemas`,
+ ).toEqual(postgresqlColumn.isUnique);
+ expect(
+ sqliteColumn.uniqueName,
+ `expect uniqueName of column ${columnName} in table ${tableName} to be the same for both schemas`,
+ ).toEqual(postgresqlColumn.uniqueName);
+ expect(
+ sqliteColumn.primary,
+ `expect primary of column ${columnName} in table ${tableName} to be the same for both schemas`,
+ ).toEqual(postgresqlColumn.primary);
+ });
+
+ const sqliteForeignKeys = sqliteTable[Symbol.for("drizzle:SQLiteInlineForeignKeys") as keyof typeof sqliteTable] as
+ | SqliteForeignKey[]
+ | undefined;
+ const postgresqlForeignKeys = postgresqlTable[
+ Symbol.for("drizzle:PgInlineForeignKeys") as keyof typeof postgresqlTable
+ ] as PostgresqlForeignKey[] | undefined;
+ if (!sqliteForeignKeys && !postgresqlForeignKeys) return;
+
+ expect(postgresqlForeignKeys, `postgresql foreign key for ${tableName} to be defined`).toBeDefined();
+ expect(sqliteForeignKeys, `sqlite foreign key for ${tableName} to be defined`).toBeDefined();
+
+ expect(
+ sqliteForeignKeys!.length,
+ `expect number of foreign keys in table ${tableName} to be the same for both schemas`,
+ ).toEqual(postgresqlForeignKeys?.length);
+
+ sqliteForeignKeys?.forEach((sqliteForeignKey) => {
+ sqliteForeignKey.getName();
+ const postgresqlForeignKey = postgresqlForeignKeys!.find((key) => key.getName() === sqliteForeignKey.getName());
+ expect(
+ postgresqlForeignKey,
+ `expect foreign key ${sqliteForeignKey.getName()} to be defined in postgresql schema`,
+ ).toBeDefined();
+
+ // In PostgreSql, onDelete is "no action" by default, so it is treated as undefined to match Sqlite.
+ expect(
+ sqliteForeignKey.onDelete,
+ `expect foreign key (${sqliteForeignKey.getName()}) onDelete to be the same for both schemas`,
+ ).toEqual(postgresqlForeignKey!.onDelete === "no action" ? undefined : postgresqlForeignKey!.onDelete);
+
+ // In PostgreSql, onUpdate is "no action" by default, so it is treated as undefined to match Sqlite.
+ expect(
+ sqliteForeignKey.onUpdate,
+ `expect foreign key (${sqliteForeignKey.getName()}) onUpdate to be the same for both schemas`,
+ ).toEqual(postgresqlForeignKey!.onUpdate === "no action" ? undefined : postgresqlForeignKey!.onUpdate);
+
+ sqliteForeignKey.reference().foreignColumns.forEach((column) => {
+ expect(
+ postgresqlForeignKey!.reference().foreignColumns.map((column) => column.name),
+ `expect foreign key (${sqliteForeignKey.getName()}) columns to be the same for both schemas`,
+ ).toContainEqual(column.name);
+ });
+
+ expect(
+ Object.keys(sqliteForeignKey.reference().foreignTable),
+ `expect foreign key (${sqliteForeignKey.getName()}) table to be the same for both schemas`,
+ ).toEqual(Object.keys(postgresqlForeignKey!.reference().foreignTable).filter((key) => key !== "enableRLS"));
+ });
+ });
+});
+
type SqliteTables = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[K in keyof typeof sqliteSchema]: (typeof sqliteSchema)[K] extends SQLiteTableWithColumns
@@ -130,6 +233,13 @@ type MysqlTables = {
: never;
};
+type PostgresqlTables = {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ [K in keyof typeof postgresqlSchema]: (typeof postgresqlSchema)[K] extends PgTableWithColumns
+ ? InferSelectModel<(typeof postgresqlSchema)[K]>
+ : never;
+};
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type InferColumnConfig> =
T extends Column ? Omit : never;
@@ -155,3 +265,14 @@ type MysqlConfig = {
}
: never;
};
+
+type PostgreisqlConfig = {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ [K in keyof typeof postgresqlSchema]: (typeof postgresqlSchema)[K] extends PgTableWithColumns
+ ? {
+ [C in keyof (typeof postgresqlSchema)[K]["_"]["config"]["columns"]]: InferColumnConfig<
+ (typeof postgresqlSchema)[K]["_"]["config"]["columns"][C]
+ >;
+ }
+ : never;
+};
diff --git a/packages/db/transactions.ts b/packages/db/transactions.ts
index e7da4ae16..1ad35b3dd 100644
--- a/packages/db/transactions.ts
+++ b/packages/db/transactions.ts
@@ -1,11 +1,10 @@
+import { isMysql, isPostgresql } from "./collection";
import type { HomarrDatabase, HomarrDatabaseMysql } from "./driver";
-import { env } from "./env";
-import * as mysqlSchema from "./schema/mysql";
-
-type MysqlSchema = typeof mysqlSchema;
+import type { MySqlSchema } from "./schema";
+import * as schema from "./schema";
interface HandleTransactionInput {
- handleAsync: (db: HomarrDatabaseMysql, schema: MysqlSchema) => Promise;
+ handleAsync: (db: HomarrDatabaseMysql, schema: MySqlSchema) => Promise;
handleSync: (db: HomarrDatabase) => void;
}
@@ -15,10 +14,10 @@ interface HandleTransactionInput {
* But it can also generally be used when dealing with different database drivers.
*/
export const handleDiffrentDbDriverOperationsAsync = async (db: HomarrDatabase, input: HandleTransactionInput) => {
- if (env.DB_DRIVER !== "mysql2") {
+ if (isMysql() || isPostgresql()) {
+ // Schema type is always the correct one based on env variables
+ await input.handleAsync(db as unknown as HomarrDatabaseMysql, schema as unknown as MySqlSchema);
+ } else {
input.handleSync(db);
- return;
}
-
- await input.handleAsync(db as unknown as HomarrDatabaseMysql, mysqlSchema);
};
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4b3f0ac35..c7ea78cf1 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1140,6 +1140,9 @@ importers:
'@testcontainers/mysql':
specifier: ^11.5.1
version: 11.5.1
+ '@testcontainers/postgresql':
+ specifier: ^11.4.0
+ version: 11.4.0
better-sqlite3:
specifier: ^12.2.0
version: 12.2.0
@@ -1151,13 +1154,16 @@ importers:
version: 0.31.4
drizzle-orm:
specifier: ^0.44.4
- version: 0.44.4(@libsql/client-wasm@0.14.0)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.2.0)(gel@2.0.0)(mysql2@3.14.3)
+ version: 0.44.4(@libsql/client-wasm@0.14.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.15.4)(better-sqlite3@12.2.0)(gel@2.0.0)(mysql2@3.14.3)(pg@8.16.3)
drizzle-zod:
specifier: ^0.8.3
- version: 0.8.3(drizzle-orm@0.44.4(@libsql/client-wasm@0.14.0)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.2.0)(gel@2.0.0)(mysql2@3.14.3))(zod@4.0.17)
+ version: 0.8.3(drizzle-orm@0.44.4(@libsql/client-wasm@0.14.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.15.4)(better-sqlite3@12.2.0)(gel@2.0.0)(mysql2@3.14.3)(pg@8.16.3))(zod@4.0.17)
mysql2:
specifier: 3.14.3
version: 3.14.3
+ pg:
+ specifier: ^8.16.3
+ version: 8.16.3
superjson:
specifier: 2.2.2
version: 2.2.2
@@ -1174,6 +1180,9 @@ importers:
'@types/better-sqlite3':
specifier: 7.6.13
version: 7.6.13
+ '@types/pg':
+ specifier: ^8.15.4
+ version: 8.15.4
dotenv-cli:
specifier: ^10.0.0
version: 10.0.0
@@ -4111,6 +4120,9 @@ packages:
'@testcontainers/mysql@11.5.1':
resolution: {integrity: sha512-znonzVMlcCLQ6t7zDH0TOEcbGkz6iwIk5x5ZP5GqEdN0z3GLUu/jEF7yQUtfCY+PbVnCkigRg4WYS4bt6+zhyA==}
+ '@testcontainers/postgresql@11.4.0':
+ resolution: {integrity: sha512-WiKsz3Np5twNZGp2kgatqGaE/KqNR271CPwvIgAvFyN7E581P34glQljM4iLfxdv1XpzVYGWRO6PbQAVDbehBQ==}
+
'@testcontainers/redis@11.5.1':
resolution: {integrity: sha512-ThGaUPUCFW4Vwmx6kfPYhhTQjq/3UXJQrU/xxiYLqgvFJNtvtYlWmzXrwORLhPkkqnoFUnfFaX3u9u1GnrlDkw==}
@@ -4571,6 +4583,9 @@ packages:
'@types/normalize-package-data@2.4.4':
resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==}
+ '@types/pg@8.15.4':
+ resolution: {integrity: sha512-I6UNVBAoYbvuWkkU3oosC8yxqH21f4/Jc4DK71JLG3dT2mdlGe1z+ep/LQGXaKaOgcvUrsQoPRqfgtMcvZiJhg==}
+
'@types/prismjs@1.26.5':
resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==}
@@ -8469,6 +8484,40 @@ packages:
resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==}
engines: {node: '>= 14.16'}
+ pg-cloudflare@1.2.7:
+ resolution: {integrity: sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==}
+
+ pg-connection-string@2.9.1:
+ resolution: {integrity: sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==}
+
+ pg-int8@1.0.1:
+ resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
+ engines: {node: '>=4.0.0'}
+
+ pg-pool@3.10.1:
+ resolution: {integrity: sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==}
+ peerDependencies:
+ pg: '>=8.0'
+
+ pg-protocol@1.10.3:
+ resolution: {integrity: sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==}
+
+ pg-types@2.2.0:
+ resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==}
+ engines: {node: '>=4'}
+
+ pg@8.16.3:
+ resolution: {integrity: sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==}
+ engines: {node: '>= 16.0.0'}
+ peerDependencies:
+ pg-native: '>=3.0.1'
+ peerDependenciesMeta:
+ pg-native:
+ optional: true
+
+ pgpass@1.0.5:
+ resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==}
+
picocolors@1.0.1:
resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==}
@@ -8575,6 +8624,22 @@ packages:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
+ postgres-array@2.0.0:
+ resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
+ engines: {node: '>=4'}
+
+ postgres-bytea@1.0.0:
+ resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==}
+ engines: {node: '>=0.10.0'}
+
+ postgres-date@1.0.7:
+ resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==}
+ engines: {node: '>=0.10.0'}
+
+ postgres-interval@1.2.0:
+ resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
+ engines: {node: '>=0.10.0'}
+
preact-render-to-string@6.5.11:
resolution: {integrity: sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==}
peerDependencies:
@@ -12652,6 +12717,13 @@ snapshots:
- bare-buffer
- supports-color
+ '@testcontainers/postgresql@11.4.0':
+ dependencies:
+ testcontainers: 11.5.1
+ transitivePeerDependencies:
+ - bare-buffer
+ - supports-color
+
'@testcontainers/redis@11.5.1':
dependencies:
testcontainers: 11.5.1
@@ -13166,6 +13238,12 @@ snapshots:
'@types/normalize-package-data@2.4.4': {}
+ '@types/pg@8.15.4':
+ dependencies:
+ '@types/node': 22.17.2
+ pg-protocol: 1.10.3
+ pg-types: 2.2.0
+
'@types/prismjs@1.26.5': {}
'@types/qs@6.9.16': {}
@@ -14683,17 +14761,19 @@ snapshots:
transitivePeerDependencies:
- supports-color
- drizzle-orm@0.44.4(@libsql/client-wasm@0.14.0)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.2.0)(gel@2.0.0)(mysql2@3.14.3):
+ drizzle-orm@0.44.4(@libsql/client-wasm@0.14.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.15.4)(better-sqlite3@12.2.0)(gel@2.0.0)(mysql2@3.14.3)(pg@8.16.3):
optionalDependencies:
'@libsql/client-wasm': 0.14.0
'@types/better-sqlite3': 7.6.13
+ '@types/pg': 8.15.4
better-sqlite3: 12.2.0
gel: 2.0.0
mysql2: 3.14.3
+ pg: 8.16.3
- drizzle-zod@0.8.3(drizzle-orm@0.44.4(@libsql/client-wasm@0.14.0)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.2.0)(gel@2.0.0)(mysql2@3.14.3))(zod@4.0.17):
+ drizzle-zod@0.8.3(drizzle-orm@0.44.4(@libsql/client-wasm@0.14.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.15.4)(better-sqlite3@12.2.0)(gel@2.0.0)(mysql2@3.14.3)(pg@8.16.3))(zod@4.0.17):
dependencies:
- drizzle-orm: 0.44.4(@libsql/client-wasm@0.14.0)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.2.0)(gel@2.0.0)(mysql2@3.14.3)
+ drizzle-orm: 0.44.4(@libsql/client-wasm@0.14.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.15.4)(better-sqlite3@12.2.0)(gel@2.0.0)(mysql2@3.14.3)(pg@8.16.3)
zod: 4.0.17
dunder-proto@1.0.1:
@@ -17576,6 +17656,41 @@ snapshots:
pathval@2.0.0: {}
+ pg-cloudflare@1.2.7:
+ optional: true
+
+ pg-connection-string@2.9.1: {}
+
+ pg-int8@1.0.1: {}
+
+ pg-pool@3.10.1(pg@8.16.3):
+ dependencies:
+ pg: 8.16.3
+
+ pg-protocol@1.10.3: {}
+
+ pg-types@2.2.0:
+ dependencies:
+ pg-int8: 1.0.1
+ postgres-array: 2.0.0
+ postgres-bytea: 1.0.0
+ postgres-date: 1.0.7
+ postgres-interval: 1.2.0
+
+ pg@8.16.3:
+ dependencies:
+ pg-connection-string: 2.9.1
+ pg-pool: 3.10.1(pg@8.16.3)
+ pg-protocol: 1.10.3
+ pg-types: 2.2.0
+ pgpass: 1.0.5
+ optionalDependencies:
+ pg-cloudflare: 1.2.7
+
+ pgpass@1.0.5:
+ dependencies:
+ split2: 4.2.0
+
picocolors@1.0.1: {}
picocolors@1.1.1: {}
@@ -17684,6 +17799,16 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
+ postgres-array@2.0.0: {}
+
+ postgres-bytea@1.0.0: {}
+
+ postgres-date@1.0.7: {}
+
+ postgres-interval@1.2.0:
+ dependencies:
+ xtend: 4.0.2
+
preact-render-to-string@6.5.11(preact@10.24.3):
dependencies:
preact: 10.24.3