Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
554 changes: 554 additions & 0 deletions DAY3-4-IMPLEMENTATION-GUIDE.md

Large diffs are not rendered by default.

20 changes: 19 additions & 1 deletion apps/api/src/app/app.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,33 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { VideosService } from './videos/videos.service';

// Mock uuid
jest.mock('uuid', () => ({
v4: jest.fn(() => 'test-uuid-123'),
}));

describe('AppController', () => {
let app: TestingModule;
let videosService: VideosService;

beforeAll(async () => {
app = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
providers: [
AppService,
{
provide: VideosService,
useValue: {
create: jest.fn(),
findAll: jest.fn(),
},
},
],
}).compile();

videosService = app.get<VideosService>(VideosService);
});

describe('getData', () => {
Expand Down
121 changes: 119 additions & 2 deletions apps/api/src/app/app.controller.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,133 @@
import { Controller, Get, Logger } from '@nestjs/common';
import {
Controller,
Get,
HttpCode,
HttpStatus,
Logger,
Post,
UploadedFile,
UseInterceptors,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import {
ApiBody,
ApiConsumes,
ApiCreatedResponse,
ApiOkResponse,
ApiOperation,
ApiTags,
} from '@nestjs/swagger';
import { v4 as uuidv4 } from 'uuid';
import { AppService } from './app.service';
import { UploadVideoDto } from './videos/dto/upload-video.dto';
import { Video, VideoStatus } from './videos/entities/video.entity';
import { VideosService } from './videos/videos.service';

interface UploadedFileInterface {
fieldname: string;
originalname: string;
encoding: string;
mimetype: string;
size: number;
buffer: Buffer;
}

@ApiTags('test')
@Controller()
export class AppController {
logger = new Logger(AppController.name);

constructor(private readonly appService: AppService) {
constructor(
private readonly appService: AppService,
private readonly videosService: VideosService,
) {
this.logger.log(`AppController constructor`);
}

@Get()
@ApiOperation({ summary: 'Get API welcome message' })
@ApiOkResponse({
description: 'Returns a welcome message',
schema: {
type: 'object',
properties: {
message: {
type: 'string',
example: 'Hello API',
},
},
},
})
getData() {
return this.appService.getData();
}

@Post('test-video')
@UseInterceptors(FileInterceptor('file'))
@ApiOperation({
summary: 'Upload a video file',
description:
'Uploads a video file and creates a video record in the database. The file will be stored in MinIO and metadata saved to PostgreSQL.',
})
@ApiConsumes('multipart/form-data')
@ApiBody({
description: 'Video file to upload',
type: UploadVideoDto,
schema: {
type: 'object',
required: ['file'],
properties: {
file: {
type: 'string',
format: 'binary',
description: 'Video file (supports: mp4, mov, avi, mkv, webm, etc.)',
example: 'my-video.mp4',
},
},
},
})
@ApiCreatedResponse({
description: 'Video uploaded and record created successfully',
type: Video,
})
@HttpCode(HttpStatus.CREATED)
async testCreateVideo(
@UploadedFile() file: UploadedFileInterface,
): Promise<Video> {
if (!file) {
throw new Error('No file uploaded');
}

this.logger.log(
`Received file upload: filename=${file.originalname}, mimetype=${file.mimetype}, size=${file.size}`,
);

// Generate a unique S3 key (placeholder until MinIO is implemented)
const fileExtension = file.originalname.split('.').pop();
const s3Key = `raw-videos/${uuidv4()}.${fileExtension}`;

// Create video record
const video = await this.videosService.create(
file.originalname,
s3Key,
file.mimetype,
VideoStatus.UPLOADED,
);

this.logger.log(`Video record created: id=${video.id}`);
return video;
}

@Get('test-videos')
@ApiOperation({
summary: 'Get all video records',
description: 'Retrieves all video records from the database',
})
@ApiOkResponse({
description: 'List of all video records',
type: [Video],
})
async testGetVideos(): Promise<Video[]> {
return await this.videosService.findAll();
}
}
22 changes: 20 additions & 2 deletions apps/api/src/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import configuration from '../config/configuration';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import configuration from '../config/configuration';
import { VideosModule } from './videos/videos.module';

@Module({
imports: [
Expand All @@ -11,6 +13,22 @@ import configuration from '../config/configuration';
load: [configuration],
envFilePath: ['.env'],
}),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
type: 'postgres',
host: configService.get<string>('database.host'),
port: configService.get<number>('database.port'),
username: configService.get<string>('database.username'),
password: configService.get<string>('database.password'),
database: configService.get<string>('database.database'),
synchronize: configService.get<boolean>('database.synchronize'),
logging: configService.get<boolean>('database.logging'),
autoLoadEntities: true, // Automatically load entities
}),
inject: [ConfigService],
}),
VideosModule,
],
controllers: [AppController],
providers: [AppService],
Expand Down
35 changes: 35 additions & 0 deletions apps/api/src/app/videos/dto/create-video.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { ApiProperty } from '@nestjs/swagger';
import { VideoStatus } from '../entities/video.entity';

