mirror of
https://github.com/directus/directus.git
synced 2026-01-30 13:27:55 -05:00
Merge pull request #786 from directus/joins
Fix join registration order
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user