Skip to content

Commit 1d14ebd

Browse files
committed
Merge remote-tracking branch 'origin/master' into feat/update-project-tooling
2 parents 2eb1f4c + 22ff659 commit 1d14ebd

File tree

5 files changed

+152
-81
lines changed

5 files changed

+152
-81
lines changed

src/ActionParameterHandler.ts

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export class ActionParameterHandler<T extends BaseDriver> {
1919
// Constructor
2020
// -------------------------------------------------------------------------
2121

22-
constructor(private driver: T) {}
22+
constructor(private driver: T) { }
2323

2424
// -------------------------------------------------------------------------
2525
// Public Methods
@@ -194,7 +194,6 @@ export class ActionParameterHandler<T extends BaseDriver> {
194194
throw new ParameterParseJsonError(paramMetadata.name, value);
195195
}
196196
}
197-
198197
return value;
199198
}
200199

@@ -212,35 +211,30 @@ export class ActionParameterHandler<T extends BaseDriver> {
212211
const options = paramMetadata.classTransform || this.driver.plainToClassTransformOptions;
213212
value = plainToClass(paramMetadata.targetType, value, options);
214213
}
215-
216214
return value;
217215
}
218216

219217
/**
220218
* Perform class-validation if enabled.
221219
*/
222220
protected validateValue(value: any, paramMetadata: ParamMetadata): Promise<any> | any {
223-
const isValidationEnabled =
224-
paramMetadata.validate instanceof Object ||
225-
paramMetadata.validate === true ||
226-
(this.driver.enableValidation === true && paramMetadata.validate !== false);
227-
const shouldValidate =
228-
paramMetadata.targetType && paramMetadata.targetType !== Object && value instanceof paramMetadata.targetType;
221+
const isValidationEnabled = (paramMetadata.validate instanceof Object || paramMetadata.validate === true)
222+
|| (this.driver.enableValidation === true && paramMetadata.validate !== false);
223+
const shouldValidate = paramMetadata.targetType
224+
&& (paramMetadata.targetType !== Object)
225+
&& (value instanceof paramMetadata.targetType);
229226

230227
if (isValidationEnabled && shouldValidate) {
231-
const options = paramMetadata.validate instanceof Object ? paramMetadata.validate : this.driver.validationOptions;
228+
const options = Object.assign({}, this.driver.validationOptions, paramMetadata.validate);
232229
return validate(value, options)
233230
.then(() => value)
234231
.catch((validationErrors: ValidationError[]) => {
235-
const error: any = new BadRequestError(
236-
`Invalid ${paramMetadata.type}, check 'errors' property for more info.`
237-
);
232+
const error: any = new BadRequestError(`Invalid ${paramMetadata.type}, check 'errors' property for more info.`);
238233
error.errors = validationErrors;
239234
error.paramName = paramMetadata.name;
240235
throw error;
241236
});
242237
}
243-
244238
return value;
245239
}
246240
}

