remove all drops from sql files, add triggers and functions for updated_at, added files and folder tables, update some types

This commit is contained in:
creations 2025-03-06 13:57:43 -05:00
parent 6fdc82dd49
commit 94ba46cc2d
Signed by: creations
GPG key ID: 8F553AA4320FC711
11 changed files with 295 additions and 101 deletions

85
config/sql/files.ts Normal file
View file

@ -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<void> {
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();
}
}
}

75
config/sql/folders.ts Normal file
View file

@ -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<void> {
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();
}
}
}

View file

@ -1,6 +1,8 @@
import { logger } from "@helpers/logger"; import { logger } from "@helpers/logger";
import { type ReservedSQL, sql } from "bun"; import { type ReservedSQL, sql } from "bun";
export const order: number = 3;
export async function createTable(reservation?: ReservedSQL): Promise<void> { export async function createTable(reservation?: ReservedSQL): Promise<void> {
let selfReservation: boolean = false; let selfReservation: boolean = false;
@ -29,26 +31,3 @@ export async function createTable(reservation?: ReservedSQL): Promise<void> {
} }
} }
} }
export async function drop(
cascade: boolean,
reservation?: ReservedSQL,
): Promise<void> {
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();
}
}
}

View file

@ -1,6 +1,8 @@
import { logger } from "@helpers/logger"; import { logger } from "@helpers/logger";
import { type ReservedSQL, sql } from "bun"; import { type ReservedSQL, sql } from "bun";
export const order: number = 2;
const defaultSettings: Setting[] = [ const defaultSettings: Setting[] = [
{ key: "default_role", value: "user" }, { key: "default_role", value: "user" },
{ key: "default_timezone", value: "UTC" }, { key: "default_timezone", value: "UTC" },
@ -26,40 +28,57 @@ export async function createTable(reservation?: ReservedSQL): Promise<void> {
"value" TEXT NOT NULL, "value" TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
updated_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) { for (const setting of defaultSettings) {
await reservation` await reservation`
INSERT INTO settings ("key", "value") INSERT INTO settings ("key", "value")
VALUES (${setting.key}, ${setting.value}) VALUES (${setting.key}, ${setting.value})
ON CONFLICT ("key") ON CONFLICT ("key") DO NOTHING;
DO NOTHING;`; `;
} }
} catch (error) { } catch (error) {
logger.error(["Could not create the settings table:", error as Error]); logger.error([
throw error; "Could not create the settings table or trigger:",
} finally { error as Error,
if (selfReservation) { ]);
reservation.release();
}
}
}
export async function drop(
cascade: boolean,
reservation?: ReservedSQL,
): Promise<void> {
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]);
throw error; throw error;
} finally { } finally {
if (selfReservation) { if (selfReservation) {

View file

@ -1,6 +1,8 @@
import { logger } from "@helpers/logger"; import { logger } from "@helpers/logger";
import { type ReservedSQL, sql } from "bun"; import { type ReservedSQL, sql } from "bun";
export const order: number = 1;
export async function createTable(reservation?: ReservedSQL): Promise<void> { export async function createTable(reservation?: ReservedSQL): Promise<void> {
let selfReservation: boolean = false; let selfReservation: boolean = false;
@ -35,29 +37,6 @@ export async function createTable(reservation?: ReservedSQL): Promise<void> {
} }
} }
export async function drop(
cascade: boolean,
reservation?: ReservedSQL,
): Promise<void> {
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 // * Validation functions
// ? should support non english characters but wont mess up the url // ? should support non english characters but wont mess up the url

View file

@ -13,12 +13,26 @@ async function initializeDatabase(): Promise<void> {
const sqlDir: string = resolve("config", "sql"); const sqlDir: string = resolve("config", "sql");
const files: string[] = await readdir(sqlDir); const files: string[] = await readdir(sqlDir);
const reservation: ReservedSQL = await sql.reserve(); const modules: Module[] = await Promise.all(
for (const file of files) { files
if (file.endsWith(".ts")) { .filter((file: string): boolean => file.endsWith(".ts"))
const { createTable } = await import(resolve(sqlDir, file)); .map(async (file: string): Promise<Module> => {
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);
} }
} }

View file

4
types/bun.d.ts vendored
View file

@ -6,10 +6,10 @@ type Params = Record<string, string>;
declare global { declare global {
type BunServer = Server; type BunServer = Server;
type ExtendedRequest = Request & { interface ExtendedRequest extends Request {
startPerf: number; startPerf: number;
query: Query; query: Query;
params: Params; params: Params;
session: UserSession | ApiUserSession | null; session: UserSession | ApiUserSession | null;
}; }
} }

2
types/char.d.ts vendored
View file

@ -7,3 +7,5 @@ type DurationObject = {
minutes: number; minutes: number;
seconds: number; seconds: number;
}; };
type UUID = `${string}-${string}-${string}-${string}-${string}`;

8
types/config.d.ts vendored
View file

@ -13,3 +13,11 @@ type Setting = {
key: string; key: string;
value: string; value: string;
}; };
type Module = {
file: string;
module: {
order?: number;
createTable?: (reservation?: ReservedSQL) => Promise<void>;
};
};

33
types/file.d.ts vendored Normal file
View file

@ -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;
};