Merge branch 'dev-1' into dev

This commit is contained in:
Wuesteon 2025-12-05 17:57:26 +01:00
commit d41d060bb3
1770 changed files with 168028 additions and 31031 deletions

View file

@ -0,0 +1,38 @@
import { drizzle } from 'drizzle-orm/postgres-js';
import * as schema from './schema';
// Use require for postgres to avoid ESM/CommonJS interop issues
// eslint-disable-next-line @typescript-eslint/no-var-requires
const postgres = require('postgres');
let connection: ReturnType<typeof postgres> | null = null;
let db: ReturnType<typeof drizzle> | null = null;
export function getConnection(databaseUrl: string) {
if (!connection) {
connection = postgres(databaseUrl, {
max: 10,
idle_timeout: 20,
connect_timeout: 10,
});
}
return connection;
}
export function getDb(databaseUrl: string) {
if (!db) {
const conn = getConnection(databaseUrl);
db = drizzle(conn, { schema });
}
return db;
}
export async function closeConnection() {
if (connection) {
await connection.end();
connection = null;
db = null;
}
}
export type Database = ReturnType<typeof getDb>;

View file

@ -0,0 +1,30 @@
import { Module, Global } from '@nestjs/common';
import type { OnModuleDestroy } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { getDb, closeConnection } from './connection';
import type { Database } from './connection';
export const DATABASE_CONNECTION = 'DATABASE_CONNECTION';
@Global()
@Module({
providers: [
{
provide: DATABASE_CONNECTION,
useFactory: (configService: ConfigService): Database => {
const databaseUrl = configService.get<string>('DATABASE_URL');
if (!databaseUrl) {
throw new Error('DATABASE_URL environment variable is not set');
}
return getDb(databaseUrl);
},
inject: [ConfigService],
},
],
exports: [DATABASE_CONNECTION],
})
export class DatabaseModule implements OnModuleDestroy {
async onModuleDestroy() {
await closeConnection();
}
}

View file

@ -0,0 +1,33 @@
import { pgTable, uuid, primaryKey } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
import { files } from './files.schema';
import { tags } from './tags.schema';
export const fileTags = pgTable(
'file_tags',
{
fileId: uuid('file_id')
.references(() => files.id, { onDelete: 'cascade' })
.notNull(),
tagId: uuid('tag_id')
.references(() => tags.id, { onDelete: 'cascade' })
.notNull(),
},
(table) => ({
pk: primaryKey({ columns: [table.fileId, table.tagId] }),
})
);
export const fileTagsRelations = relations(fileTags, ({ one }) => ({
file: one(files, {
fields: [fileTags.fileId],
references: [files.id],
}),
tag: one(tags, {
fields: [fileTags.tagId],
references: [tags.id],
}),
}));
export type FileTag = typeof fileTags.$inferSelect;
export type NewFileTag = typeof fileTags.$inferInsert;

View file

@ -0,0 +1,36 @@
import { pgTable, uuid, varchar, timestamp, bigint, integer, text } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
import { files } from './files.schema';
export const fileVersions = pgTable('file_versions', {
id: uuid('id').primaryKey().defaultRandom(),
fileId: uuid('file_id')
.references(() => files.id, { onDelete: 'cascade' })
.notNull(),
// Version info
versionNumber: integer('version_number').notNull(),
// Storage info for this version
storagePath: varchar('storage_path', { length: 1000 }).notNull(),
storageKey: varchar('storage_key', { length: 500 }).notNull(),
size: bigint('size', { mode: 'number' }).notNull(),
checksum: varchar('checksum', { length: 64 }),
// Metadata
comment: text('comment'), // Optional version comment
createdBy: varchar('created_by', { length: 255 }).notNull(),
// Timestamps
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
});
export const fileVersionsRelations = relations(fileVersions, ({ one }) => ({
file: one(files, {
fields: [fileVersions.fileId],
references: [files.id],
}),
}));
export type FileVersion = typeof fileVersions.$inferSelect;
export type NewFileVersion = typeof fileVersions.$inferInsert;

View file

@ -0,0 +1,56 @@
import {
pgTable,
uuid,
varchar,
text,
timestamp,
bigint,
boolean,
integer,
} from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
import { folders } from './folders.schema';
export const files = pgTable('files', {
id: uuid('id').primaryKey().defaultRandom(),
userId: varchar('user_id', { length: 255 }).notNull(),
// File metadata
name: varchar('name', { length: 500 }).notNull(),
originalName: varchar('original_name', { length: 500 }).notNull(),
mimeType: varchar('mime_type', { length: 255 }).notNull(),
size: bigint('size', { mode: 'number' }).notNull(),
// Storage location
storagePath: varchar('storage_path', { length: 1000 }).notNull(),
storageKey: varchar('storage_key', { length: 500 }).notNull().unique(),
// Hierarchy
parentFolderId: uuid('parent_folder_id').references(() => folders.id, { onDelete: 'set null' }),
// File properties
checksum: varchar('checksum', { length: 64 }), // SHA-256
thumbnailPath: varchar('thumbnail_path', { length: 500 }),
// Versioning
currentVersion: integer('current_version').default(1).notNull(),
// Status flags
isFavorite: boolean('is_favorite').default(false).notNull(),
isDeleted: boolean('is_deleted').default(false).notNull(),
deletedAt: timestamp('deleted_at', { withTimezone: true }),
// Timestamps
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
});
export const filesRelations = relations(files, ({ one }) => ({
parentFolder: one(folders, {
fields: [files.parentFolderId],
references: [folders.id],
}),
}));
export type File = typeof files.$inferSelect;
export type NewFile = typeof files.$inferInsert;

