Skip to content

Commit 07159ee

Browse files
Caneryycursoragent
andauthored
feat(documents): add DTO and Swagger docs for document access endpoints (#851)
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent ca41f2e commit 07159ee

4 files changed

Lines changed: 116 additions & 6 deletions

File tree

‎src/documents/documents-download.controller.ts‎

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,30 @@
11
// @ts-nocheck
22

3-
import { Body, Controller, Get, Param, Post, Query, Res, UseGuards } from '@nestjs/common';
3+
import { Body, Controller, Get, HttpStatus, Param, Post, Query, Res, UseGuards } from '@nestjs/common';
4+
import {
5+
ApiTags,
6+
ApiOperation,
7+
ApiBearerAuth,
8+
ApiParam,
9+
ApiQuery,
10+
ApiBody,
11+
ApiResponse,
12+
} from '@nestjs/swagger';
413
import { Response } from 'express';
514
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
615
import { CurrentUser } from '../auth/decorators/current-user.decorator';
716
import { AuthUserPayload } from '../auth/types/auth-user.type';
8-
import { DownloadDocumentDto, RequestSignedUploadDto } from './dto/document-access.dto';
17+
import {
18+
DownloadDocumentDto,
19+
RequestSignedUploadDto,
20+
SignedUploadUrlResponseDto,
21+
} from './dto/document-access.dto';
922
import { DocumentsService } from './documents.service';
1023
import { SignedUrlService } from './signed-url/signed-url.service';
1124
import { SignedUrlOperation } from './signed-url/signed-url-provider.interface';
1225

26+
@ApiTags('Documents')
27+
@ApiBearerAuth()
1328
@Controller('documents')
1429
@UseGuards(JwtAuthGuard)
1530
export class DocumentsDownloadController {
@@ -24,6 +39,23 @@ export class DocumentsDownloadController {
2439
* Then we redirect to a short-lived signed GET URL.
2540
*/
2641
@Get(':id/download')
42+
@ApiOperation({
43+
summary: 'Download a document',
44+
description: 'Authorizes access and redirects to a short-lived signed download URL.',
45+
})
46+
@ApiParam({ name: 'id', description: 'Document ID' })
47+
@ApiQuery({
48+
name: 'versionId',
49+
required: false,
50+
description: 'Optional version ID to download a specific version',
51+
type: String,
52+
})
53+
@ApiResponse({
54+
status: HttpStatus.FOUND,
55+
description: 'Redirects to a signed download URL',
56+
})
57+
@ApiResponse({ status: HttpStatus.NOT_FOUND, description: 'Document or version not found' })
58+
@ApiResponse({ status: HttpStatus.FORBIDDEN, description: 'Access denied' })
2759
async download(
2860
@Param('id') id: string,
2961
@Query() query: DownloadDocumentDto,
@@ -101,6 +133,18 @@ export class DocumentsDownloadController {
101133
* for backward compatibility and will continue to work indefinitely.
102134
*/
103135
@Post('signed-upload-url')
136+
@ApiOperation({
137+
summary: 'Request a signed upload URL',
138+
description: 'Returns a pre-signed URL for client-side direct upload to object storage.',
139+
})
140+
@ApiBody({ type: RequestSignedUploadDto })
141+
@ApiResponse({
142+
status: HttpStatus.OK,
143+
description: 'Signed upload URL generated',
144+
type: SignedUploadUrlResponseDto,
145+
})
146+
@ApiResponse({ status: HttpStatus.BAD_REQUEST, description: 'Invalid request data' })
147+
@ApiResponse({ status: HttpStatus.FORBIDDEN, description: 'Access denied' })
104148
async requestSignedUploadUrl(
105149
@Body() dto: RequestSignedUploadDto,
106150
@CurrentUser() user: AuthUserPayload,

‎src/documents/documents.controller.ts‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import {
1111
Query,
1212
Res,
1313
UseGuards,
14+
HttpStatus,
1415
} from '@nestjs/common';
16+
import { ApiOperation, ApiBody, ApiResponse } from '@nestjs/swagger';
1517
import { Response } from 'express';
1618
import { DocumentsService } from './documents.service';
1719
import {
@@ -130,6 +132,25 @@ export class DocumentsController {
130132
// ── #404 / #569 Bulk Download (with authorization) ──────────────────────
131133

132134
@Post('bulk-download')
135+
@ApiOperation({
136+
summary: 'Bulk download documents',
137+
description: 'Downloads multiple authorized documents as a ZIP archive.',
138+
})
139+
@ApiBody({ type: BulkDownloadDto })
140+
@ApiResponse({
141+
status: HttpStatus.OK,
142+
description: 'ZIP archive stream',
143+
content: {
144+
'application/zip': {
145+
schema: { type: 'string', format: 'binary' },
146+
},
147+
},
148+
})
149+
@ApiResponse({ status: HttpStatus.NOT_FOUND, description: 'No documents found' })
150+
@ApiResponse({
151+
status: HttpStatus.FORBIDDEN,
152+
description: 'Access denied to one or more documents',
153+
})
133154
bulkDownload(
134155
@Body() dto: BulkDownloadDto,
135156
@Res() res: Response,
Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,82 @@
11
// @ts-nocheck
22

3-
import { IsOptional, IsString, IsUUID } from 'class-validator';
3+
import { IsOptional, IsString, IsUUID, IsNumber, Min } from 'class-validator';
4+
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
5+
import { Type } from 'class-transformer';
46

57
export class DownloadDocumentDto {
8+
@ApiPropertyOptional({
9+
description: 'Optional document version ID to download a specific version',
10+
format: 'uuid',
11+
})
612
@IsOptional()
713
@IsUUID()
814
versionId?: string;
915
}
1016

1117
export class RequestSignedUploadDto {
18+
@ApiPropertyOptional({ description: 'Property ID the document belongs to', format: 'uuid' })
19+
@IsOptional()
1220
@IsUUID()
1321
propertyId?: string;
1422

23+
@ApiPropertyOptional({
24+
description: 'Transaction ID the document is associated with',
25+
format: 'uuid',
26+
})
27+
@IsOptional()
1528
@IsUUID()
16-
// transactionId can be null depending on document context; optional in controller
1729
transactionId?: string;
1830

31+
@ApiProperty({ description: 'Original file name', example: 'contract.pdf' })
1932
@IsString()
2033
fileName: string;
2134

35+
@ApiProperty({ description: 'MIME type of the file', example: 'application/pdf' })
2236
@IsString()
2337
mimeType: string;
2438

39+
@ApiProperty({ description: 'File size in bytes', example: 102400 })
40+
@Type(() => Number)
41+
@IsNumber()
42+
@Min(1)
2543
fileSizeBytes: number;
2644

45+
@ApiPropertyOptional({
46+
description: 'Existing document ID when replacing a document',
47+
format: 'uuid',
48+
})
2749
@IsOptional()
2850
@IsUUID()
29-
documentId?: string; // if updating an existing doc
51+
documentId?: string;
3052

53+
@ApiPropertyOptional({ description: 'Document category', example: 'CONTRACT' })
3154
@IsOptional()
3255
@IsString()
3356
category?: string;
3457

58+
@ApiPropertyOptional({ description: 'Document description' })
3559
@IsOptional()
3660
@IsString()
3761
description?: string;
3862

63+
@ApiPropertyOptional({ description: 'Dispute ID the document is linked to', format: 'uuid' })
3964
@IsOptional()
4065
@IsUUID()
41-
// optional dispute binding
4266
disputeId?: string;
4367
}
68+
69+
export class SignedUploadUrlResponseDto {
70+
@ApiProperty({ description: 'Pre-signed URL for direct client upload' })
71+
url: string;
72+
73+
@ApiProperty({ description: 'Object storage key for the uploaded file' })
74+
objectKey: string;
75+
76+
@ApiProperty({
77+
description: 'Expiration time of the signed URL',
78+
type: String,
79+
format: 'date-time',
80+
})
81+
expiresAt: Date;
82+
}

‎src/documents/dto/document.dto.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// @ts-nocheck
22

33
import { IsString, IsOptional, IsArray, IsDateString, IsBoolean, IsIn } from 'class-validator';
4+
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
45

56
export const DOCUMENT_TYPE_ENUM = [
67
'TITLE_DEED',
@@ -85,6 +86,11 @@ export class SignDocumentDto {
8586
}
8687

8788
export class BulkDownloadDto {
89+
@ApiProperty({
90+
description: 'Document IDs to include in the ZIP archive',
91+
type: [String],
92+
example: ['550e8400-e29b-41d4-a716-446655440000'],
93+
})
8894
@IsArray()
8995
@IsString({ each: true })
9096
documentIds: string[];

0 commit comments

Comments
 (0)