Files
stack/apps/api/src/knowledge/import-export.controller.ts
Jason Woltje c4c15ee87e feat: add markdown import/export (closes #77, #78)
- Add POST /api/knowledge/import endpoint for .md and .zip files
- Add GET /api/knowledge/export endpoint with markdown/json formats
- Import parses frontmatter (title, tags, status, visibility)
- Export includes frontmatter in markdown format
- Add ImportExportActions component with drag-and-drop UI
- Add import progress dialog with success/error summary
- Add export dropdown with format selection
- Include comprehensive test suite
- Support bulk import with detailed error reporting
2026-01-30 00:05:15 -06:00

94 lines
2.7 KiB
TypeScript

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";
/**
* 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"))
async importEntries(
@Workspace() workspaceId: string,
@CurrentUser() user: any,
@UploadedFile() file: Express.Multer.File
): Promise<ImportResponseDto> {
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<void> {
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);
}
}