Merge pull request #786 from directus/joins

Fix join registration order
This commit is contained in:
Rijk van Zanten
2020-10-26 18:34:32 +01:00
committed by GitHub
2 changed files with 230 additions and 144 deletions

View File

@@ -40,7 +40,7 @@ export class MetaService {
const dbQuery = database(collection).count('*', { as: 'count' });
if (query.filter) {
applyFilter(dbQuery, query.filter, collection);
await applyFilter(dbQuery, query.filter, collection);
}
const records = await dbQuery;

View File

@@ -1,7 +1,7 @@
import { QueryBuilder } from 'knex';
import { Query, Filter } from '../types';
import database, { schemaInspector } from '../database';
import { clone } from 'lodash';
import { clone, isPlainObject } from 'lodash';
export default async function applyQuery(collection: string, dbQuery: QueryBuilder, query: Query) {
if (query.filter) {
@@ -46,116 +46,244 @@ export default async function applyQuery(collection: string, dbQuery: QueryBuild
}
}
export async function applyFilter(dbQuery: QueryBuilder, filter: Filter, collection: string) {
for (const [key, value] of Object.entries(filter)) {
if (key === '_or') {
value.forEach((subFilter: Record<string, any>) => {
dbQuery.orWhere((subQuery) => applyFilter(subQuery, subFilter, collection));
});
export async function applyFilter(rootQuery: QueryBuilder, rootFilter: Filter, collection: string) {
const relations = await database.select('*').from('directus_relations');
continue;
addWhereClauses(rootQuery, rootFilter, collection);
addJoins(rootQuery, rootFilter, collection);
function addWhereClauses(dbQuery: QueryBuilder, filter: Filter, collection: string) {
for (const [key, value] of Object.entries(filter)) {
if (key === '_or') {
/** @NOTE these callback functions aren't called until Knex runs the query */
dbQuery.orWhere((subQuery) => {
value.forEach((subFilter: Record<string, any>) => {
addWhereClauses(subQuery, subFilter, collection);
});
});
continue;
}
if (key === '_and') {
/** @NOTE these callback functions aren't called until Knex runs the query */
dbQuery.andWhere((subQuery) => {
value.forEach((subFilter: Record<string, any>) => {
addWhereClauses(subQuery, subFilter, collection);
});
});
continue;
}
const filterPath = getFilterPath(key, value);
const { operator: filterOperator, value: filterValue } = getOperation(key, value);
if (filterPath.length > 1) {
const columnName = getWhereColumn(filterPath, collection);
applyFilterToQuery(columnName, filterOperator, filterValue);
} else {
applyFilterToQuery(`${collection}.${filterPath[0]}`, filterOperator, filterValue);
}
}
if (key === '_and') {
value.forEach((subFilter: Record<string, any>) => {
dbQuery.andWhere((subQuery) => applyFilter(subQuery, subFilter, collection));
});
function applyFilterToQuery(key: string, operator: string, compareValue: any) {
if (operator === '_eq') {
dbQuery.where({ [key]: compareValue });
}
continue;
if (operator === '_neq') {
dbQuery.whereNot({ [key]: compareValue });
}
if (operator === '_contains') {
dbQuery.where(key, 'like', `%${compareValue}%`);
}
if (operator === '_ncontains') {
dbQuery.where(key, 'like', `%${compareValue}%`);
}
if (operator === '_gt') {
dbQuery.where(key, '>', compareValue);
}
if (operator === '_gte') {
dbQuery.where(key, '>=', compareValue);
}
if (operator === '_lt') {
dbQuery.where(key, '<', compareValue);
}
if (operator === '_lte') {
dbQuery.where(key, '<=', compareValue);
}
if (operator === '_in') {
let value = compareValue;
if (typeof value === 'string') value = value.split(',');
dbQuery.whereIn(key, value as string[]);
}
if (operator === '_nin') {
let value = compareValue;
if (typeof value === 'string') value = value.split(',');
dbQuery.whereNotIn(key, value as string[]);
}
if (operator === '_null') {
dbQuery.whereNull(key);
}
if (operator === '_nnull') {
dbQuery.whereNotNull(key);
}
if (operator === '_empty') {
dbQuery.andWhere((query) => {
query.whereNull(key);
query.orWhere(key, '=', '');
});
}
if (operator === '_nempty') {
dbQuery.andWhere((query) => {
query.whereNotNull(key);
query.orWhere(key, '!=', '');
});
}
if (operator === '_between') {
let value = compareValue;
if (typeof value === 'string') value = value.split(',');
dbQuery.whereBetween(key, value);
}
if (operator === '_nbetween') {
let value = compareValue;
if (typeof value === 'string') value = value.split(',');
dbQuery.whereNotBetween(key, value);
}
}
const filterPath = getFilterPath(key, value);
const { operator: filterOperator, value: filterValue } = getOperation(key, value);
function getWhereColumn(path: string[], collection: string) {
path = clone(path);
const column =
filterPath.length > 1
? await applyJoins(dbQuery, filterPath, collection)
: `${collection}.${filterPath[0]}`;
let columnName = '';
applyFilterToQuery(column, filterOperator, filterValue);
followRelation(path);
return columnName;
function followRelation(pathParts: string[], parentCollection: string = collection) {
const relation = relations.find((relation) => {
return (
(relation.many_collection === parentCollection &&
relation.many_field === pathParts[0]) ||
(relation.one_collection === parentCollection &&
relation.one_field === pathParts[0])
);
});
if (!relation) return;
const isM2O =
relation.many_collection === parentCollection &&
relation.many_field === pathParts[0];
pathParts.shift();
const parent = isM2O ? relation.one_collection! : relation.many_collection;
if (pathParts.length === 1) {
columnName = `${parent}.${pathParts[0]}`;
}
if (pathParts.length) {
followRelation(pathParts, parent);
}
}
}
}
function applyFilterToQuery(key: string, operator: string, compareValue: any) {
if (operator === '_eq') {
dbQuery.where({ [key]: compareValue });
/**
* @NOTE Yes this is very similar in structure and functionality as the other loop. However,
* due to the order of execution that Knex has in the nested andWhere / orWhere structures,
* joins that are added in there aren't added in time
*/
function addJoins(dbQuery: QueryBuilder, filter: Filter, collection: string) {
for (const [key, value] of Object.entries(filter)) {
if (key === '_or') {
value.forEach((subFilter: Record<string, any>) => {
addJoins(dbQuery, subFilter, collection);
});
continue;
}
if (key === '_and') {
value.forEach((subFilter: Record<string, any>) => {
addJoins(dbQuery, subFilter, collection);
});
continue;
}
const filterPath = getFilterPath(key, value);
if (filterPath.length > 1) {
addJoin(filterPath, collection);
}
}
if (operator === '_neq') {
dbQuery.whereNot({ [key]: compareValue });
}
function addJoin(path: string[], collection: string) {
path = clone(path);
if (operator === '_contains') {
dbQuery.where(key, 'like', `%${compareValue}%`);
}
followRelation(path);
if (operator === '_ncontains') {
dbQuery.where(key, 'like', `%${compareValue}%`);
}
function followRelation(pathParts: string[], parentCollection: string = collection) {
const relation = relations.find((relation) => {
return (
(relation.many_collection === parentCollection &&
relation.many_field === pathParts[0]) ||
(relation.one_collection === parentCollection &&
relation.one_field === pathParts[0])
);
});
if (operator === '_gt') {
dbQuery.where(key, '>', compareValue);
}
if (!relation) return;
if (operator === '_gte') {
dbQuery.where(key, '>=', compareValue);
}
const isM2O =
relation.many_collection === parentCollection &&
relation.many_field === pathParts[0];
if (operator === '_lt') {
dbQuery.where(key, '<', compareValue);
}
if (isM2O) {
dbQuery.leftJoin(
relation.one_collection!,
`${parentCollection}.${relation.many_field}`,
`${relation.one_collection}.${relation.one_primary}`
);
} else {
dbQuery.leftJoin(
relation.many_collection,
`${parentCollection}.${relation.one_primary}`,
`${relation.many_collection}.${relation.many_field}`
);
}
if (operator === '_lte') {
dbQuery.where(key, '<=', compareValue);
}
pathParts.shift();
if (operator === '_in') {
let value = compareValue;
if (typeof value === 'string') value = value.split(',');
const parent = isM2O ? relation.one_collection! : relation.many_collection;
dbQuery.whereIn(key, value as string[]);
}
if (operator === '_nin') {
let value = compareValue;
if (typeof value === 'string') value = value.split(',');
dbQuery.whereNotIn(key, value as string[]);
}
if (operator === '_null') {
dbQuery.whereNull(key);
}
if (operator === '_nnull') {
dbQuery.whereNotNull(key);
}
if (operator === '_empty') {
dbQuery.andWhere((query) => {
query.whereNull(key);
query.orWhere(key, '=', '');
});
}
if (operator === '_nempty') {
dbQuery.andWhere((query) => {
query.whereNotNull(key);
query.orWhere(key, '!=', '');
});
}
if (operator === '_between') {
let value = compareValue;
if (typeof value === 'string') value = value.split(',');
dbQuery.whereBetween(key, value);
}
if (operator === '_nbetween') {
let value = compareValue;
if (typeof value === 'string') value = value.split(',');
dbQuery.whereNotBetween(key, value);
if (pathParts.length) {
followRelation(pathParts, parent);
}
}
}
}
}
@@ -163,7 +291,11 @@ export async function applyFilter(dbQuery: QueryBuilder, filter: Filter, collect
function getFilterPath(key: string, value: Record<string, any>) {
const path = [key];
if (Object.keys(value)[0].startsWith('_') === false) {
if (Object.keys(value)[0].startsWith('_') === true) {
return path;
}
if (isPlainObject(value)) {
path.push(...getFilterPath(Object.keys(value)[0], Object.values(value)[0]));
}
@@ -171,57 +303,11 @@ function getFilterPath(key: string, value: Record<string, any>) {
}
function getOperation(key: string, value: Record<string, any>): { operator: string; value: any } {
if (key.startsWith('_') && key !== '_and' && key !== '_or')
if (key.startsWith('_') && key !== '_and' && key !== '_or') {
return { operator: key as string, value };
} else if (isPlainObject(value) === false) {
return { operator: '_eq', value };
}
return getOperation(Object.keys(value)[0], Object.values(value)[0]);
}
async function applyJoins(dbQuery: QueryBuilder, path: string[], collection: string) {
path = clone(path);
let keyName = '';
await addJoins(path);
return keyName;
async function addJoins(pathParts: string[], parentCollection: string = collection) {
const relation = await database
.select('*')
.from('directus_relations')
.where({ one_collection: parentCollection, one_field: pathParts[0] })
.orWhere({ many_collection: parentCollection, many_field: pathParts[0] })
.first();
if (!relation) return;
const isM2O =
relation.many_collection === parentCollection && relation.many_field === pathParts[0];
if (isM2O) {
dbQuery.leftJoin(
relation.one_collection,
`${parentCollection}.${relation.many_field}`,
`${relation.one_collection}.${relation.one_primary}`
);
} else {
dbQuery.leftJoin(
relation.many_collection,
`${relation.one_collection}.${relation.one_primary}`,
`${relation.many_collection}.${relation.many_field}`
);
}
pathParts.shift();
const parent = isM2O ? relation.one_collection : relation.many_collection;
if (pathParts.length === 1) {
keyName = `${parent}.${pathParts[0]}`;
}
if (pathParts.length) {
await addJoins(pathParts, parent);
}
}
}