From ea79d692e4bdf322ddb51c78163abe9b17b06709 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Wed, 16 Jun 2021 17:39:39 -0400 Subject: [PATCH] Allow functions in sort/filter --- api/src/database/run-ast.ts | 2 +- api/src/utils/apply-query.ts | 59 ++++++++++++++++++++++-------------- 2 files changed, 37 insertions(+), 24 deletions(-) diff --git a/api/src/database/run-ast.ts b/api/src/database/run-ast.ts index a220f79935..57aec476e4 100644 --- a/api/src/database/run-ast.ts +++ b/api/src/database/run-ast.ts @@ -171,7 +171,7 @@ function getDBQuery( delete queryCopy.limit; } - applyQuery(table, dbQuery, queryCopy, schema); + applyQuery(knex, table, dbQuery, queryCopy, schema); return dbQuery; } diff --git a/api/src/utils/apply-query.ts b/api/src/utils/apply-query.ts index 6110f5d846..493f951816 100644 --- a/api/src/utils/apply-query.ts +++ b/api/src/utils/apply-query.ts @@ -4,6 +4,8 @@ import { customAlphabet } from 'nanoid'; import validate from 'uuid-validate'; import { InvalidQueryException } from '../exceptions'; import { Aggregate, Filter, Query, Relation, SchemaOverview } from '../types'; +import { applyFunctionToColumnName } from './apply-function-to-column-name'; +import { getColumn } from './get-column'; import { getRelationType } from './get-relation-type'; const generateAlias = customAlphabet('abcdefghijklmnopqrstuvwxyz', 5); @@ -12,6 +14,7 @@ const generateAlias = customAlphabet('abcdefghijklmnopqrstuvwxyz', 5); * Apply the Query to a given Knex query builder instance */ export default function applyQuery( + knex: Knex, collection: string, dbQuery: Knex.QueryBuilder, query: Query, @@ -22,7 +25,7 @@ export default function applyQuery( dbQuery.orderBy( query.sort.map((sort) => ({ ...sort, - column: `${collection}.${sort.column}`, + column: getColumn(knex, collection, sort.column, false) as any, })) ); } @@ -40,7 +43,7 @@ export default function applyQuery( } if (query.filter) { - applyFilter(schema, dbQuery, query.filter, collection, subQuery); + applyFilter(knex, schema, dbQuery, query.filter, collection, subQuery); } if (query.search) { @@ -48,7 +51,7 @@ export default function applyQuery( } if (query.group) { - dbQuery.groupBy(query.group); + dbQuery.groupBy(query.group.map(applyFunctionToColumnName)); } if (query.aggregate) { @@ -95,6 +98,7 @@ export default function applyQuery( * ``` */ export function applyFilter( + knex: Knex, schema: SchemaOverview, rootQuery: Knex.QueryBuilder, rootFilter: Filter, @@ -106,7 +110,7 @@ export function applyFilter( const aliasMap: Record = {}; addJoins(rootQuery, rootFilter, collection); - addWhereClauses(rootQuery, rootFilter, collection); + addWhereClauses(knex, rootQuery, rootFilter, collection); function addJoins(dbQuery: Knex.QueryBuilder, filter: Filter, collection: string) { for (const [key, value] of Object.entries(filter)) { @@ -223,6 +227,7 @@ export function applyFilter( } function addWhereClauses( + knex: Knex, dbQuery: Knex.QueryBuilder, filter: Filter, collection: string, @@ -239,7 +244,7 @@ export function applyFilter( /** @NOTE this callback function isn't called until Knex runs the query */ dbQuery[logical].where((subQuery) => { value.forEach((subFilter: Record) => { - addWhereClauses(subQuery, subFilter, collection, key === '_and' ? 'and' : 'or'); + addWhereClauses(knex, subQuery, subFilter, collection, key === '_and' ? 'and' : 'or'); }); }); @@ -282,6 +287,7 @@ export function applyFilter( subQueryKnex.select({ [field]: column }).from(collection); applyQuery( + knex, relation!.collection, subQueryKnex, { @@ -295,26 +301,33 @@ export function applyFilter( } function applyFilterToQuery(key: string, operator: string, compareValue: any, logical: 'and' | 'or' = 'and') { + const [table, column] = key.split('.'); + + // Is processed through Knex.Raw, so should be safe to string-inject into these where queries + const selectionRaw = getColumn(knex, table, column, false) as any; + // Knex supports "raw" in the columnName parameter, but isn't typed as such. Too bad.. + // See https://github.com/knex/knex/issues/4518 @TODO remove as any once knex is updated + // These operators don't rely on a value, and can thus be used without one (eg `?filter[field][_null]`) if (operator === '_null' || (operator === '_nnull' && compareValue === false)) { - dbQuery[logical].whereNull(key); + dbQuery[logical].whereNull(selectionRaw); } if (operator === '_nnull' || (operator === '_null' && compareValue === false)) { - dbQuery[logical].whereNotNull(key); + dbQuery[logical].whereNotNull(selectionRaw); } if (operator === '_empty' || (operator === '_nempty' && compareValue === false)) { dbQuery[logical].andWhere((query) => { - query.whereNull(key); - query.orWhere(key, '=', ''); + query.whereNull(selectionRaw); + query.orWhere(selectionRaw, '=', ''); }); } if (operator === '_nempty' || (operator === '_empty' && compareValue === false)) { dbQuery[logical].andWhere((query) => { - query.whereNotNull(key); - query.orWhere(key, '!=', ''); + query.whereNotNull(selectionRaw); + query.orWhere(selectionRaw, '!=', ''); }); } @@ -333,49 +346,49 @@ export function applyFilter( } if (operator === '_eq') { - dbQuery[logical].where({ [key]: compareValue }); + dbQuery[logical].where(selectionRaw, '=', compareValue); } if (operator === '_neq') { - dbQuery[logical].whereNot({ [key]: compareValue }); + dbQuery[logical].whereNot(selectionRaw, compareValue); } if (operator === '_contains') { - dbQuery[logical].where(key, 'like', `%${compareValue}%`); + dbQuery[logical].where(selectionRaw, 'like', `%${compareValue}%`); } if (operator === '_ncontains') { - dbQuery[logical].whereNot(key, 'like', `%${compareValue}%`); + dbQuery[logical].whereNot(selectionRaw, 'like', `%${compareValue}%`); } if (operator === '_gt') { - dbQuery[logical].where(key, '>', compareValue); + dbQuery[logical].where(selectionRaw, '>', compareValue); } if (operator === '_gte') { - dbQuery[logical].where(key, '>=', compareValue); + dbQuery[logical].where(selectionRaw, '>=', compareValue); } if (operator === '_lt') { - dbQuery[logical].where(key, '<', compareValue); + dbQuery[logical].where(selectionRaw, '<', compareValue); } if (operator === '_lte') { - dbQuery[logical].where(key, '<=', compareValue); + dbQuery[logical].where(selectionRaw, '<=', compareValue); } if (operator === '_in') { let value = compareValue; if (typeof value === 'string') value = value.split(','); - dbQuery[logical].whereIn(key, value as string[]); + dbQuery[logical].whereIn(selectionRaw, value as string[]); } if (operator === '_nin') { let value = compareValue; if (typeof value === 'string') value = value.split(','); - dbQuery[logical].whereNotIn(key, value as string[]); + dbQuery[logical].whereNotIn(selectionRaw, value as string[]); } if (operator === '_between') { @@ -384,7 +397,7 @@ export function applyFilter( let value = compareValue; if (typeof value === 'string') value = value.split(','); - dbQuery[logical].whereBetween(key, value); + dbQuery[logical].whereBetween(selectionRaw, value); } if (operator === '_nbetween') { @@ -393,7 +406,7 @@ export function applyFilter( let value = compareValue; if (typeof value === 'string') value = value.split(','); - dbQuery[logical].whereNotBetween(key, value); + dbQuery[logical].whereNotBetween(selectionRaw, value); } }