export class CreateVideoDto {
@ApiProperty({
description: 'Original filename of the video',
example: 'my-video.mp4',
maxLength: 255,
})
originalFilename: string;

@ApiProperty({
description: 'S3/MinIO object key where the video will be stored',
example: 'raw-videos/test-uuid.mp4',
maxLength: 500,
})
s3Key: string;

@ApiProperty({
description: 'MIME type of the video file',
example: 'video/mp4',
maxLength: 100,
})
mimeType: string;

@ApiProperty({
description: 'Initial status of the video',
enum: VideoStatus,
example: VideoStatus.UPLOADED,
required: false,
default: VideoStatus.UPLOADING,
})
status?: VideoStatus;
}

12 changes: 12 additions & 0 deletions apps/api/src/app/videos/dto/upload-video.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ApiProperty } from '@nestjs/swagger';

export class UploadVideoDto {
@ApiProperty({
type: 'string',
format: 'binary',
description: 'Video file to upload (supports: mp4, mov, avi, mkv, webm, etc.)',
example: 'video.mp4',
})
file: string | Buffer; // File upload - will be handled by multer or busboy
}

78 changes: 78 additions & 0 deletions apps/api/src/app/videos/entities/video.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { ApiProperty } from '@nestjs/swagger';
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';

export enum VideoStatus {
UPLOADING = 'UPLOADING',
UPLOADED = 'UPLOADED',
QUEUED = 'QUEUED',
PROCESSING = 'PROCESSING',
COMPLETED = 'COMPLETED',
FAILED = 'FAILED',
}

@Entity('videos')
export class Video {
@ApiProperty({
description: 'Unique identifier for the video',
example: '123e4567-e89b-12d3-a456-426614174000',
})
@PrimaryGeneratedColumn('uuid')
id: string;

@ApiProperty({
description: 'Original filename of the uploaded video',
example: 'my-video.mp4',
maxLength: 255,
})
@Column({ type: 'varchar', length: 255 })
originalFilename: string;

@ApiProperty({
description: 'S3/MinIO object key where the video is stored',
example: 'raw-videos/123e4567-e89b-12d3-a456-426614174000.mp4',
maxLength: 500,
})
@Column({ type: 'varchar', length: 500 })
s3Key: string;

@ApiProperty({
description: 'Current processing status of the video',
enum: VideoStatus,
example: VideoStatus.UPLOADED,
})
@Column({
type: 'enum',
enum: VideoStatus,
default: VideoStatus.UPLOADING,
})
status: VideoStatus;

@ApiProperty({
description: 'MIME type of the video file',
example: 'video/mp4',
maxLength: 100,
})
@Column({ type: 'varchar', length: 100 })
mimeType: string;

@ApiProperty({
description: 'Timestamp when the video record was created',
example: '2024-01-01T00:00:00.000Z',
})
@CreateDateColumn()
createdAt: Date;

@ApiProperty({
description: 'Timestamp when the video record was last updated',
example: '2024-01-01T00:00:00.000Z',
})
@UpdateDateColumn()
updatedAt: Date;
}

12 changes: 12 additions & 0 deletions apps/api/src/app/videos/videos.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Video } from './entities/video.entity';
import { VideosService } from './videos.service';

@Module({
imports: [TypeOrmModule.forFeature([Video])],
providers: [VideosService],
exports: [VideosService],
})
export class VideosModule {}

Loading