Skip to content

Commit dd8e301

Browse files
authored
Merge pull request #70 from StatelessStudio/v0.5.0
V0.5.0
2 parents ff374d4 + d68f5a3 commit dd8e301

File tree

8 files changed

+207
-10
lines changed

8 files changed

+207
-10
lines changed

CHANGELOG.md

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

3+
## [0.5.0] Dec-31-2018
4+
5+
Created `CanSearchRelation()` decorator and fixed relation search bugs
6+
7+
### Additions
8+
- Created `CanSearchRelation()` ([Issue #68] Must be able to search by joined columns)
9+
10+
### Fixes
11+
- [Issue #67] GET query fails on order by joined column
12+
- [Issue #66] GET query must query by bodyguard keys, unless user is admin
13+
314
## [0.4.2] Dec-26-2018
415

516
Fixed onlySelf() on GET

package-lock.json

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

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "pointyapi",
3-
"version": "0.4.2",
3+
"version": "0.5.0",
44
"author": "stateless-studio",
55
"license": "MIT",
66
"scripts": {

src/bodyguard.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ const CanReadSymbol = Symbol('CanRead');
141141
const CanWriteSymbol = Symbol('CanWrite');
142142
const BodyguardKeySymbol = Symbol('BodyguardKey');
143143
const CanSearchSymbol = Symbol('CanSearch');
144+
const CanSearchRelationSymbol = Symbol('CanSearchRelation');
144145

145146
export function isBodyguardKey(target: any, propertyKey: string) {
146147
return Reflect.getMetadata(BodyguardKeySymbol, target, propertyKey);
@@ -158,6 +159,10 @@ export function getCanSearch(target: any, propertyKey: string) {
158159
return Reflect.getMetadata(CanSearchSymbol, target, propertyKey);
159160
}
160161

162+
export function getCanSearchRelation(target: any, propertyKey: string) {
163+
return Reflect.getMetadata(CanSearchRelationSymbol, target, propertyKey);
164+
}
165+
161166
export function BodyguardKey() {
162167
return Reflect.metadata(BodyguardKeySymbol, true);
163168
}
@@ -198,13 +203,18 @@ export function CanSearch(who: string = '__anyone__') {
198203
return Reflect.metadata(CanSearchSymbol, who);
199204
}
200205

206+
export function CanSearchRelation(params: object) {
207+
return Reflect.metadata(CanSearchRelationSymbol, params);
208+
}
209+
201210
export { isAdmin } from './bodyguard/is-admin';
202211
export { isSelf } from './bodyguard/is-self';
203212

204213
export { compareNestedBodyguards } from './bodyguard/compare-nested';
205214
export { compareIdToUser } from './bodyguard/compare-id-to-user';
206215
export { getBodyguardKeys } from './bodyguard/get-bodyguard-keys';
207216
export { getSearchableFields } from './bodyguard/get-searchable-fields';
217+
export { getSearchableRelations } from './bodyguard/get-searchable-relations';
208218
export { getReadableFields } from './bodyguard/get-readable-fields';
209219

210220
export { responseFilter } from './bodyguard/response-filter';
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { getCanSearchRelation } from '../bodyguard';
2+
import { BaseModel } from '../models';
3+
4+
/**
5+
* Get an array of searchable fields for the object
6+
* @param obj Object to receive keys for
7+
* @param user User object to check for permissions
8+
*/
9+
export function getSearchableRelations(obj: BaseModel): string[] {
10+
let searchableFields: string[] = [];
11+
12+
for (const member in obj) {
13+
const canSearchRelation = getCanSearchRelation(obj, member);
14+
15+
if (canSearchRelation) {
16+
const who = canSearchRelation.who;
17+
const fields = canSearchRelation.fields.map((field) => {
18+
return `${member}.${field}`;
19+
});
20+
21+
searchableFields = searchableFields.concat(fields);
22+
}
23+
}
24+
25+
return searchableFields;
26+
}

src/middleware/get-query.ts

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import { Request, Response, NextFunction } from 'express';
22
import {
33
getSearchableFields,
4+
getSearchableRelations,
45
getReadableFields,
56
getBodyguardKeys
67
} from '../bodyguard';
78
import { runHook } from '../run-hook';
9+
import { UserRole } from '../enums/user-role';
810

911
function createSearchQuery(payloadType, obj: Object, objKey: string = 'obj') {
1012
const searchableFields = getSearchableFields(payloadType);
13+
const searchableRelations = getSearchableRelations(payloadType);
1114
let queryString = '';
1215
const queryParams = {};
1316
const hasSearchable = searchableFields.length;
@@ -30,6 +33,15 @@ function createSearchQuery(payloadType, obj: Object, objKey: string = 'obj') {
3033
queryParams['__search'] = `%${value}%`;
3134
});
3235

36+
searchableRelations.forEach((field) => {
37+
// Append searchable key to queryString
38+
queryString += `${field} LIKE :__search OR `;
39+
40+
// Append parameter to queryParams (with wildcards)
41+
const value = obj['__search'].replace(/[\s]+/, '%');
42+
queryParams['__search'] = `%${value}%`;
43+
});
44+
3345
queryString = queryString.replace(/ OR +$/, '');
3446
queryString += ')';
3547
}
@@ -211,7 +223,7 @@ export async function getQuery(
211223
const orderByOrders = [];
212224
if ('__orderBy' in request.query) {
213225
for (const key in request.query.__orderBy) {
214-
orderByKeys.push(objMnemonic + '.' + key);
226+
orderByKeys.push(key);
215227
orderByOrders.push(
216228
request.query.__orderBy[key] === 'DESC' ? 'DESC' : 'ASC'
217229
);
@@ -245,7 +257,8 @@ export async function getQuery(
245257
}
246258

247259
// Search
248-
const { queryString, queryParams } = createSearchQuery(
260+
// tslint:disable-next-line:prefer-const
261+
let { queryString, queryParams } = createSearchQuery(
249262
new request.payloadType(),
250263
request.query
251264
);
@@ -259,9 +272,29 @@ export async function getQuery(
259272

260273
// Join bodyguard keys, unless this is the User
261274
if (request.payloadType !== request.userType) {
262-
getBodyguardKeys(new request.payloadType()).forEach((key) => {
275+
// Append to join array
276+
const bodyguardKeys = getBodyguardKeys(new request.payloadType());
277+
278+
bodyguardKeys.forEach((key) => {
263279
request.joinMembers.push(key);
264280
});
281+
282+
// Append to where clause
283+
if (
284+
bodyguardKeys &&
285+
request.user &&
286+
request.user.role !== UserRole.Admin
287+
) {
288+
queryString += ` AND (`;
289+
bodyguardKeys.forEach((key) => {
290+
queryString += `${objMnemonic}.${key}=:bodyGuard${key} OR `;
291+
292+
queryParams['bodyGuard' + key] = request.user.id;
293+
});
294+
295+
queryString = queryString.replace(/ OR +$/, '');
296+
queryString += `)`;
297+
}
265298
}
266299

267300
// Create selection

test/examples/chat/models/chat-message.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ import {
1919
OnlySelfCanWrite,
2020
OnlyAdminCanWrite,
2121
BodyguardKey,
22-
CanSearch
22+
CanSearch,
23+
CanSearchRelation,
24+
getSearchableFields
2325
} from '../../../../src/bodyguard';
2426

2527
// Models
@@ -43,6 +45,10 @@ export class ChatMessage extends BaseModel {
4345
@BodyguardKey()
4446
@OnlySelfCanRead()
4547
@OnlySelfCanWrite()
48+
@CanSearchRelation({
49+
who: '__self__',
50+
fields: [ 'username', 'fname', 'lname' ]
51+
})
4652
public from: User = undefined;
4753

4854
// To User
@@ -54,6 +60,10 @@ export class ChatMessage extends BaseModel {
5460
@BodyguardKey()
5561
@OnlySelfCanRead()
5662
@OnlySelfCanWrite()
63+
@CanSearchRelation({
64+
who: '__self__',
65+
fields: [ 'username', 'fname', 'lname' ]
66+
})
5767
public to: User = undefined;
5868

5969
// Time created

test/spec/chat/chat/chat-get.spec.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,4 +324,111 @@ describe('[Chat] Chat API Get', async () => {
324324
})
325325
.catch((error) => fail(JSON.stringify(error)));
326326
});
327+
328+
it('can search by nested objects', async () => {
329+
const user = await http
330+
.post('/api/v1/user', {
331+
fname: 'Nested',
332+
lname: 'Tester',
333+
username: 'nestedGet1',
334+
password: 'password123',
335+
336+
})
337+
.catch((error) =>
338+
fail('Could not create base user: ' + JSON.stringify(error))
339+
);
340+
341+
const token = await http
342+
.post('/api/v1/auth', {
343+
__user: 'nestedGet1',
344+
password: 'password123'
345+
})
346+
.catch((error) =>
347+
fail('Could not create User API Token' + JSON.stringify(error))
348+
);
349+
350+
if (user && token) {
351+
const chat = await http
352+
.post(
353+
'/api/v1/chat',
354+
{
355+
to: { id: user.body['id'] },
356+
body: 'test'
357+
},
358+
[ 200 ],
359+
token.body['token']
360+
)
361+
.catch((error) =>
362+
fail(
363+
'Could not create chat-message: ' +
364+
JSON.stringify(error)
365+
)
366+
);
367+
368+
await http
369+
.get(
370+
'/api/v1/chat',
371+
{
372+
__search: 'nestedGet1'
373+
},
374+
[ 200 ],
375+
token.body['token']
376+
)
377+
.then((result) => {
378+
if (result.body instanceof Array) {
379+
expect(result.body['length']).toEqual(1);
380+
result.body.forEach((chatResult) => {
381+
expect(chatResult.from.username).toEqual(
382+
'nestedGet1'
383+
);
384+
});
385+
}
386+
else {
387+
fail('Result is not an array.');
388+
}
389+
})
390+
.catch((error) => fail(JSON.stringify(error)));
391+
}
392+
else {
393+
fail('Could not create base user =and/or chat');
394+
}
395+
});
396+
397+
it('can sort by nested objects', async () => {
398+
await http
399+
.get(
400+
'/api/v1/chat',
401+
{
402+
__search: '',
403+
__orderBy: {
404+
'from.username': 'DESC'
405+
}
406+
},
407+
[ 200 ],
408+
this.token.body.token
409+
)
410+
.then((result) => {
411+
if (result.body instanceof Array) {
412+
expect(result.body.length).toBeGreaterThanOrEqual(2);
413+
414+
let firstId;
415+
let secondId;
416+
417+
result.body.forEach((chat, i) => {
418+
if (i === 0) {
419+
firstId = chat.from.id;
420+
}
421+
else if (i === 1) {
422+
secondId = chat.from.id;
423+
}
424+
});
425+
426+
expect(firstId).toBeGreaterThan(secondId);
427+
}
428+
else {
429+
fail('Result is not an array');
430+
}
431+
})
432+
.catch((error) => fail(JSON.stringify(error)));
433+
});
327434
});

0 commit comments

Comments
 (0)