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 };