Handle empty relationships
This commit is contained in:
parent
7ea2c187a7
commit
76d0107d4d
|
@ -23,12 +23,14 @@ import {
|
|||
InternalSearchFilterOperator,
|
||||
JsonFieldMetadata,
|
||||
JsonTypes,
|
||||
LogicalOperator,
|
||||
Operation,
|
||||
prefixed,
|
||||
QueryJson,
|
||||
QueryOptions,
|
||||
RangeOperator,
|
||||
RelationshipsJson,
|
||||
SearchFilterKey,
|
||||
SearchFilters,
|
||||
SortOrder,
|
||||
SqlClient,
|
||||
|
@ -96,6 +98,22 @@ function isSqs(table: Table): boolean {
|
|||
)
|
||||
}
|
||||
|
||||
const allowEmptyRelationships: Record<SearchFilterKey, boolean> = {
|
||||
[BasicOperator.EQUAL]: false,
|
||||
[BasicOperator.NOT_EQUAL]: true,
|
||||
[BasicOperator.EMPTY]: false,
|
||||
[BasicOperator.NOT_EMPTY]: true,
|
||||
[BasicOperator.FUZZY]: false,
|
||||
[BasicOperator.STRING]: false,
|
||||
[RangeOperator.RANGE]: false,
|
||||
[ArrayOperator.CONTAINS]: false,
|
||||
[ArrayOperator.NOT_CONTAINS]: true,
|
||||
[ArrayOperator.CONTAINS_ANY]: false,
|
||||
[ArrayOperator.ONE_OF]: false,
|
||||
[LogicalOperator.AND]: false,
|
||||
[LogicalOperator.OR]: false,
|
||||
}
|
||||
|
||||
class InternalBuilder {
|
||||
private readonly client: SqlClient
|
||||
private readonly query: QueryJson
|
||||
|
@ -405,6 +423,7 @@ class InternalBuilder {
|
|||
|
||||
addRelationshipForFilter(
|
||||
query: Knex.QueryBuilder,
|
||||
allowEmptyRelationships: boolean,
|
||||
filterKey: string,
|
||||
whereCb: (query: Knex.QueryBuilder) => Knex.QueryBuilder
|
||||
): Knex.QueryBuilder {
|
||||
|
@ -426,9 +445,10 @@ class InternalBuilder {
|
|||
relationship.to &&
|
||||
relationship.tableName
|
||||
) {
|
||||
let subQuery = mainKnex
|
||||
const joinTable = mainKnex
|
||||
.select(mainKnex.raw(1))
|
||||
.from({ [toAlias]: relatedTableName })
|
||||
let subQuery = joinTable.clone()
|
||||
const manyToMany = validateManyToMany(relationship)
|
||||
if (manyToMany) {
|
||||
const throughAlias =
|
||||
|
@ -440,7 +460,6 @@ class InternalBuilder {
|
|||
subQuery = subQuery
|
||||
// add a join through the junction table
|
||||
.innerJoin(throughTable, function () {
|
||||
// @ts-ignore
|
||||
this.on(
|
||||
`${toAlias}.${manyToMany.toPrimary}`,
|
||||
"=",
|
||||
|
@ -460,18 +479,33 @@ class InternalBuilder {
|
|||
if (this.client === SqlClient.SQL_LITE) {
|
||||
subQuery = this.addJoinFieldCheck(subQuery, manyToMany)
|
||||
}
|
||||
|
||||
query = query.whereExists(whereCb(subQuery))
|
||||
if (allowEmptyRelationships) {
|
||||
query = query.orWhereNotExists(
|
||||
joinTable.clone().innerJoin(throughTable, function () {
|
||||
this.on(
|
||||
`${fromAlias}.${manyToMany.fromPrimary}`,
|
||||
"=",
|
||||
`${throughAlias}.${manyToMany.from}`
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
} else {
|
||||
const foreignKey = `${fromAlias}.${relationship.from}`
|
||||
// "join" to the main table, making sure the ID matches that of the main
|
||||
subQuery = subQuery.where(
|
||||
`${toAlias}.${relationship.to}`,
|
||||
"=",
|
||||
mainKnex.raw(
|
||||
this.quotedIdentifier(`${fromAlias}.${relationship.from}`)
|
||||
)
|
||||
mainKnex.raw(this.quotedIdentifier(foreignKey))
|
||||
)
|
||||
|
||||
query = query.whereExists(whereCb(subQuery))
|
||||
if (allowEmptyRelationships) {
|
||||
query = query.orWhereNull(foreignKey)
|
||||
}
|
||||
}
|
||||
query = query.whereExists(whereCb(subQuery))
|
||||
break
|
||||
}
|
||||
}
|
||||
return query
|
||||
|
@ -502,6 +536,7 @@ class InternalBuilder {
|
|||
}
|
||||
function iterate(
|
||||
structure: AnySearchFilter,
|
||||
operation: SearchFilterKey,
|
||||
fn: (
|
||||
query: Knex.QueryBuilder,
|
||||
key: string,
|
||||
|
@ -558,9 +593,14 @@ class InternalBuilder {
|
|||
if (allOr) {
|
||||
query = query.or
|
||||
}
|
||||
query = builder.addRelationshipForFilter(query, updatedKey, q => {
|
||||
return handleRelationship(q, updatedKey, value)
|
||||
})
|
||||
query = builder.addRelationshipForFilter(
|
||||
query,
|
||||
allowEmptyRelationships[operation],
|
||||
updatedKey,
|
||||
q => {
|
||||
return handleRelationship(q, updatedKey, value)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -592,7 +632,7 @@ class InternalBuilder {
|
|||
return `[${value.join(",")}]`
|
||||
}
|
||||
if (this.client === SqlClient.POSTGRES) {
|
||||
iterate(mode, (q, key, value) => {
|
||||
iterate(mode, ArrayOperator.CONTAINS, (q, key, value) => {
|
||||
const wrap = any ? "" : "'"
|
||||
const op = any ? "\\?| array" : "@>"
|
||||
const fieldNames = key.split(/\./g)
|
||||
|
@ -610,7 +650,7 @@ class InternalBuilder {
|
|||
this.client === SqlClient.MARIADB
|
||||
) {
|
||||
const jsonFnc = any ? "JSON_OVERLAPS" : "JSON_CONTAINS"
|
||||
iterate(mode, (q, key, value) => {
|
||||
iterate(mode, ArrayOperator.CONTAINS, (q, key, value) => {
|
||||
return q[rawFnc](
|
||||
`${not}COALESCE(${jsonFnc}(${key}, '${stringifyArray(
|
||||
value
|
||||
|
@ -619,7 +659,7 @@ class InternalBuilder {
|
|||
})
|
||||
} else {
|
||||
const andOr = mode === filters?.containsAny ? " OR " : " AND "
|
||||
iterate(mode, (q, key, value) => {
|
||||
iterate(mode, ArrayOperator.CONTAINS, (q, key, value) => {
|
||||
let statement = ""
|
||||
const identifier = this.quotedIdentifier(key)
|
||||
for (let i in value) {
|
||||
|
@ -673,6 +713,7 @@ class InternalBuilder {
|
|||
const fnc = allOr ? "orWhereIn" : "whereIn"
|
||||
iterate(
|
||||
filters.oneOf,
|
||||
ArrayOperator.ONE_OF,
|
||||
(q, key: string, array) => {
|
||||
if (this.client === SqlClient.ORACLE) {
|
||||
key = this.convertClobs(key)
|
||||
|
@ -697,7 +738,7 @@ class InternalBuilder {
|
|||
)
|
||||
}
|
||||
if (filters.string) {
|
||||
iterate(filters.string, (q, key, value) => {
|
||||
iterate(filters.string, BasicOperator.STRING, (q, key, value) => {
|
||||
const fnc = allOr ? "orWhere" : "where"
|
||||
// postgres supports ilike, nothing else does
|
||||
if (this.client === SqlClient.POSTGRES) {
|
||||
|
@ -712,10 +753,10 @@ class InternalBuilder {
|
|||
})
|
||||
}
|
||||
if (filters.fuzzy) {
|
||||
iterate(filters.fuzzy, like)
|
||||
iterate(filters.fuzzy, BasicOperator.FUZZY, like)
|
||||
}
|
||||
if (filters.range) {
|
||||
iterate(filters.range, (q, key, value) => {
|
||||
iterate(filters.range, RangeOperator.RANGE, (q, key, value) => {
|
||||
const isEmptyObject = (val: any) => {
|
||||
return (
|
||||
val &&
|
||||
|
@ -781,7 +822,7 @@ class InternalBuilder {
|
|||
})
|
||||
}
|
||||
if (filters.equal) {
|
||||
iterate(filters.equal, (q, key, value) => {
|
||||
iterate(filters.equal, BasicOperator.EQUAL, (q, key, value) => {
|
||||
const fnc = allOr ? "orWhereRaw" : "whereRaw"
|
||||
if (this.client === SqlClient.MS_SQL) {
|
||||
return q[fnc](
|
||||
|
@ -801,7 +842,7 @@ class InternalBuilder {
|
|||
})
|
||||
}
|
||||
if (filters.notEqual) {
|
||||
iterate(filters.notEqual, (q, key, value) => {
|
||||
iterate(filters.notEqual, BasicOperator.NOT_EQUAL, (q, key, value) => {
|
||||
const fnc = allOr ? "orWhereRaw" : "whereRaw"
|
||||
if (this.client === SqlClient.MS_SQL) {
|
||||
return q[fnc](
|
||||
|
@ -822,13 +863,13 @@ class InternalBuilder {
|
|||
})
|
||||
}
|
||||
if (filters.empty) {
|
||||
iterate(filters.empty, (q, key) => {
|
||||
iterate(filters.empty, BasicOperator.EMPTY, (q, key) => {
|
||||
const fnc = allOr ? "orWhereNull" : "whereNull"
|
||||
return q[fnc](key)
|
||||
})
|
||||
}
|
||||
if (filters.notEmpty) {
|
||||
iterate(filters.notEmpty, (q, key) => {
|
||||
iterate(filters.notEmpty, BasicOperator.NOT_EMPTY, (q, key) => {
|
||||
const fnc = allOr ? "orWhereNotNull" : "whereNotNull"
|
||||
return q[fnc](key)
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue