Skip to content

Commit 2979ea1

Browse files
Merge pull request nestjs#10025 from thiagomini/fix/10017-parse-file-pipe-builder
Fix/10017 parse file pipe builder
2 parents 7c40213 + 79041c1 commit 2979ea1

File tree

5 files changed

+97
-5
lines changed

5 files changed

+97
-5
lines changed

packages/common/pipes/file/parse-file-options.interface.ts

+6
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,10 @@ export interface ParseFileOptions {
55
validators?: FileValidator[];
66
errorHttpStatusCode?: ErrorHttpStatusCode;
77
exceptionFactory?: (error: string) => any;
8+
9+
/**
10+
* Defines if file parameter is required.
11+
* @default true
12+
*/
13+
fileIsRequired?: boolean;
814
}

packages/common/pipes/file/parse-file.pipe.ts

+14-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
import { isUndefined } from '../../utils/shared.utils';
12
import { Injectable, Optional } from '../../decorators/core';
23
import { HttpStatus } from '../../enums';
3-
import { HttpErrorByCode } from '../../utils/http-error-by-code.util';
44
import { PipeTransform } from '../../interfaces/features/pipe-transform.interface';
5-
import { ParseFileOptions } from './parse-file-options.interface';
5+
import { HttpErrorByCode } from '../../utils/http-error-by-code.util';
66
import { FileValidator } from './file-validator.interface';
7+
import { ParseFileOptions } from './parse-file-options.interface';
78

89
/**
910
* Defines the built-in ParseFile Pipe. This pipe can be used to validate incoming files
@@ -19,22 +20,33 @@ import { FileValidator } from './file-validator.interface';
1920
export class ParseFilePipe implements PipeTransform<any> {
2021
protected exceptionFactory: (error: string) => any;
2122
private readonly validators: FileValidator[];
23+
private readonly fileIsRequired: boolean;
2224

2325
constructor(@Optional() options: ParseFileOptions = {}) {
2426
const {
2527
exceptionFactory,
2628
errorHttpStatusCode = HttpStatus.BAD_REQUEST,
2729
validators = [],
30+
fileIsRequired,
2831
} = options;
2932

3033
this.exceptionFactory =
3134
exceptionFactory ||
3235
(error => new HttpErrorByCode[errorHttpStatusCode](error));
3336

3437
this.validators = validators;
38+
this.fileIsRequired = fileIsRequired ?? true;
3539
}
3640

3741
async transform(value: any): Promise<any> {
42+
if (isUndefined(value)) {
43+
if (this.fileIsRequired) {
44+
throw this.exceptionFactory('File is required');
45+
}
46+
47+
return value;
48+
}
49+
3850
if (this.validators.length) {
3951
await this.validate(value);
4052
}

packages/common/test/pipes/file/parse-file.pipe.spec.ts

+60
Original file line numberDiff line numberDiff line change
@@ -116,5 +116,65 @@ describe('ParseFilePipe', () => {
116116
});
117117
});
118118
});
119+
120+
describe('when fileIsRequired is false', () => {
121+
beforeEach(() => {
122+
parseFilePipe = new ParseFilePipe({
123+
validators: [],
124+
fileIsRequired: false,
125+
});
126+
});
127+
128+
it('should pass validation if no file is provided', async () => {
129+
const requestFile = undefined;
130+
131+
await expect(parseFilePipe.transform(requestFile)).to.eventually.eql(
132+
requestFile,
133+
);
134+
});
135+
});
136+
137+
describe('when fileIsRequired is true', () => {
138+
beforeEach(() => {
139+
parseFilePipe = new ParseFilePipe({
140+
validators: [],
141+
fileIsRequired: true,
142+
});
143+
});
144+
145+
it('should throw an error if no file is provided', async () => {
146+
const requestFile = undefined;
147+
148+
await expect(parseFilePipe.transform(requestFile)).to.be.rejectedWith(
149+
BadRequestException,
150+
);
151+
});
152+
153+
it('should pass validation if a file is provided', async () => {
154+
const requestFile = {
155+
path: 'some-path',
156+
};
157+
158+
await expect(parseFilePipe.transform(requestFile)).to.eventually.eql(
159+
requestFile,
160+
);
161+
});
162+
});
163+
164+
describe('when fileIsRequired is not explicitly provided', () => {
165+
beforeEach(() => {
166+
parseFilePipe = new ParseFilePipe({
167+
validators: [new AlwaysInvalidValidator({})],
168+
});
169+
});
170+
171+
it('should throw an error if no file is provided', async () => {
172+
const requestFile = undefined;
173+
174+
await expect(parseFilePipe.transform(requestFile)).to.be.rejectedWith(
175+
BadRequestException,
176+
);
177+
});
178+
});
119179
});
120180
});

sample/29-file-upload/e2e/app/app.e2e-spec.ts

+12
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,18 @@ describe('E2E FileTest', () => {
5151
.expect(400);
5252
});
5353

54+
it('should throw when file is required but no file is uploaded', async () => {
55+
return request(app.getHttpServer())
56+
.post('/file/fail-validation')
57+
.expect(400);
58+
});
59+
60+
it('should allow for optional file uploads with validation enabled (fixes #10017)', () => {
61+
return request(app.getHttpServer())
62+
.post('/file/pass-validation')
63+
.expect(201);
64+
});
65+
5466
afterAll(async () => {
5567
await app.close();
5668
});

sample/29-file-upload/src/app.controller.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,15 @@ export class AppController {
4242
.addFileTypeValidator({
4343
fileType: 'json',
4444
})
45-
.build(),
45+
.build({
46+
fileIsRequired: false,
47+
}),
4648
)
47-
file: Express.Multer.File,
49+
file?: Express.Multer.File,
4850
) {
4951
return {
5052
body,
51-
file: file.buffer.toString(),
53+
file: file?.buffer.toString(),
5254
};
5355
}
5456

0 commit comments

Comments
 (0)