src/RoutingControllers.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Action } from './Action';
22
import { ActionParameterHandler } from './ActionParameterHandler';
33
import { getFromContainer } from './container';
44
import { BaseDriver } from './driver/BaseDriver';
5+
import { InterceptorInterface } from './InterceptorInterface';
56
import { MetadataBuilder } from './metadata-builder/MetadataBuilder';
67
import { ActionMetadata } from './metadata/ActionMetadata';
78
import { InterceptorMetadata } from './metadata/InterceptorMetadata';
@@ -175,7 +176,7 @@ export class RoutingControllers<T extends BaseDriver> {
175176
if (use.interceptor.prototype && use.interceptor.prototype.intercept) {
176177
// if this is function instance of InterceptorInterface
177178
return function (action: Action, result: any) {
178-
return getFromContainer(use.interceptor, action).intercept(action, result);
179+
return getFromContainer<InterceptorInterface>(use.interceptor, action).intercept(action, result);
179180
};
180181
}
181182
return use.interceptor;

src/driver/express/ExpressDriver.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -391,12 +391,12 @@ export class ExpressDriver extends BaseDriver {
391391
*/
392392
protected prepareMiddlewares(uses: UseMetadata[]) {
393393
const middlewareFunctions: Function[] = [];
394-
uses.forEach(use => {
394+
uses.forEach((use: UseMetadata) => {
395395
if (use.middleware.prototype && use.middleware.prototype.use) {
396396
// if this is function instance of MiddlewareInterface
397397
middlewareFunctions.push((request: any, response: any, next: (err: any) => any) => {
398398
try {
399-
const useResult = getFromContainer(use.middleware).use(request, response, next);
399+
const useResult = getFromContainer<ExpressMiddlewareInterface>(use.middleware).use(request, response, next);
400400
if (isPromiseLike(useResult)) {
401401
useResult.catch((error: any) => {
402402
this.handleError(error, undefined, { request, response, next });
@@ -412,7 +412,7 @@ export class ExpressDriver extends BaseDriver {
412412
} else if (use.middleware.prototype && use.middleware.prototype.error) {
413413
// if this is function instance of ErrorMiddlewareInterface
414414
middlewareFunctions.push(function (error: any, request: any, response: any, next: (err: any) => any) {
415-
return getFromContainer(use.middleware).error(error, request, response, next);
415+
return getFromContainer<ExpressErrorMiddlewareInterface>(use.middleware).error(error, request, response, next);
416416
});
417417
} else {
418418
middlewareFunctions.push(use.middleware);

src/driver/koa/KoaDriver.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ export class KoaDriver extends BaseDriver {
335335
// if this is function instance of MiddlewareInterface
336336
middlewareFunctions.push(async (context: any, next: (err?: any) => Promise<any>) => {
337337
try {
338-
return await getFromContainer(use.middleware).use(context, next);
338+
return await getFromContainer<KoaMiddlewareInterface>(use.middleware).use(context, next);
339339
} catch (error) {
340340
return await this.handleError(error, undefined, {
341341
request: context.request,

test/functional/class-validator-options.spec.ts

Lines changed: 138 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
import { Expose } from 'class-transformer';
22
import { defaultMetadataStorage } from 'class-transformer/storage';
3+
import { Length } from 'class-validator';
34
import { Server as HttpServer } from 'http';
45
import HttpStatusCodes from 'http-status-codes';
56
import qs from 'qs';
67
import { Get } from '../../src/decorator/Get';
78
import { JsonController } from '../../src/decorator/JsonController';
89
import { QueryParam } from '../../src/decorator/QueryParam';
9-
import { createExpressServer, getMetadataArgsStorage, ResponseClassTransformOptions } from '../../src/index';
10+
import { createExpressServer, getMetadataArgsStorage, ResponseClassTransformOptions, RoutingControllersOptions } from '../../src/index';
1011
import { axios } from '../utilities/axios';
1112
import DoneCallback = jest.DoneCallback;
1213

1314
describe(``, () => {
1415
let expressServer: HttpServer;
15-
16+
let requestFilter: UserFilter;
17+
1618
class UserFilter {
19+
@Length(5, 15)
1720
keyword: string;
1821
}
1922

@@ -31,8 +34,6 @@ describe(``, () => {
3134
afterAll(() => defaultMetadataStorage.clear());
3235

3336
describe('no options', () => {
34-
let requestFilter: UserFilter;
35-
3637
beforeEach((done: DoneCallback) => {
3738
requestFilter = undefined;
3839
getMetadataArgsStorage().reset();
@@ -63,12 +64,12 @@ describe(``, () => {
6364
expect.assertions(4);
6465
const response = await axios.get(
6566
'/user?' +
66-
qs.stringify({
67-
filter: {
68-
keyword: 'Um',
69-
__somethingPrivate: 'blablabla',
70-
},
71-
})
67+
qs.stringify({
68+
filter: {
69+
keyword: 'Um',
70+
__somethingPrivate: 'blablabla',
71+
},
72+
})
7273
);
7374
expect(response.status).toEqual(HttpStatusCodes.OK);
7475
expect(response.data).toEqual({
@@ -82,66 +83,141 @@ describe(``, () => {
8283
keyword: 'Um',
8384
__somethingPrivate: 'blablabla',
8485
});
85-
});
86-
}); // ------ end no options
86+
}); // ------ end no options
87+
});
8788

8889
describe('global options', () => {
89-
let requestFilter: UserFilter;
9090

91-
beforeEach((done: DoneCallback) => {
92-
requestFilter = undefined;
93-
getMetadataArgsStorage().reset();
91+
describe("should merge local validation options with global validation options prioritizing local", () => {
9492

95-
@JsonController()
96-
class ClassTransformUserController {
97-
@Get('/user')
98-
getUsers(@QueryParam('filter') filter: UserFilter): any {
99-
requestFilter = filter;
100-
const user = new UserModel();
101-
user.id = 1;
102-
user._firstName = 'Umed';
103-
user._lastName = 'Khudoiberdiev';
104-
return user;
93+
beforeEach((done) => {
94+
requestFilter = undefined;
95+
getMetadataArgsStorage().reset();
96+
97+
@JsonController()
98+
class ClassTransformUserController {
99+
100+
@Get("/user")
101+
getUsers(@QueryParam("filter", { validate: { skipMissingProperties: false } }) filter: UserFilter): any {
102+
requestFilter = filter;
103+
const user = new UserModel();
104+
user.id = 1;
105+
user._firstName = "Umed";
106+
user._lastName = "Khudoiberdiev";
107+
return user;
108+
}
105109
}
106-
}
107110

108-
expressServer = createExpressServer({
109-
validation: false,
110-
classToPlainTransformOptions: {
111-
excludePrefixes: ['_'],
112-
},
113-
plainToClassTransformOptions: {
114-
excludePrefixes: ['__'],
115-
},
116-
}).listen(3001, done);
117-
});
111+
const options: RoutingControllersOptions = {
112+
validation: {
113+
whitelist: true,
114+
skipMissingProperties: true
115+
}
116+
};
118117

119-
afterEach((done: DoneCallback) => {
120-
expressServer.close(done);
121-
});
118+
expressServer = createExpressServer(options).listen(3001, done)
119+
});
122120

123-
it('should apply global options', async () => {
124-
expect.assertions(4);
125-
const response = await axios.get(
126-
'/user?' +
121+
122+
afterEach(done => {
123+
expressServer.close(done)
124+
});
125+
126+
it(`succeed`, async () => {
127+
const response = await axios.get(
128+
'/user?' +
127129
qs.stringify({
128130
filter: {
129-
keyword: 'Um',
131+
keyword: 'aValidKeyword',
132+
notKeyword: 'Um',
130133
__somethingPrivate: 'blablabla',
131134
},
132135
})
133-
);
134-
expect(response.status).toEqual(HttpStatusCodes.OK);
135-
expect(response.data).toEqual({
136-
id: 1,
137-
name: 'Umed Khudoiberdiev',
136+
);
137+
expect(response.status).toEqual(200);
138+
expect(requestFilter).toEqual({
139+
keyword: "aValidKeyword"
140+
});
141+
})
142+
});
143+
144+
describe("should pass the valid param after validation", () => {
145+
beforeEach((done) => {
146+
requestFilter = undefined;
147+
getMetadataArgsStorage().reset();
148+
149+
@JsonController()
150+
class UserController {
151+
152+
@Get("/user")
153+
getUsers(@QueryParam("filter") filter: UserFilter): any {
154+
requestFilter = filter;
155+
const user = new UserModel();
156+
user.id = 1;
157+
user._firstName = "Umed";
158+
user._lastName = "Khudoiberdiev";
159+
return user;
160+
}
161+
}
162+
163+
const options: RoutingControllersOptions = {
164+
validation: true
165+
};
166+
167+
expressServer = createExpressServer(options).listen(3001, done)
138168
});
139-
expect(requestFilter).toBeInstanceOf(UserFilter);
140-
expect(requestFilter).toEqual({
141-
keyword: 'Um',
169+
170+
afterEach(done => {
171+
expressServer.close(done)
142172
});
143-
});
144-
}); // ----- end global options
173+
174+
it(`succeed`, async () => {
175+
const response = await axios.get(
176+
'/user?' +
177+
qs.stringify({
178+
filter: {
179+
keyword: 'aValidKeyword',
180+
notKeyword: 'Um',
181+
__somethingPrivate: 'blablabla',
182+
},
183+
})
184+
);
185+
186+
expect(response.status).toEqual(200);
187+
expect(response.data).toMatchObject({
188+
id: 1,
189+
_firstName: "Umed",
190+
_lastName: "Khudoiberdiev"
191+
});
192+
expect(requestFilter).toBeInstanceOf(UserFilter);
193+
expect(requestFilter).toMatchObject({
194+
keyword: "aValidKeyword",
195+
__somethingPrivate: "blablabla",
196+
});
197+
})
198+
199+
it('should contain param name on validation failed', async () => {
200+
expect.assertions(2);
201+
try {
202+
await axios.get(
203+
'/user?' +
204+
qs.stringify({
205+
filter: {
206+
keyword: 'Um',
207+
__somethingPrivate: 'blablabla',
208+
},
209+
})
210+
);
211+
}
212+
catch(error) {
213+
expect(error.response.status).toEqual(HttpStatusCodes.BAD_REQUEST);
214+
expect(error.response.data.errors[0].property).toBe(`keyword`);
215+
}
216+
217+
});
218+
}); // ----- end global options
219+
});
220+
145221

146222
describe('local options', () => {
147223
let requestFilter: UserFilter;
@@ -177,12 +253,12 @@ describe(``, () => {
177253
expect.assertions(4);
178254
const response = await axios.get(
179255
'/user?' +
180-
qs.stringify({
181-
filter: {
182-
keyword: 'Um',
183-
__somethingPrivate: 'blablabla',
184-
},
185-
})
256+
qs.stringify({
257+
filter: {
258+
keyword: 'Um',
259+
__somethingPrivate: 'blablabla',
260+
},
261+
})
186262
);
187263
expect(response.status).toEqual(HttpStatusCodes.OK);
188264
expect(response.data).toEqual({
@@ -195,4 +271,4 @@ describe(``, () => {
195271
});
196272
});
197273
}); //----- end local options
198-
});
274+
});

0 commit comments

Comments
 (0)