View file

@ -0,0 +1,41 @@
import { pgTable, uuid, varchar, timestamp, boolean, text, integer } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
export const folders = pgTable('folders', {
id: uuid('id').primaryKey().defaultRandom(),
userId: varchar('user_id', { length: 255 }).notNull(),
// Folder metadata
name: varchar('name', { length: 255 }).notNull(),
color: varchar('color', { length: 20 }),
description: text('description'),
// Hierarchy (self-referencing)
parentFolderId: uuid('parent_folder_id'),
// Path for efficient queries (e.g., "/root-uuid/parent-uuid/current-uuid")
path: text('path').notNull(),
depth: integer('depth').default(0).notNull(),
// Status flags
isFavorite: boolean('is_favorite').default(false).notNull(),
isDeleted: boolean('is_deleted').default(false).notNull(),
deletedAt: timestamp('deleted_at', { withTimezone: true }),
// Timestamps
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
});
// Self-referencing relation
export const foldersRelations = relations(folders, ({ one, many }) => ({
parentFolder: one(folders, {
fields: [folders.parentFolderId],
references: [folders.id],
relationName: 'folder_parent',
}),
childFolders: many(folders, { relationName: 'folder_parent' }),
}));
export type Folder = typeof folders.$inferSelect;
export type NewFolder = typeof folders.$inferInsert;

View file

@ -0,0 +1,17 @@
// Folders (must be first due to self-reference)
export * from './folders.schema';
// Files (references folders)
export * from './files.schema';
// File versions (references files)
export * from './file-versions.schema';
// Tags
export * from './tags.schema';
// File-Tags junction (references files and tags)
export * from './file-tags.schema';
// Shares (references files and folders)
export * from './shares.schema';

View file

@ -0,0 +1,50 @@
import { pgTable, uuid, varchar, timestamp, boolean, integer, pgEnum } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
import { files } from './files.schema';
import { folders } from './folders.schema';
export const shareTypeEnum = pgEnum('share_type', ['file', 'folder']);
export const shareAccessEnum = pgEnum('share_access', ['view', 'edit', 'download']);
export const shares = pgTable('shares', {
id: uuid('id').primaryKey().defaultRandom(),
userId: varchar('user_id', { length: 255 }).notNull(), // Owner
// Share target (one of these will be set)
fileId: uuid('file_id').references(() => files.id, { onDelete: 'cascade' }),
folderId: uuid('folder_id').references(() => folders.id, { onDelete: 'cascade' }),
shareType: shareTypeEnum('share_type').notNull(),
// Share link
shareToken: varchar('share_token', { length: 64 }).notNull().unique(),
accessLevel: shareAccessEnum('access_level').default('view').notNull(),
// Security
password: varchar('password', { length: 255 }), // Hashed password
maxDownloads: integer('max_downloads'),
downloadCount: integer('download_count').default(0).notNull(),
// Expiration
expiresAt: timestamp('expires_at', { withTimezone: true }),
// Status
isActive: boolean('is_active').default(true).notNull(),
// Timestamps
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
lastAccessedAt: timestamp('last_accessed_at', { withTimezone: true }),
});
export const sharesRelations = relations(shares, ({ one }) => ({
file: one(files, {
fields: [shares.fileId],
references: [files.id],
}),
folder: one(folders, {
fields: [shares.folderId],
references: [folders.id],
}),
}));
export type Share = typeof shares.$inferSelect;
export type NewShare = typeof shares.$inferInsert;

View file

@ -0,0 +1,20 @@
import { pgTable, uuid, varchar, timestamp } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
import { fileTags } from './file-tags.schema';
export const tags = pgTable('tags', {
id: uuid('id').primaryKey().defaultRandom(),
userId: varchar('user_id', { length: 255 }).notNull(),
name: varchar('name', { length: 50 }).notNull(),
color: varchar('color', { length: 20 }),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
});
export const tagsRelations = relations(tags, ({ many }) => ({
fileTags: many(fileTags),
}));
export type Tag = typeof tags.$inferSelect;
export type NewTag = typeof tags.$inferInsert;