feat: Implement device-specific video uploads and generate comprehensive OpenAPI documentation.
This commit is contained in:
437
Backend/docs/openapi.ts
Normal file
437
Backend/docs/openapi.ts
Normal file
@@ -0,0 +1,437 @@
|
||||
import { OpenAPIRegistry, OpenApiGeneratorV3, extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi';
|
||||
import { z } from 'zod';
|
||||
|
||||
extendZodWithOpenApi(z);
|
||||
|
||||
const registry = new OpenAPIRegistry();
|
||||
|
||||
const ErrorResponseSchema = registry.register(
|
||||
'ErrorResponse',
|
||||
z.object({
|
||||
message: z.string(),
|
||||
errors: z.unknown().optional(),
|
||||
}),
|
||||
);
|
||||
|
||||
const VideoRecordSchema = registry.register(
|
||||
'VideoRecord',
|
||||
z.object({
|
||||
id: z.string().uuid(),
|
||||
objectKey: z.string(),
|
||||
bucket: z.string(),
|
||||
status: z.string(),
|
||||
createdAt: z.string().datetime(),
|
||||
expiresAt: z.string().datetime().nullable(),
|
||||
}),
|
||||
);
|
||||
|
||||
const VideoUploadUrlRequestSchema = registry.register(
|
||||
'VideoUploadUrlRequest',
|
||||
z.object({
|
||||
fileName: z.string().min(1).max(255),
|
||||
deviceId: z.string().uuid(),
|
||||
prefix: z.string().optional(),
|
||||
}),
|
||||
);
|
||||
|
||||
const AdminUploadUrlRequestSchema = registry.register(
|
||||
'AdminUploadUrlRequest',
|
||||
z.object({
|
||||
fileName: z.string().min(1).max(255),
|
||||
prefix: z.string().optional(),
|
||||
}),
|
||||
);
|
||||
|
||||
const UploadUrlResponseSchema = registry.register(
|
||||
'UploadUrlResponse',
|
||||
z.object({
|
||||
message: z.string(),
|
||||
bucket: z.string(),
|
||||
objectKey: z.string(),
|
||||
uploadUrl: z.string().url(),
|
||||
expiresInSeconds: z.number().int().positive(),
|
||||
video: VideoRecordSchema,
|
||||
}),
|
||||
);
|
||||
|
||||
const DownloadUrlQuerySchema = registry.register(
|
||||
'DownloadUrlQuery',
|
||||
z.object({
|
||||
objectKey: z.string().min(1),
|
||||
}),
|
||||
);
|
||||
|
||||
const DownloadUrlResponseSchema = registry.register(
|
||||
'DownloadUrlResponse',
|
||||
z.object({
|
||||
message: z.string(),
|
||||
bucket: z.string(),
|
||||
objectKey: z.string(),
|
||||
downloadUrl: z.string().url(),
|
||||
expiresInSeconds: z.number().int().positive(),
|
||||
}),
|
||||
);
|
||||
|
||||
const ListVideosQuerySchema = registry.register(
|
||||
'ListVideosQuery',
|
||||
z.object({
|
||||
prefix: z.string().optional(),
|
||||
limit: z.coerce.number().int().min(1).max(100).default(20),
|
||||
}),
|
||||
);
|
||||
|
||||
const ListedObjectSchema = registry.register(
|
||||
'ListedObject',
|
||||
z.object({
|
||||
objectKey: z.string().nullable(),
|
||||
size: z.number(),
|
||||
etag: z.string().nullable(),
|
||||
lastModified: z.string().datetime().nullable(),
|
||||
}),
|
||||
);
|
||||
|
||||
const ListVideosResponseSchema = registry.register(
|
||||
'ListVideosResponse',
|
||||
z.object({
|
||||
bucket: z.string(),
|
||||
count: z.number().int().nonnegative(),
|
||||
objects: z.array(ListedObjectSchema),
|
||||
}),
|
||||
);
|
||||
|
||||
const DeleteVideoResponseSchema = registry.register(
|
||||
'DeleteVideoResponse',
|
||||
z.object({
|
||||
message: z.string(),
|
||||
bucket: z.string(),
|
||||
objectKey: z.string(),
|
||||
}),
|
||||
);
|
||||
|
||||
registry.registerPath({
|
||||
method: 'get',
|
||||
path: '/',
|
||||
summary: 'Health check',
|
||||
tags: ['System'],
|
||||
responses: {
|
||||
200: {
|
||||
description: 'Service health text',
|
||||
content: {
|
||||
'text/plain': {
|
||||
schema: z.string(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: 'post',
|
||||
path: '/videos/upload-url',
|
||||
summary: 'Create a presigned upload URL',
|
||||
tags: ['Videos'],
|
||||
security: [{ cookieAuth: [] }],
|
||||
request: {
|
||||
body: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: VideoUploadUrlRequestSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
201: {
|
||||
description: 'Presigned upload URL generated',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: UploadUrlResponseSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
400: {
|
||||
description: 'Invalid request body',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: ErrorResponseSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
401: {
|
||||
description: 'Unauthorized',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: ErrorResponseSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: 'get',
|
||||
path: '/videos/download-url',
|
||||
summary: 'Create a signed download URL',
|
||||
tags: ['Videos'],
|
||||
security: [{ cookieAuth: [] }],
|
||||
request: {
|
||||
query: DownloadUrlQuerySchema,
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: 'Signed download URL generated',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: DownloadUrlResponseSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
400: {
|
||||
description: 'Invalid query params',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: ErrorResponseSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
401: {
|
||||
description: 'Unauthorized',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: ErrorResponseSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: 'get',
|
||||
path: '/videos',
|
||||
summary: 'List objects in the configured bucket',
|
||||
tags: ['Videos'],
|
||||
security: [{ cookieAuth: [] }],
|
||||
request: {
|
||||
query: ListVideosQuerySchema,
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: 'Object listing',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: ListVideosResponseSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
400: {
|
||||
description: 'Invalid query params',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: ErrorResponseSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
401: {
|
||||
description: 'Unauthorized',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: ErrorResponseSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: 'delete',
|
||||
path: '/videos',
|
||||
summary: 'Delete object by object key',
|
||||
tags: ['Videos'],
|
||||
security: [{ cookieAuth: [] }],
|
||||
request: {
|
||||
query: DownloadUrlQuerySchema,
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: 'Object deleted',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: DeleteVideoResponseSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
400: {
|
||||
description: 'Invalid query params',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: ErrorResponseSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
401: {
|
||||
description: 'Unauthorized',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: ErrorResponseSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: 'post',
|
||||
path: '/admin/upload-url',
|
||||
summary: 'Admin: create presigned upload URL',
|
||||
tags: ['Admin'],
|
||||
security: [{ basicAuth: [] }],
|
||||
request: {
|
||||
body: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: AdminUploadUrlRequestSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
201: {
|
||||
description: 'Admin upload URL generated',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: z.object({
|
||||
message: z.string(),
|
||||
bucket: z.string(),
|
||||
objectKey: z.string(),
|
||||
uploadUrl: z.string().url(),
|
||||
expiresAt: z.string().datetime(),
|
||||
expiresInSeconds: z.number().int().positive(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
400: {
|
||||
description: 'Invalid request body',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: ErrorResponseSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
401: {
|
||||
description: 'Unauthorized',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: ErrorResponseSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: 'get',
|
||||
path: '/admin/objects',
|
||||
summary: 'Admin: list objects in bucket',
|
||||
tags: ['Admin'],
|
||||
security: [{ basicAuth: [] }],
|
||||
request: {
|
||||
query: ListVideosQuerySchema,
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: 'Admin object listing',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: ListVideosResponseSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
400: {
|
||||
description: 'Invalid query params',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: ErrorResponseSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
401: {
|
||||
description: 'Unauthorized',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: ErrorResponseSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: 'delete',
|
||||
path: '/admin/object',
|
||||
summary: 'Admin: delete object by key',
|
||||
tags: ['Admin'],
|
||||
security: [{ basicAuth: [] }],
|
||||
request: {
|
||||
query: DownloadUrlQuerySchema,
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: 'Object deleted',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: DeleteVideoResponseSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
400: {
|
||||
description: 'Invalid query params',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: ErrorResponseSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
401: {
|
||||
description: 'Unauthorized',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: ErrorResponseSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export function buildOpenApiDocument() {
|
||||
const generator = new OpenApiGeneratorV3(registry.definitions);
|
||||
|
||||
return generator.generateDocument({
|
||||
openapi: '3.0.3',
|
||||
info: {
|
||||
title: 'Backend API',
|
||||
version: '1.0.0',
|
||||
description: 'Auto-generated API documentation from Zod schemas.',
|
||||
},
|
||||
servers: [{ url: process.env.BETTER_AUTH_URL ?? 'http://localhost:3000' }],
|
||||
tags: [
|
||||
{ name: 'System', description: 'Service endpoints' },
|
||||
{ name: 'Videos', description: 'Authenticated video object operations' },
|
||||
{ name: 'Admin', description: 'Basic-auth protected admin operations' },
|
||||
],
|
||||
components: {
|
||||
securitySchemes: {
|
||||
cookieAuth: {
|
||||
type: 'apiKey',
|
||||
in: 'cookie',
|
||||
name: 'better-auth.session_token',
|
||||
description: 'Better Auth session cookie',
|
||||
},
|
||||
basicAuth: {
|
||||
type: 'http',
|
||||
scheme: 'basic',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user