From ac28dd14e4ef895fe29cbbc9e1136fa1a2962b89 Mon Sep 17 00:00:00 2001 From: Matiss Jurevics Date: Thu, 11 Dec 2025 16:15:00 +0000 Subject: [PATCH] feat: add videos table to schema and implement video upload route with metadata persistence --- Backend/db/schema.ts | 17 +- Backend/drizzle/0003_little_shard.sql | 17 ++ Backend/drizzle/meta/0003_snapshot.json | 238 ++++++++++++++++++++++++ Backend/drizzle/meta/_journal.json | 7 + Backend/routes/videos.ts | 30 +++ 5 files changed, 307 insertions(+), 2 deletions(-) create mode 100644 Backend/drizzle/0003_little_shard.sql create mode 100644 Backend/drizzle/meta/0003_snapshot.json diff --git a/Backend/db/schema.ts b/Backend/db/schema.ts index 3a53850..987ea7b 100644 --- a/Backend/db/schema.ts +++ b/Backend/db/schema.ts @@ -1,4 +1,4 @@ -import { pgTable, timestamp, uuid, varchar } from 'drizzle-orm/pg-core'; +import { pgTable, timestamp, uuid, varchar, text } from 'drizzle-orm/pg-core'; export const users = pgTable('users', { id: uuid('id').defaultRandom().primaryKey(), @@ -14,4 +14,17 @@ export const events = pgTable('events', { title: varchar('title', { length: 255 }).notNull(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), videoUrl: varchar('video_url', { length: 255 }).notNull().unique(), -}); \ No newline at end of file +}); + +export const videos = pgTable('videos', { + id: uuid('id').defaultRandom().primaryKey(), + userId: uuid('user_id').notNull().references(() => users.id), + objectKey: varchar('object_key', { length: 1024 }).notNull().unique(), + bucket: varchar('bucket', { length: 255 }).notNull(), + uploadUrl: text('upload_url').notNull(), + downloadUrl: text('download_url'), + status: varchar('status', { length: 32 }).notNull().default('pending'), + expiresAt: timestamp('expires_at', { withTimezone: true }), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), +}); diff --git a/Backend/drizzle/0003_little_shard.sql b/Backend/drizzle/0003_little_shard.sql new file mode 100644 index 0000000..ae0b27e --- /dev/null +++ b/Backend/drizzle/0003_little_shard.sql @@ -0,0 +1,17 @@ +CREATE TABLE "videos" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "user_id" uuid NOT NULL, + "object_key" varchar(1024) NOT NULL, + "bucket" varchar(255) NOT NULL, + "upload_url" text NOT NULL, + "download_url" text, + "status" varchar(32) DEFAULT 'pending' NOT NULL, + "expires_at" timestamp with time zone, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "videos_object_key_unique" UNIQUE("object_key") +); +--> statement-breakpoint +ALTER TABLE "events" ADD COLUMN "creator_id" uuid;--> statement-breakpoint +ALTER TABLE "videos" ADD CONSTRAINT "videos_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "events" ADD CONSTRAINT "events_creator_id_users_id_fk" FOREIGN KEY ("creator_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action; \ No newline at end of file diff --git a/Backend/drizzle/meta/0003_snapshot.json b/Backend/drizzle/meta/0003_snapshot.json new file mode 100644 index 0000000..cdb1ef7 --- /dev/null +++ b/Backend/drizzle/meta/0003_snapshot.json @@ -0,0 +1,238 @@ +{ + "id": "9ccc4d0d-ac49-4a3a-83e3-6b1d7eb9ba03", + "prevId": "1edd6921-94b0-4232-805a-d0f96da49b67", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.events": { + "name": "events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "creator_id": { + "name": "creator_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "video_url": { + "name": "video_url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "events_creator_id_users_id_fk": { + "name": "events_creator_id_users_id_fk", + "tableFrom": "events", + "tableTo": "users", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "events_video_url_unique": { + "name": "events_video_url_unique", + "nullsNotDistinct": false, + "columns": [ + "video_url" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.videos": { + "name": "videos", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "object_key": { + "name": "object_key", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": true + }, + "bucket": { + "name": "bucket", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "upload_url": { + "name": "upload_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "download_url": { + "name": "download_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "videos_user_id_users_id_fk": { + "name": "videos_user_id_users_id_fk", + "tableFrom": "videos", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "videos_object_key_unique": { + "name": "videos_object_key_unique", + "nullsNotDistinct": false, + "columns": [ + "object_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/Backend/drizzle/meta/_journal.json b/Backend/drizzle/meta/_journal.json index 7e4ecab..14719b4 100644 --- a/Backend/drizzle/meta/_journal.json +++ b/Backend/drizzle/meta/_journal.json @@ -22,6 +22,13 @@ "when": 1770406019154, "tag": "0002_hesitant_molecule_man", "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1770407807059, + "tag": "0003_little_shard", + "breakpoints": true } ] } \ No newline at end of file diff --git a/Backend/routes/videos.ts b/Backend/routes/videos.ts index d64efa9..b47c7e5 100644 --- a/Backend/routes/videos.ts +++ b/Backend/routes/videos.ts @@ -1,6 +1,8 @@ import { Router } from 'express'; import { z } from 'zod'; +import { db } from '../db/client'; +import { videos } from '../db/schema'; import { requireAuth } from '../middleware/auth'; import { ensureMinioBucket, @@ -55,6 +57,33 @@ router.post('/upload-url', async (req, res) => { const objectKey = buildObjectKey(user.userId, parsed.data.fileName, parsed.data.prefix); const uploadUrl = await minioClient.presignedPutObject(minioBucket, objectKey, minioPresignedExpirySeconds); + const now = new Date(); + const expiresAt = new Date(now.getTime() + minioPresignedExpirySeconds * 1000); + + const [videoRecord] = await db + .insert(videos) + .values({ + userId: user.userId, + objectKey, + bucket: minioBucket, + uploadUrl, + status: 'upload_link_sent', + expiresAt, + updatedAt: now, + }) + .returning({ + id: videos.id, + objectKey: videos.objectKey, + bucket: videos.bucket, + status: videos.status, + createdAt: videos.createdAt, + expiresAt: videos.expiresAt, + }); + + if (!videoRecord) { + res.status(500).json({ message: 'Unable to persist video metadata' }); + return; + } res.status(201).json({ message: 'Dummy upload URL generated', @@ -62,6 +91,7 @@ router.post('/upload-url', async (req, res) => { objectKey, uploadUrl, expiresInSeconds: minioPresignedExpirySeconds, + video: videoRecord, }); });