From 94ba46cc2d1e6eecbad6518448ff9b53806897c7 Mon Sep 17 00:00:00 2001 From: creations Date: Thu, 6 Mar 2025 13:57:43 -0500 Subject: [PATCH] remove all drops from sql files, add triggers and functions for updated_at, added files and folder tables, update some types --- config/sql/files.ts | 85 +++++++++++++++++++++++++++++++++ config/sql/folders.ts | 75 +++++++++++++++++++++++++++++ config/sql/invites.ts | 25 +--------- config/sql/settings.ts | 87 +++++++++++++++++++++------------- config/sql/users.ts | 53 +++++++-------------- src/index.ts | 24 ++++++++-- src/routes/api/files/upload.ts | 0 types/bun.d.ts | 4 +- types/char.d.ts | 2 + types/config.d.ts | 8 ++++ types/file.d.ts | 33 +++++++++++++ 11 files changed, 295 insertions(+), 101 deletions(-) create mode 100644 config/sql/files.ts create mode 100644 config/sql/folders.ts create mode 100644 src/routes/api/files/upload.ts create mode 100644 types/file.d.ts diff --git a/config/sql/files.ts b/config/sql/files.ts new file mode 100644 index 0000000..438111c --- /dev/null +++ b/config/sql/files.ts @@ -0,0 +1,85 @@ +import { logger } from "@helpers/logger"; +import { type ReservedSQL, sql } from "bun"; + +export const order: number = 5; + +export async function createTable(reservation?: ReservedSQL): Promise { + let selfReservation: boolean = false; + + if (!reservation) { + reservation = await sql.reserve(); + selfReservation = true; + } + + try { + await reservation` + CREATE TABLE IF NOT EXISTS files ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + owner UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + folder UUID DEFAULT NULL REFERENCES folders(id) ON DELETE SET NULL, + + name VARCHAR(255) NOT NULL, + original_name VARCHAR(255), + mime_type VARCHAR(255) NOT NULL, + size BIGINT NOT NULL, + + views INTEGER DEFAULT 0, + max_views INTEGER DEFAULT 1, + password TEXT DEFAULT NULL, + favorite BOOLEAN DEFAULT FALSE, + tags TEXT[] DEFAULT ARRAY[]::TEXT[], + thumbnail BOOLEAN NOT NULL DEFAULT FALSE, + + created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL, + expires_at TIMESTAMPTZ DEFAULT NULL + ); + `; + + const functionExists: { exists: boolean }[] = await reservation` + SELECT EXISTS ( + SELECT 1 FROM pg_proc + JOIN pg_namespace ON pg_proc.pronamespace = pg_namespace.oid + WHERE proname = 'update_files_updated_at' AND nspname = 'public' + ); + `; + + if (!functionExists[0].exists) { + await reservation` + CREATE FUNCTION update_files_updated_at() + RETURNS TRIGGER AS $$ + BEGIN + NEW.updated_at = NOW(); + RETURN NEW; + END; + $$ LANGUAGE plpgsql; + `; + } + + const triggerExists: { exists: boolean }[] = await reservation` + SELECT EXISTS ( + SELECT 1 FROM pg_trigger + WHERE tgname = 'trigger_update_files_updated_at' + ); + `; + + if (!triggerExists[0].exists) { + await reservation` + CREATE TRIGGER trigger_update_files_updated_at + BEFORE UPDATE ON files + FOR EACH ROW + EXECUTE FUNCTION update_files_updated_at(); + `; + } + } catch (error) { + logger.error([ + "Could not create the files table or trigger:", + error as Error, + ]); + throw error; + } finally { + if (selfReservation) { + reservation.release(); + } + } +} diff --git a/config/sql/folders.ts b/config/sql/folders.ts new file mode 100644 index 0000000..1946c4a --- /dev/null +++ b/config/sql/folders.ts @@ -0,0 +1,75 @@ +import { logger } from "@helpers/logger"; +import { type ReservedSQL, sql } from "bun"; + +export const order: number = 4; + +export async function createTable(reservation?: ReservedSQL): Promise { + let selfReservation: boolean = false; + + if (!reservation) { + reservation = await sql.reserve(); + selfReservation = true; + } + + try { + await reservation` + CREATE TABLE IF NOT EXISTS folders ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + owner UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + + name VARCHAR(255) NOT NULL, + public BOOLEAN NOT NULL DEFAULT FALSE, + allow_uploads BOOLEAN NOT NULL DEFAULT FALSE, + + created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL + ); + `; + + const functionExists: { exists: boolean }[] = await reservation` + SELECT EXISTS ( + SELECT 1 FROM pg_proc + JOIN pg_namespace ON pg_proc.pronamespace = pg_namespace.oid + WHERE proname = 'update_folders_updated_at' AND nspname = 'public' + ); + `; + + if (!functionExists[0].exists) { + await reservation` + CREATE FUNCTION update_folders_updated_at() + RETURNS TRIGGER AS $$ + BEGIN + NEW.updated_at = NOW(); + RETURN NEW; + END; + $$ LANGUAGE plpgsql; + `; + } + + const triggerExists: { exists: boolean }[] = await reservation` + SELECT EXISTS ( + SELECT 1 FROM pg_trigger + WHERE tgname = 'trigger_update_folders_updated_at' + ); + `; + + if (!triggerExists[0].exists) { + await reservation` + CREATE TRIGGER trigger_update_folders_updated_at + BEFORE UPDATE ON folders + FOR EACH ROW + EXECUTE FUNCTION update_folders_updated_at(); + `; + } + } catch (error) { + logger.error([ + "Could not create the folders table or trigger:", + error as Error, + ]); + throw error; + } finally { + if (selfReservation) { + reservation.release(); + } + } +} diff --git a/config/sql/invites.ts b/config/sql/invites.ts index 601cef6..e098d32 100644 --- a/config/sql/invites.ts +++ b/config/sql/invites.ts @@ -1,6 +1,8 @@ import { logger } from "@helpers/logger"; import { type ReservedSQL, sql } from "bun"; +export const order: number = 3; + export async function createTable(reservation?: ReservedSQL): Promise { let selfReservation: boolean = false; @@ -29,26 +31,3 @@ export async function createTable(reservation?: ReservedSQL): Promise { } } } - -export async function drop( - cascade: boolean, - reservation?: ReservedSQL, -): Promise { - let selfReservation: boolean = false; - - if (!reservation) { - reservation = await sql.reserve(); - selfReservation = true; - } - - try { - await reservation`DROP TABLE IF EXISTS invites ${cascade ? "CASCADE" : ""};`; - } catch (error) { - logger.error(["Could not drop the invites table:", error as Error]); - throw error; - } finally { - if (selfReservation) { - reservation.release(); - } - } -} diff --git a/config/sql/settings.ts b/config/sql/settings.ts index 1ddf8b1..d9b80f4 100644 --- a/config/sql/settings.ts +++ b/config/sql/settings.ts @@ -1,6 +1,8 @@ import { logger } from "@helpers/logger"; import { type ReservedSQL, sql } from "bun"; +export const order: number = 2; + const defaultSettings: Setting[] = [ { key: "default_role", value: "user" }, { key: "default_timezone", value: "UTC" }, @@ -21,45 +23,62 @@ export async function createTable(reservation?: ReservedSQL): Promise { try { await reservation` - CREATE TABLE IF NOT EXISTS settings ( - "key" VARCHAR(64) PRIMARY KEY NOT NULL UNIQUE, - "value" TEXT NOT NULL, - created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL, - updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL - );`; + CREATE TABLE IF NOT EXISTS settings ( + "key" VARCHAR(64) PRIMARY KEY NOT NULL UNIQUE, + "value" TEXT NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL, + updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL + ); + `; + + const functionExists: { exists: boolean }[] = await reservation` + SELECT EXISTS ( + SELECT 1 FROM pg_proc + JOIN pg_namespace ON pg_proc.pronamespace = pg_namespace.oid + WHERE proname = 'update_settings_updated_at' AND nspname = 'public' + ); + `; + + if (!functionExists[0].exists) { + await reservation` + CREATE FUNCTION update_settings_updated_at() + RETURNS TRIGGER AS $$ + BEGIN + NEW.updated_at = NOW(); + RETURN NEW; + END; + $$ LANGUAGE plpgsql; + `; + } + + const triggerExists: { exists: boolean }[] = await reservation` + SELECT EXISTS ( + SELECT 1 FROM pg_trigger + WHERE tgname = 'trigger_update_settings_updated_at' + ); + `; + + if (!triggerExists[0].exists) { + await reservation` + CREATE TRIGGER trigger_update_settings_updated_at + BEFORE UPDATE ON settings + FOR EACH ROW + EXECUTE FUNCTION update_settings_updated_at(); + `; + } for (const setting of defaultSettings) { await reservation` - INSERT INTO settings ("key", "value") - VALUES (${setting.key}, ${setting.value}) - ON CONFLICT ("key") - DO NOTHING;`; + INSERT INTO settings ("key", "value") + VALUES (${setting.key}, ${setting.value}) + ON CONFLICT ("key") DO NOTHING; + `; } } catch (error) { - logger.error(["Could not create the settings table:", error as Error]); - throw error; - } finally { - if (selfReservation) { - reservation.release(); - } - } -} - -export async function drop( - cascade: boolean, - reservation?: ReservedSQL, -): Promise { - let selfReservation: boolean = false; - - if (!reservation) { - reservation = await sql.reserve(); - selfReservation = true; - } - - try { - await reservation`DROP TABLE IF EXISTS settings ${cascade ? "CASCADE" : ""};`; - } catch (error) { - logger.error(["Could not drop the settings table:", error as Error]); + logger.error([ + "Could not create the settings table or trigger:", + error as Error, + ]); throw error; } finally { if (selfReservation) { diff --git a/config/sql/users.ts b/config/sql/users.ts index 118c6aa..385ddf4 100644 --- a/config/sql/users.ts +++ b/config/sql/users.ts @@ -1,6 +1,8 @@ import { logger } from "@helpers/logger"; import { type ReservedSQL, sql } from "bun"; +export const order: number = 1; + export async function createTable(reservation?: ReservedSQL): Promise { let selfReservation: boolean = false; @@ -11,20 +13,20 @@ export async function createTable(reservation?: ReservedSQL): Promise { try { await reservation` - CREATE TABLE IF NOT EXISTS users ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - authorization_token UUID NOT NULL UNIQUE DEFAULT gen_random_uuid(), - username VARCHAR(20) NOT NULL UNIQUE, - email VARCHAR(254) NOT NULL UNIQUE, - email_verified boolean NOT NULL DEFAULT false, - password TEXT NOT NULL, - avatar boolean NOT NULL DEFAULT false, - roles TEXT[] NOT NULL DEFAULT ARRAY['user'], - timezone VARCHAR(64) DEFAULT NULL, - invited_by UUID REFERENCES users(id) ON DELETE SET NULL, - created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL, - last_seen TIMESTAMPTZ DEFAULT NOW() NOT NULL - );`; + CREATE TABLE IF NOT EXISTS users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + authorization_token UUID NOT NULL UNIQUE DEFAULT gen_random_uuid(), + username VARCHAR(20) NOT NULL UNIQUE, + email VARCHAR(254) NOT NULL UNIQUE, + email_verified boolean NOT NULL DEFAULT false, + password TEXT NOT NULL, + avatar boolean NOT NULL DEFAULT false, + roles TEXT[] NOT NULL DEFAULT ARRAY['user'], + timezone VARCHAR(64) DEFAULT NULL, + invited_by UUID REFERENCES users(id) ON DELETE SET NULL, + created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL, + last_seen TIMESTAMPTZ DEFAULT NOW() NOT NULL + );`; } catch (error) { logger.error(["Could not create the users table:", error as Error]); throw error; @@ -35,29 +37,6 @@ export async function createTable(reservation?: ReservedSQL): Promise { } } -export async function drop( - cascade: boolean, - reservation?: ReservedSQL, -): Promise { - let selfReservation: boolean = false; - - if (!reservation) { - reservation = await sql.reserve(); - selfReservation = true; - } - - try { - await reservation`DROP TABLE IF EXISTS users ${cascade ? "CASCADE" : ""};`; - } catch (error) { - logger.error(["Could not drop the users table:", error as Error]); - throw error; - } finally { - if (selfReservation) { - reservation.release(); - } - } -} - // * Validation functions // ? should support non english characters but wont mess up the url diff --git a/src/index.ts b/src/index.ts index 3170c02..5af5367 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,12 +13,26 @@ async function initializeDatabase(): Promise { const sqlDir: string = resolve("config", "sql"); const files: string[] = await readdir(sqlDir); - const reservation: ReservedSQL = await sql.reserve(); - for (const file of files) { - if (file.endsWith(".ts")) { - const { createTable } = await import(resolve(sqlDir, file)); + const modules: Module[] = await Promise.all( + files + .filter((file: string): boolean => file.endsWith(".ts")) + .map(async (file: string): Promise => { + const module: Module["module"] = await import( + resolve(sqlDir, file) + ); + return { file, module }; + }), + ); - await createTable(reservation); + modules.sort( + (a: Module, b: Module): number => + (a.module.order ?? 0) - (b.module.order ?? 0), + ); + + const reservation: ReservedSQL = await sql.reserve(); + for (const { module } of modules) { + if (module.createTable) { + await module.createTable(reservation); } } diff --git a/src/routes/api/files/upload.ts b/src/routes/api/files/upload.ts new file mode 100644 index 0000000..e69de29 diff --git a/types/bun.d.ts b/types/bun.d.ts index 414c40f..3e80e42 100644 --- a/types/bun.d.ts +++ b/types/bun.d.ts @@ -6,10 +6,10 @@ type Params = Record; declare global { type BunServer = Server; - type ExtendedRequest = Request & { + interface ExtendedRequest extends Request { startPerf: number; query: Query; params: Params; session: UserSession | ApiUserSession | null; - }; + } } diff --git a/types/char.d.ts b/types/char.d.ts index 67ebf8c..e536f24 100644 --- a/types/char.d.ts +++ b/types/char.d.ts @@ -7,3 +7,5 @@ type DurationObject = { minutes: number; seconds: number; }; + +type UUID = `${string}-${string}-${string}-${string}-${string}`; diff --git a/types/config.d.ts b/types/config.d.ts index a265432..322a951 100644 --- a/types/config.d.ts +++ b/types/config.d.ts @@ -13,3 +13,11 @@ type Setting = { key: string; value: string; }; + +type Module = { + file: string; + module: { + order?: number; + createTable?: (reservation?: ReservedSQL) => Promise; + }; +}; diff --git a/types/file.d.ts b/types/file.d.ts new file mode 100644 index 0000000..eae4c2a --- /dev/null +++ b/types/file.d.ts @@ -0,0 +1,33 @@ +type File = { + id: UUID; + owner: UUID; + folder?: UUID | null; + + name: string; + original_name?: string | null; + mime_type: string; + size: number; + + views: number; + max_views: number; + password?: string | null; + favorite: boolean; + tags: string[]; + thumbnail: boolean; + + created_at: Date; + updated_at: Date; + expires_at?: Date | null; +}; + +type Folder = { + id: UUID; + owner: UUID; + + name: string; + public: boolean; + allow_uploads: boolean; + + created_at: Date; + updated_at: Date; +};