import { Controller, Post, Get, Query, UseGuards, UseInterceptors, UploadedFile, Res, BadRequestException, } from "@nestjs/common"; import { FileInterceptor } from "@nestjs/platform-express"; import { Response } from "express"; import { ImportExportService } from "./services/import-export.service"; import { ExportQueryDto, ExportFormat, ImportResponseDto } from "./dto"; import { AuthGuard } from "../auth/guards/auth.guard"; import { WorkspaceGuard, PermissionGuard } from "../common/guards"; import { Workspace, Permission, RequirePermission } from "../common/decorators"; import { CurrentUser } from "../auth/decorators/current-user.decorator"; import type { AuthUser } from "../auth/types/better-auth-request.interface"; /** * Controller for knowledge import/export endpoints * All endpoints require authentication and workspace context */ @Controller("knowledge") @UseGuards(AuthGuard, WorkspaceGuard, PermissionGuard) export class ImportExportController { constructor(private readonly importExportService: ImportExportService) {} /** * POST /api/knowledge/import * Import knowledge entries from uploaded file (.md or .zip) * Requires: MEMBER role or higher */ @Post("import") @RequirePermission(Permission.WORKSPACE_MEMBER) @UseInterceptors( FileInterceptor("file", { limits: { fileSize: 50 * 1024 * 1024, // 50MB max file size }, fileFilter: (_req, file, callback) => { // Only accept .md and .zip files const allowedMimeTypes = [ "text/markdown", "application/zip", "application/x-zip-compressed", ]; const allowedExtensions = [".md", ".zip"]; const fileExtension = file.originalname .toLowerCase() .slice(file.originalname.lastIndexOf(".")); if (allowedMimeTypes.includes(file.mimetype) || allowedExtensions.includes(fileExtension)) { callback(null, true); } else { callback( new BadRequestException("Invalid file type. Only .md and .zip files are accepted."), false ); } }, }) ) async importEntries( @Workspace() workspaceId: string, @CurrentUser() user: AuthUser, @UploadedFile() file: Express.Multer.File | undefined ): Promise { if (!file) { throw new BadRequestException("No file uploaded"); } const result = await this.importExportService.importEntries(workspaceId, user.id, file); return { success: result.failed === 0, totalFiles: result.totalFiles, imported: result.imported, failed: result.failed, results: result.results, }; } /** * GET /api/knowledge/export * Export knowledge entries as a zip file * Query params: * - format: 'markdown' (default) or 'json' * - entryIds: optional array of entry IDs to export (exports all if not provided) * Requires: Any workspace member */ @Get("export") @RequirePermission(Permission.WORKSPACE_ANY) async exportEntries( @Workspace() workspaceId: string, @Query() query: ExportQueryDto, @Res() res: Response ): Promise { const format = query.format ?? ExportFormat.MARKDOWN; const entryIds = query.entryIds; const { stream, filename } = await this.importExportService.exportEntries( workspaceId, format, entryIds ); // Set response headers res.setHeader("Content-Type", "application/zip"); res.setHeader("Content-Disposition", `attachment; filename="${filename}"`); // Pipe the archive stream to response stream.pipe(res); } }