Skip to content

Commit 0f604f9

Browse files
authored
Merge pull request #184 from StatelessStudio/v3.0.1
## [3.0.1] Jul-02-2019
2 parents 5c22fbf + 922cfa4 commit 0f604f9

6 files changed

Lines changed: 203 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# PointyApi Changelog
22

3+
## [3.0.1] Jul-02-2019
4+
5+
### Fixes
6+
- [Issue #182] queryValidator() should run class-validator
7+
- [Issue #181] queryValidator() isKeyInModel fail does not send response
8+
39
## [3.0.0] Jun-20-2019
410

511
### Breaking Changes (See migration.md)

package-lock.json

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "pointyapi",
3-
"version": "3.0.0",
3+
"version": "3.0.1",
44
"author": "stateless-studio",
55
"license": "MIT",
66
"scripts": {
@@ -26,7 +26,7 @@
2626
"@types/btoa": "^1.2.3",
2727
"@types/express": "^4.17.0",
2828
"@types/jsonwebtoken": "^7.2.8",
29-
"@types/node": "^10.14.8",
29+
"@types/node": "^10.14.12",
3030
"@types/request": "^2.48.1",
3131
"atob": "^2.1.2",
3232
"bcryptjs": "^2.4.3",
@@ -53,7 +53,7 @@
5353
"nyc": "^14.1.1",
5454
"source-map-support": "^0.5.12",
5555
"ts-node": "^7.0.1",
56-
"tslint": "^5.17.0",
56+
"tslint": "^5.18.0",
5757
"tslint-eslint-rules": "^5.4.0",
5858
"typescript": "^3.5.2"
5959
},

src/models/example-user.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import {
66
IsEmail,
77
IsAlphanumeric,
88
IsDate,
9-
IsOptional
9+
IsOptional,
10+
IsInt
1011
} from 'class-validator';
1112

1213
// Bodyguards
@@ -30,9 +31,11 @@ import { UserRole, UserStatus } from '../enums';
3031
export class ExampleUser extends BaseUser {
3132
// ID
3233
@PrimaryGeneratedColumn()
34+
@IsInt()
35+
@IsOptional()
3336
@BodyguardKey()
3437
@AnyoneCanRead()
35-
public id: any = undefined;
38+
public id: number = undefined;
3639

3740
// Time created
3841
@Column({ type: 'timestamp', default: new Date() })

src/query-tools/query-validator.ts

Lines changed: 143 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,59 @@ import { Request, Response } from 'express';
22
import { isKeyInModel } from '../utils';
33
import { queryTypes, queryTypeKeys } from './query-types';
44
import { getReadableFields, getReadableRelations } from '../bodyguard';
5+
import {
6+
validateSync,
7+
MetadataStorage,
8+
getFromContainer
9+
} from 'class-validator';
10+
11+
/**
12+
* Get class-validator constraints for a class
13+
* @param someClass BaseModel class(e.g `request.payloadType`)
14+
* @param key (Optional) Key to get constraints for. If unset, constraints for
15+
* all keys will be returned.
16+
*/
17+
function getValidationConstraints(someClass: Function, key?: string) {
18+
const container = <MetadataStorage>getFromContainer(MetadataStorage);
19+
const metadata = container.getTargetValidationMetadatas(
20+
someClass,
21+
JSON.stringify(someClass)
22+
);
23+
const properties = container.groupByPropertyName(metadata);
24+
25+
const validators = Object.keys(properties).reduce((schema, property) => {
26+
schema[property] = properties[
27+
property
28+
].reduce((propertySchema, { type, constraints }) => {
29+
if (Array.isArray(constraints) && constraints.length === 1) {
30+
constraints = constraints[0];
31+
}
32+
33+
if (typeof constraints === 'undefined') {
34+
propertySchema[type] = true;
35+
}
36+
else {
37+
propertySchema[type] = constraints;
38+
}
39+
40+
return propertySchema;
41+
}, {});
42+
43+
return schema;
44+
}, {});
45+
46+
if (key) {
47+
if (key in validators) {
48+
return validators[key];
49+
}
50+
else {
51+
return false;
52+
}
53+
}
54+
else {
55+
return validators;
56+
}
57+
}
558

659
/**
760
* Validate a GET query type
@@ -37,6 +90,10 @@ function queryFieldValidator(
3790
}
3891

3992
if (!isKeyInModel(key, request.payload, response)) {
93+
response.validationResponder(
94+
'Member "' + key + '" does not exist in model'
95+
);
96+
4097
return false;
4198
}
4299

@@ -55,14 +112,22 @@ function queryFieldValidator(
55112
}
56113
}
57114
else if (request.query[type] instanceof Object) {
115+
const validators = getValidationConstraints(request.payloadType);
116+
58117
for (const key in request.query[type]) {
59-
if (request.query[type][key] === undefined) {
118+
const value = request.query[type][key];
119+
120+
if (value === undefined) {
60121
delete request.query[type][key];
61122

62123
continue;
63124
}
64125

65126
if (!isKeyInModel(key, request.payload, response)) {
127+
response.validationResponder(
128+
'Member "' + key + '" does not exist in model'
129+
);
130+
66131
return false;
67132
}
68133

@@ -72,12 +137,88 @@ function queryFieldValidator(
72137
key && key.indexOf('.') ? key.split('.')[0] : key
73138
)
74139
) {
75-
response.validationResponder(
140+
response.forbiddenResponder(
76141
'Cannot "' + type + '" by member "' + key + '"'
77142
);
78143

79144
return false;
80145
}
146+
147+
// Cast to int if need be
148+
if (
149+
key in validators &&
150+
'isInt' in validators[key] &&
151+
validators[key]['isInt'] === true
152+
) {
153+
if (Array.isArray(value)) {
154+
for (let i = 0; i < value.length; i++) {
155+
const asInt = parseInt(value[i], 10);
156+
157+
if (!isNaN(asInt)) {
158+
value[i] = asInt;
159+
}
160+
}
161+
}
162+
else {
163+
const asInt = parseInt(value, 10);
164+
165+
if (!isNaN(asInt)) {
166+
request.query[type][key] = asInt;
167+
}
168+
}
169+
}
170+
}
171+
172+
if (
173+
type === 'where' ||
174+
type === 'whereAnyOf' ||
175+
type === 'not' ||
176+
type === 'lessThan' ||
177+
type === 'lessThanOrEqual' ||
178+
type === 'greaterThan' ||
179+
type === 'greaterThanOrEqual'
180+
) {
181+
const testObject = Object.assign(
182+
new request.payloadType(),
183+
request.query[type]
184+
);
185+
186+
const validationErrors = validateSync(testObject, {
187+
skipMissingProperties: true
188+
});
189+
if (validationErrors && validationErrors.length) {
190+
response.validationResponder(validationErrors);
191+
192+
return false;
193+
}
194+
}
195+
}
196+
else if (type === 'id') {
197+
const validator = getValidationConstraints(
198+
request.payloadType,
199+
'id'
200+
);
201+
202+
if (validator && 'isInt' in validator && validator.isInt === true) {
203+
const asInt = parseInt(request.query.id, 10);
204+
205+
if (!isNaN(asInt)) {
206+
request.query.id = asInt;
207+
}
208+
}
209+
210+
const testObject = Object.assign(new request.payloadType(), {
211+
id: request.query[type]
212+
});
213+
214+
const validationErrors = validateSync(testObject, {
215+
skipMissingProperties: true
216+
});
217+
218+
if (validationErrors && validationErrors.length) {
219+
response.validationResponder(validationErrors);
220+
221+
return false;
81222
}
82223
}
83224
}

test/spec/api/query-tools/query-validator.spec.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,32 @@ describe('[Utils] queryValidator()', () => {
6666
expect(hasValidationResponder).toBe(true);
6767
});
6868

69+
it('fires validation responder if ID query has invalid value type', () => {
70+
const { request, response } = createMockRequest();
71+
request.query = { id: 'a' };
72+
request.payloadType = ExampleUser;
73+
request.payload = new ExampleUser();
74+
75+
let hasValidationResponder = false;
76+
response.validationResponder = () => (hasValidationResponder = true);
77+
78+
expect(queryValidator(request, response)).toBe(false);
79+
expect(hasValidationResponder).toBe(true);
80+
});
81+
82+
it('fires validation responder if the query key has invalid value type', () => {
83+
const { request, response } = createMockRequest();
84+
request.query = { where: { id: 'a' } };
85+
request.payloadType = ExampleUser;
86+
request.payload = new ExampleUser();
87+
88+
let hasValidationResponder = false;
89+
response.validationResponder = () => (hasValidationResponder = true);
90+
91+
expect(queryValidator(request, response)).toBe(false);
92+
expect(hasValidationResponder).toBe(true);
93+
});
94+
6995
it('fires validation responder if a key is not in the model (object)', () => {
7096
const { request, response } = createMockRequest();
7197
request.query = { where: { notInModel: 'test' } };
@@ -110,4 +136,17 @@ describe('[Utils] queryValidator()', () => {
110136
expect(queryValidator(request, response)).toBe(true);
111137
expect(hasValidationResponder).toBe(false);
112138
});
139+
140+
it('can get by id', () => {
141+
const { request, response } = createMockRequest();
142+
request.query = { id: 1 };
143+
request.payloadType = ExampleUser;
144+
request.payload = new ExampleUser();
145+
146+
let hasValidationResponder = false;
147+
response.validationResponder = () => (hasValidationResponder = true);
148+
149+
expect(queryValidator(request, response)).toBe(true);
150+
expect(hasValidationResponder).toBe(false);
151+
});
113152
});

0 commit comments

Comments
 (0)