162 lines
4.8 KiB
TypeScript
162 lines
4.8 KiB
TypeScript
import type {
|
|
QueryCondition,
|
|
QueryConditionNumber,
|
|
QueryConditionText,
|
|
QueryFilter,
|
|
} from '@morten-olsen/stash-query-dsl';
|
|
import { type Knex } from 'knex';
|
|
/**
|
|
* Escapes a JSON key for use in PostgreSQL JSON operators.
|
|
* Escapes single quotes by doubling them, which is the PostgreSQL standard.
|
|
*/
|
|
const escapeJsonKey = (key: string): string => {
|
|
return key.replace(/'/g, "''");
|
|
};
|
|
|
|
const getFieldSelector = (query: Knex.QueryBuilder, field: string[], tableName?: string) => {
|
|
const baseColumn = field[0];
|
|
if (field.length === 1) {
|
|
return tableName ? `${tableName}.${baseColumn}` : baseColumn;
|
|
}
|
|
|
|
const baseFieldRef = tableName ? query.client.ref(baseColumn).withSchema(tableName) : query.client.ref(baseColumn);
|
|
const jsonPath = field.slice(1);
|
|
let sqlExpression = baseFieldRef.toString();
|
|
|
|
for (let i = 0; i < jsonPath.length - 1; i++) {
|
|
const escapedKey = escapeJsonKey(jsonPath[i]);
|
|
sqlExpression += ` -> '${escapedKey}'`;
|
|
}
|
|
|
|
const finalElement = jsonPath[jsonPath.length - 1];
|
|
const escapedFinalKey = escapeJsonKey(finalElement);
|
|
sqlExpression += ` ->> '${escapedFinalKey}'`;
|
|
return query.client.raw(sqlExpression);
|
|
};
|
|
|
|
const applyQueryConditionText = (query: Knex.QueryBuilder, { field, tableName, conditions }: QueryConditionText) => {
|
|
const selector = getFieldSelector(query, field, tableName);
|
|
if (conditions.equal) {
|
|
query = query.where(selector, '=', conditions.equal);
|
|
}
|
|
if (conditions.notEqual) {
|
|
query = query.where(selector, '<>', conditions.notEqual);
|
|
}
|
|
if (conditions.like) {
|
|
query = query.whereLike(selector, conditions.like);
|
|
}
|
|
if (conditions.notLike) {
|
|
query = query.not.whereLike(selector, conditions.notLike);
|
|
}
|
|
if (conditions.equal === null) {
|
|
query = query.whereNull(selector);
|
|
}
|
|
if (conditions.notEqual === null) {
|
|
query = query.whereNotNull(selector);
|
|
}
|
|
if (conditions.in) {
|
|
query = query.whereIn(selector, conditions.in);
|
|
}
|
|
if (conditions.notIn) {
|
|
query = query.whereNotIn(selector, conditions.notIn);
|
|
}
|
|
return query;
|
|
};
|
|
|
|
const applyQueryConditionNumber = (
|
|
query: Knex.QueryBuilder,
|
|
{ field, tableName, conditions }: QueryConditionNumber,
|
|
) => {
|
|
const selector = getFieldSelector(query, field, tableName);
|
|
if (conditions.equals !== undefined && conditions.equals !== null) {
|
|
query = query.where(selector, '=', conditions.equals);
|
|
}
|
|
if (conditions.notEquals !== undefined && conditions.notEquals !== null) {
|
|
query = query.where(selector, '<>', conditions.notEquals);
|
|
}
|
|
if (conditions.equals === null) {
|
|
query = query.whereNull(selector);
|
|
}
|
|
if (conditions.notEquals === null) {
|
|
query = query.whereNotNull(selector);
|
|
}
|
|
if (conditions.greaterThan) {
|
|
query = query.where(selector, '>', conditions.greaterThan);
|
|
}
|
|
if (conditions.greaterThanOrEqual) {
|
|
query = query.where(selector, '>=', conditions.greaterThanOrEqual);
|
|
}
|
|
if (conditions.lessThan) {
|
|
query = query.where(selector, '<', conditions.lessThan);
|
|
}
|
|
if (conditions.lessThanOrEqual) {
|
|
query = query.where(selector, '<=', conditions.lessThanOrEqual);
|
|
}
|
|
if (conditions.in) {
|
|
query = query.whereIn(selector, conditions.in);
|
|
}
|
|
if (conditions.notIn) {
|
|
query = query.whereNotIn(selector, conditions.notIn);
|
|
}
|
|
return query;
|
|
};
|
|
|
|
const applyQueryCondition = (query: Knex.QueryBuilder, options: QueryCondition) => {
|
|
switch (options.type) {
|
|
case 'text': {
|
|
return applyQueryConditionText(query, options);
|
|
}
|
|
case 'number': {
|
|
return applyQueryConditionNumber(query, options);
|
|
}
|
|
default: {
|
|
throw new Error(`Unknown filter type`);
|
|
}
|
|
}
|
|
};
|
|
|
|
const applyQueryFilter = (query: Knex.QueryBuilder, filter: QueryFilter) => {
|
|
if (filter.type === 'operator') {
|
|
if (filter.conditions.length === 0) {
|
|
return query;
|
|
}
|
|
|
|
switch (filter.operator) {
|
|
case 'or': {
|
|
return query.where((subquery) => {
|
|
let isFirst = true;
|
|
for (const condition of filter.conditions) {
|
|
if (isFirst) {
|
|
applyQueryFilter(subquery, condition);
|
|
isFirst = false;
|
|
} else {
|
|
subquery.orWhere((subSubquery) => {
|
|
applyQueryFilter(subSubquery, condition);
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
case 'and': {
|
|
return query.where((subquery) => {
|
|
let isFirst = true;
|
|
for (const condition of filter.conditions) {
|
|
if (isFirst) {
|
|
applyQueryFilter(subquery, condition);
|
|
isFirst = false;
|
|
} else {
|
|
subquery.andWhere((subSubquery) => {
|
|
applyQueryFilter(subSubquery, condition);
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
} else {
|
|
return applyQueryCondition(query, filter);
|
|
}
|
|
};
|
|
|
|
export { applyQueryCondition, applyQueryFilter };
|