Skip to content

Commit

Permalink
Add support to insensitive case operators (#11737)
Browse files Browse the repository at this point in the history
* feat: refactor apply-query & implements icontains

* feat: implements icontains in app

* New translations en-US.yaml

* tests: fix returns the filter operators for binary

* implement rest insensitive operators

* fix: proposal to fix #5996 (convert "in" into "exists")

* delete unused imports

* fix: prevent error when operator filter value is null

* fix: apply-query imports

* some tests

* fix: _nstarts_with not applied correctly

* tests: add some filter mathematical tests
  • Loading branch information
bernatvadell committed May 18, 2022
1 parent 79ddd35 commit 7e1b946
Show file tree
Hide file tree
Showing 42 changed files with 914 additions and 154 deletions.
210 changes: 60 additions & 150 deletions api/src/utils/apply-query.ts → api/src/utils/apply-query/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { Aggregate, FieldFunction, Filter, Query, Relation, SchemaOverview } from '@directus/shared/types';
import { getOutputTypeForFunction } from '@directus/shared/utils';
import { Aggregate, Filter, Query, Relation, SchemaOverview } from '@directus/shared/types';
import { Knex } from 'knex';
import { clone, isPlainObject, set } from 'lodash';
import { customAlphabet } from 'nanoid';
import validate from 'uuid-validate';
import { getHelpers } from '../database/helpers';
import { InvalidQueryException } from '../exceptions';
import { getColumn } from './get-column';
import { getColumnPath } from './get-column-path';
import { getRelationInfo } from './get-relation-info';
import { getHelpers } from '../../database/helpers';
import { InvalidQueryException } from '../../exceptions/invalid-query';
import { getColumn } from '../get-column';
import { getColumnPath } from '../get-column-path';
import { getRelationInfo } from '../get-relation-info';
import operators from './operators';

const generateAlias = customAlphabet('abcdefghijklmnopqrstuvwxyz', 5);

Expand Down Expand Up @@ -284,6 +284,10 @@ export function applyFilter(

return rootQuery;

function isNegativeOperator(operator: string) {
return operator.indexOf('_n') === 0;
}

function addJoins(dbQuery: Knex.QueryBuilder, filter: Filter, collection: string) {
for (const [key, value] of Object.entries(filter)) {
if (key === '_or' || key === '_and') {
Expand Down Expand Up @@ -315,6 +319,42 @@ export function applyFilter(
}
}

function callbackSubqueryRelation(relation: Relation, value: any) {
return function (subQueryKnex: Knex.QueryBuilder<any, any>) {
const field = relation!.field;
const collection = relation!.collection;
const column = `${collection}.${field}`;

subQueryKnex.from(collection).whereRaw(`${field} = ${column}`);

applyQuery(
knex,
relation!.collection,
subQueryKnex,
{
filter: value,
},
schema,
true
);
};
}

function inverseFilters(value: any) {
for (const field in value) {
for (const operator in value[field]) {
let inverseOperator = operator;
if (isNegativeOperator(operator)) {
inverseOperator = '_' + operator.substring(2);
} else {
inverseOperator = '_n' + operator.substring(1);
}
value[field][inverseOperator] = value[field][operator];
delete value[field][operator];
}
}
}

function addWhereClauses(
knex: Knex,
dbQuery: Knex.QueryBuilder,
Expand Down Expand Up @@ -370,25 +410,11 @@ export function applyFilter(
pkField = knex.raw(`CAST(?? AS CHAR(255))`, [pkField]);
}

const subQueryBuilder = (filter: Filter) => (subQueryKnex: Knex.QueryBuilder<any, unknown[]>) => {
const field = relation!.field;
const collection = relation!.collection;
const column = `${collection}.${field}`;

subQueryKnex
.select({ [field]: column })
.from(collection)
.whereNotNull(column);

applyQuery(knex, relation!.collection, subQueryKnex, { filter }, schema, true);
};

if (Object.keys(value)?.[0] === '_none') {
dbQuery[logical].whereNotIn(pkField as string, subQueryBuilder(Object.values(value)[0] as Filter));
} else if (Object.keys(value)?.[0] === '_some') {
dbQuery[logical].whereIn(pkField as string, subQueryBuilder(Object.values(value)[0] as Filter));
if (isNegativeOperator(filterOperator)) {
inverseFilters(value);
dbQuery[logical].whereNotExists(callbackSubqueryRelation(relation, value));
} else {
dbQuery[logical].whereIn(pkField as string, subQueryBuilder(value));
dbQuery[logical].whereExists(callbackSubqueryRelation(relation, value));
}
}
}
Expand All @@ -402,38 +428,6 @@ export function applyFilter(
// 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(selectionRaw);
}

if (operator === '_nnull' || (operator === '_null' && compareValue === false)) {
dbQuery[logical].whereNotNull(selectionRaw);
}

if (operator === '_empty' || (operator === '_nempty' && compareValue === false)) {
dbQuery[logical].andWhere((query) => {
query.where(key, '=', '');
});
}

if (operator === '_nempty' || (operator === '_empty' && compareValue === false)) {
dbQuery[logical].andWhere((query) => {
query.where(key, '!=', '');
});
}

// Cast filter value (compareValue) based on function used
if (column.includes('(') && column.includes(')')) {
const functionName = column.split('(')[0] as FieldFunction;
const type = getOutputTypeForFunction(functionName);

if (['bigInteger', 'integer', 'float', 'decimal'].includes(type)) {
compareValue = Number(compareValue);
}
}

// Cast filter value (compareValue) based on type of field being filtered against
const [collection, field] = key.split('.');

if (collection in schema.collections && field in schema.collections[collection].fields) {
Expand Down Expand Up @@ -468,99 +462,15 @@ export function applyFilter(
compareValue = compareValue.filter((val) => val !== undefined);
}

if (operator === '_eq') {
dbQuery[logical].where(selectionRaw, '=', compareValue);
}

if (operator === '_neq') {
dbQuery[logical].whereNot(selectionRaw, compareValue);
}

if (operator === '_contains') {
dbQuery[logical].where(selectionRaw, 'like', `%${compareValue}%`);
}

if (operator === '_ncontains') {
dbQuery[logical].whereNot(selectionRaw, 'like', `%${compareValue}%`);
}

if (operator === '_starts_with') {
dbQuery[logical].where(key, 'like', `${compareValue}%`);
}

if (operator === '_nstarts_with') {
dbQuery[logical].whereNot(key, 'like', `${compareValue}%`);
}

if (operator === '_ends_with') {
dbQuery[logical].where(key, 'like', `%${compareValue}`);
}

if (operator === '_nends_with') {
dbQuery[logical].whereNot(key, 'like', `%${compareValue}`);
}

if (operator === '_gt') {
dbQuery[logical].where(selectionRaw, '>', compareValue);
}

if (operator === '_gte') {
dbQuery[logical].where(selectionRaw, '>=', compareValue);
}

if (operator === '_lt') {
dbQuery[logical].where(selectionRaw, '<', compareValue);
}

if (operator === '_lte') {
dbQuery[logical].where(selectionRaw, '<=', compareValue);
}

if (operator === '_in') {
let value = compareValue;
if (typeof value === 'string') value = value.split(',');

dbQuery[logical].whereIn(selectionRaw, value as string[]);
}

if (operator === '_nin') {
let value = compareValue;
if (typeof value === 'string') value = value.split(',');

dbQuery[logical].whereNotIn(selectionRaw, value as string[]);
}

if (operator === '_between') {
if (compareValue.length !== 2) return;

let value = compareValue;
if (typeof value === 'string') value = value.split(',');

dbQuery[logical].whereBetween(selectionRaw, value);
}

if (operator === '_nbetween') {
if (compareValue.length !== 2) return;

let value = compareValue;
if (typeof value === 'string') value = value.split(',');

dbQuery[logical].whereNotBetween(selectionRaw, value);
}

if (operator == '_intersects') {
dbQuery[logical].whereRaw(helpers.st.intersects(key, compareValue));
}

if (operator == '_nintersects') {
dbQuery[logical].whereRaw(helpers.st.nintersects(key, compareValue));
}
if (operator == '_intersects_bbox') {
dbQuery[logical].whereRaw(helpers.st.intersects_bbox(key, compareValue));
}

if (operator == '_nintersects_bbox') {
dbQuery[logical].whereRaw(helpers.st.nintersects_bbox(key, compareValue));
if (operator in operators) {
operators[operator].apply({
query: dbQuery[logical],
helpers,
selectionRaw,
compareValue,
});
} else {
throw new Error(`Operator ${operator} not supported`);
}
}
}
Expand Down
12 changes: 12 additions & 0 deletions api/src/utils/apply-query/operators/between.operator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { registerOperator } from './operator-register';

export default registerOperator({
operator: '_between',
apply: ({ query, selectionRaw, compareValue }) => {
let value = compareValue;
if (typeof value === 'string') value = value.split(',');
if (!(value instanceof Array)) throw new Error('Invalid value for between operator');
if (value.length !== 2) throw new Error('Expected two values for between operator');
query.whereBetween(selectionRaw, value as [any, any]);
},
});
8 changes: 8 additions & 0 deletions api/src/utils/apply-query/operators/contains.operator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { registerOperator } from './operator-register';

export default registerOperator({
operator: '_contains',
apply: ({ query, selectionRaw, compareValue }) => {
query.where(selectionRaw, 'like', `%${compareValue}%`);
},
});
8 changes: 8 additions & 0 deletions api/src/utils/apply-query/operators/ends-with.operator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { registerOperator } from './operator-register';

export default registerOperator({
operator: '_ends_with',
apply: ({ query, selectionRaw, compareValue }) => {
query.where(selectionRaw, 'like', `%${compareValue}`);
},
});
8 changes: 8 additions & 0 deletions api/src/utils/apply-query/operators/equals.operator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { registerOperator } from './operator-register';

export default registerOperator({
operator: '_eq',
apply: ({ query, selectionRaw, compareValue }) => {
query.where(selectionRaw, '=', compareValue);
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { registerOperator } from './operator-register';

export default registerOperator({
operator: '_gte',
apply: ({ query, selectionRaw, compareValue }) => {
query.where(selectionRaw, '>=', compareValue);
},
});
8 changes: 8 additions & 0 deletions api/src/utils/apply-query/operators/greather-than.operator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { registerOperator } from './operator-register';

export default registerOperator({
operator: '_gt',
apply: ({ query, selectionRaw, compareValue }) => {
query.where(selectionRaw, '>', compareValue);
},
});
11 changes: 11 additions & 0 deletions api/src/utils/apply-query/operators/in.operator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { registerOperator } from './operator-register';

export default registerOperator({
operator: '_in',
apply: ({ query, selectionRaw, compareValue }) => {
let value = compareValue;
if (typeof value === 'string') value = value.split(',');
if (!(value instanceof Array)) throw new Error('Invalid value for in operator');
query.whereIn(selectionRaw, value as string[]);
},
});
70 changes: 70 additions & 0 deletions api/src/utils/apply-query/operators/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import betweenOperator from './between.operator';
import containsOperator from './contains.operator';
import endsWithOperator from './ends-with.operator';
import equalsOperator from './equals.operator';
import greatherThanEqualsOperator from './greather-than-equals.operator';
import greatherThanOperator from './greather-than.operator';
import inOperator from './in.operator';
import insensitiveContainsOperator from './insensitive-contains.operator';
import insensitiveEndsWithOperator from './insensitive-ends-with.operator';
import insensitiveEqualsOperator from './insensitive-equals.operator';
import insensitiveNotContainsOperator from './insensitive-not-contains.operator';
import insensitiveNotEndsWithOperator from './insensitive-not-ends-with.operator';
import insensitiveNotEqualsOperator from './insensitive-not-equals.operator';
import insensitiveNotStartsWithOperator from './insensitive-not-starts-with.operator';
import insensitiveStartsWithOperator from './insensitive-starts-with.operator';
import intersectsBboxOperator from './intersects-bbox.operator';
import intersectsOperator from './intersects.operator';
import isEmptyOperator from './is-empty.operator';
import isNotEmptyOperator from './is-not-empty.operator';
import isNotNullOperator from './is-not-null.operator';
import isNullOperator from './is-null.operator';
import lessThanEqualsOperator from './less-than-equals.operator';
import lessThanOperator from './less-than.operator';
import notBetweenOperator from './not-between.operator';
import notContainsOperator from './not-contains.operator';
import notEndsWithOperator from './not-ends-with.operator';
import notEqualsOperator from './not-equals.operator';
import notInOperator from './not-in.operator';
import notIntersectsBboxOperator from './not-intersects-bbox.operator';
import notIntersectsOperator from './not-intersects.operator';
import notStartsWithOperator from './not-starts-with.operator';
import { OperatorRegister } from './operator-register';
import startsWithOperator from './starts-with.operator';

const operators = [
isNullOperator,
isNotNullOperator,
isEmptyOperator,
isNotEmptyOperator,
equalsOperator,
notEqualsOperator,
containsOperator,
notContainsOperator,
startsWithOperator,
notStartsWithOperator,
endsWithOperator,
notEndsWithOperator,
greatherThanOperator,
greatherThanEqualsOperator,
lessThanOperator,
lessThanEqualsOperator,
inOperator,
notInOperator,
betweenOperator,
notBetweenOperator,
intersectsOperator,
notIntersectsOperator,
intersectsBboxOperator,
notIntersectsBboxOperator,
insensitiveContainsOperator,
insensitiveNotContainsOperator,
insensitiveEqualsOperator,
insensitiveNotEqualsOperator,
insensitiveStartsWithOperator,
insensitiveNotStartsWithOperator,
insensitiveEndsWithOperator,
insensitiveNotEndsWithOperator,
];

export default operators.reduce((a, b) => ({ ...a, [b.operator]: b }), {}) as Record<string, OperatorRegister>;

0 comments on commit 7e1b946

Please sign in to comment.