Skip to content

Commit 9f4a291

Browse files
committed
[Issue #217] Allow query fields to provide other fields as values
1 parent 5866469 commit 9f4a291

8 files changed

Lines changed: 393 additions & 44 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Additions
66
- [Issue #195] addResource() should allow templating
77
- [Issue #216] Add order string query to allow ordering by rand()
8+
- [Issue #217] Allow query fields to provide other fields as values
89

910
### Fixes
1011
- [Issue #225] Order by is case sensitive

src/query-tools/create-search-query.ts

Lines changed: 105 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { getSearchableFields, getSearchableRelations } from '../bodyguard';
22
import { BaseModelInterface } from '../models/base-model';
33
import { Query } from './query';
4+
import { QueryColumnReference } from './query-column-reference';
45

56
/**
67
* Create a SQL query string for a search
@@ -20,20 +21,36 @@ export function createSearchQuery(
2021
for (const queryType in query) {
2122
if (queryType === 'where') {
2223
for (const column in query.where) {
23-
const key = 'where_' + column;
24+
const val = query.where[column];
25+
const ref = new QueryColumnReference(val, objKey);
2426

25-
queryString += `${objKey}.${column}=:${key} AND `;
26-
queryParams[key] = `${query.where[column]}`;
27+
if (ref.isReference) {
28+
queryString += `${objKey}.${column}=${ref.str()} AND `;
29+
}
30+
else {
31+
const key = 'where_' + column;
32+
33+
queryString += `${objKey}.${column}=:${key} AND `;
34+
queryParams[key] = `${val}`;
35+
}
2736
}
2837
}
2938
else if (queryType === 'whereAnyOf') {
3039
queryString += '(';
3140

3241
for (const column in query.whereAnyOf) {
33-
const key = 'whereAnyOf_' + column;
42+
const val = query.whereAnyOf[column];
43+
const ref = new QueryColumnReference(val, objKey);
3444

35-
queryString += `${objKey}.${column}=:${key} OR `;
36-
queryParams[key] = `${query.whereAnyOf[column]}`;
45+
if (ref.isReference) {
46+
queryString += `${objKey}.${column}=${ref.str()} OR `;
47+
}
48+
else {
49+
const key = 'whereAnyOf_' + column;
50+
51+
queryString += `${objKey}.${column}=:${key} OR `;
52+
queryParams[key] = `${val}`;
53+
}
3754
}
3855

3956
queryString = queryString.replace(/ OR +$/, '');
@@ -96,69 +113,119 @@ export function createSearchQuery(
96113

97114
// Range should be an array of two
98115
if ('length' in range && range.length === 2) {
99-
const key = 'between_' + column;
100-
const key1 = key + '1';
101-
const key2 = key + '2';
116+
queryString += `(${objKey}.${column} BETWEEN `;
102117

103-
// Range should be int
104-
range.map((val) => {
105-
return parseInt(val, 10);
106-
});
118+
for (let i = 0; i < 2; i++) {
119+
const val = range[i];
120+
const ref = new QueryColumnReference(val, objKey);
107121

108-
// Append key to queryString
109-
queryString += `(${objKey}.${column} BETWEEN ` +
110-
`:${key1} AND :${key2}) AND `;
122+
if (ref.isReference) {
123+
queryString += ref.str();
124+
}
125+
else {
126+
const key = `between_${column}${i}`;
127+
128+
queryString += ':' + key;
129+
queryParams[key] = val;
130+
}
131+
132+
queryString += ' AND ';
133+
}
134+
135+
queryString = queryString.replace(/ AND +$/, '');
136+
queryString += ') AND ';
111137

112-
queryParams[key1] = range[0];
113-
queryParams[key2] = range[1];
114138
}
115139
}
116140
}
117141
else if (queryType === 'lessThan') {
118142
for (const column in query.lessThan) {
119-
const key = 'lt_' + column;
143+
const val = query.lessThan[column];
144+
const ref = new QueryColumnReference(val, objKey);
120145

121-
// Append key to queryString
122-
queryString += `${objKey}.${column} < :${key} AND `;
123-
queryParams[key] = `${query.lessThan[column]}`;
146+
if (ref.isReference) {
147+
queryString += `${objKey}.${column} < ${ref.str()} AND `;
148+
}
149+
else {
150+
const key = 'lt_' + column;
151+
152+
// Append key to queryString
153+
queryString += `${objKey}.${column} < :${key} AND `;
154+
queryParams[key] = `${val}`;
155+
}
124156
}
125157
}
126158
else if (queryType === 'greaterThan') {
127159
for (const column in query.greaterThan) {
128-
const key = 'gt_' + column;
160+
const val = query.greaterThan[column];
161+
const ref = new QueryColumnReference(val, objKey);
129162

130-
queryString += `${objKey}.${column} > :${key} AND `;
131-
queryParams[key] = `${query.greaterThan[column]}`;
163+
if (ref.isReference) {
164+
queryString += `${objKey}.${column} > ${ref.str()} AND `;
165+
}
166+
else {
167+
const key = 'gt_' + column;
168+
169+
queryString += `${objKey}.${column} > :${key} AND `;
170+
queryParams[key] = `${val}`;
171+
}
132172
}
133173
}
134174
else if (queryType === 'lessThanOrEqual') {
135175
for (const column in query.lessThanOrEqual) {
136-
const key = 'lte_' + column;
176+
const val = query.lessThanOrEqual[column];
177+
const ref = new QueryColumnReference(val, objKey);
137178

138-
queryString +=
139-
`${objKey}.${column} <= ` +
140-
`:${key} AND `;
141-
142-
queryParams[key] = `${query.lessThanOrEqual[column]}`;
179+
if (ref.isReference) {
180+
queryString +=
181+
`${objKey}.${column} <= ${ref.str()} AND `;
182+
}
183+
else {
184+
const key = 'lte_' + column;
185+
186+
queryString +=
187+
`${objKey}.${column} <= ` +
188+
`:${key} AND `;
189+
190+
queryParams[key] = `${val}`;
191+
}
143192
}
144193
}
145194
else if (queryType === 'greaterThanOrEqual') {
146195
for (const column in query.greaterThanOrEqual) {
147-
const key = 'gte_' + column;
196+
const val = query.greaterThanOrEqual[column];
197+
const ref = new QueryColumnReference(val, objKey);
148198

149-
queryString +=
199+
if (ref.isReference) {
200+
queryString +=
150201
`${objKey}.${column} >= ` +
151-
`:${key} AND `;
202+
`${ref.str()} AND `;
203+
}
204+
else {
205+
const key = 'gte_' + column;
206+
207+
queryString +=
208+
`${objKey}.${column} >= ` +
209+
`:${key} AND `;
152210

153-
queryParams[key] = `${query.greaterThanOrEqual[column]}`;
211+
queryParams[key] = `${val}`;
212+
}
154213
}
155214
}
156215
else if (queryType === 'not') {
157216
for (const column in query.not) {
158-
const key = 'not_' + column;
217+
const val = query.not[column];
218+
const ref = new QueryColumnReference(val, objKey);
159219

160-
queryString += `${objKey}.${column}!=:${key} AND `;
161-
queryParams[key] = `${query.not[column]}`;
220+
if (ref.isReference) {
221+
queryString += `${objKey}.${column}!=${ref.str()} AND `;
222+
}
223+
else {
224+
const key = 'not_' + column;
225+
226+
queryString += `${objKey}.${column}!=:${key} AND `;
227+
queryParams[key] = `${val}`;
228+
}
162229
}
163230
}
164231
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
export interface QueryColumnReferenceInterface {
2+
column: string;
3+
table?: string;
4+
}
5+
6+
export class QueryColumnReference implements QueryColumnReferenceInterface {
7+
public isReference: boolean = false;
8+
public column: string;
9+
public table: string;
10+
11+
public constructor(val: unknown, objKey: string) {
12+
if (val && typeof val === 'object' && 'column' in val) {
13+
const ref = val as QueryColumnReferenceInterface;
14+
this.column = ref.column;
15+
this.table = ref.table ? ref.table : objKey
16+
this.isReference = true;
17+
}
18+
}
19+
20+
public str(): null|string {
21+
if (!this.isReference) {
22+
return null;
23+
}
24+
25+
return `"${this.table}".${this.column}`;
26+
}
27+
}

src/query-tools/query-validator.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Request, Response } from 'express';
2-
import { isKeyInModel } from '../utils';
2+
import { isKeyInModel, isJson } from '../utils';
33
import { queryTypes, queryTypeKeys } from './query-types';
44
import { getReadableFields, getReadableRelations } from '../bodyguard';
55
import { validateSync } from 'class-validator';
@@ -199,7 +199,8 @@ export function queryValidator(request: Request, response: Response): boolean {
199199
}
200200

201201
// Is query type proper (array, object, etc)?
202-
let queryTypeConstructor: string = typeof requestQueryParams[type];
202+
const value = requestQueryParams[type]
203+
let queryTypeConstructor: string = typeof value;
203204

204205
if (
205206
queryTypeConstructor === 'object' &&
@@ -208,6 +209,12 @@ export function queryValidator(request: Request, response: Response): boolean {
208209
queryTypeConstructor = 'array';
209210
}
210211

212+
// Query param objects are passed as JSON
213+
if (queryTypeConstructor === 'string' && isJson(value)) {
214+
requestQueryParams[type] = JSON.parse(value);
215+
queryTypeConstructor = 'object';
216+
}
217+
211218
if (!queryTypes[type].includes(queryTypeConstructor)) {
212219
if (response) {
213220
response.validationResponder(
@@ -218,7 +225,7 @@ export function queryValidator(request: Request, response: Response): boolean {
218225
']. It is: ' +
219226
queryTypeConstructor +
220227
'. Value: ' +
221-
requestQueryParams[type]
228+
value
222229
);
223230
}
224231

src/utils/is-json.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@
44
* @return Returns if the string is valid JSON
55
*/
66
export function isJson(str: string): boolean {
7+
if (!str || !str.indexOf) {
8+
return false;
9+
}
10+
11+
if (str.indexOf('{') !== 0 && str.indexOf('{') !== 0) {
12+
return false;
13+
}
14+
715
try {
816
JSON.parse(str);
917
} catch (e) {

0 commit comments

Comments
 (0)