// Copyright (c) 2023 Sourcefuse Technologies // // This software is released under the MIT License. // https://opensource.org/licenses/MIT import {AnyObject, Model, ShortHandEqualType} from '@loopback/repository'; import {HttpErrors} from '@loopback/rest'; import {Errors} from '../../const'; import {ColumnMap} from '../../types'; import {SearchQueryBuilder} from '../base'; export class PsqlQueryBuilder extends SearchQueryBuilder { unionString = ' UNION ALL '; schema: string; _placeholderIndex = 1; search( model: typeof Model, columns: Array | ColumnMap, ignoreColumns: (keyof T)[], ) { const tableName = this.getTableName(model); const sourceName = this.getModelName(model); const schemaName = this.getSchemaName(model); const {columnList, selectors} = this.getColumnListFromArrayOrMap( model, columns, ignoreColumns, ); if (!columnList) { throw new HttpErrors.BadRequest(Errors.NO_COLUMNS_TO_MATCH); } const where = this.whereBuild(model, this.query.where?.[sourceName]); const whereClause = [ `to_tsvector(${schemaName}.f_concat_ws(' ', ${columnList})) @@ to_tsquery($1)`, ]; if (where.sql) { whereClause.push(where.sql); } let query = `SELECT ${selectors}, '${sourceName}' as source, ts_rank_cd(to_tsvector(${schemaName}.f_concat_ws(' ', ${columnList})), to_tsquery($1)) as rank from ${schemaName}.${tableName} where ${whereClause.join(' AND ')}`; query = query.replace('\n', ''); this.baseQueryList.push({ sql: query, params: where.params, }); } paramString(index: number) { return `$${index}`; } paramsBuild(param: string): AnyObject | Array { const params = this.baseQueryList.reduce( (t: ShortHandEqualType[], v) => [...t, ...v.params], [], ); return [this._formatAndSanitize(param), ...params]; } getModelName(model: typeof Model) { const mappedName = this.modelNameMap.get(model.name); return ( mappedName ?? model.definition?.settings?.postgresql?.table ?? model.modelName ?? model.name.toLowerCase() ); } getSchemaName(model: typeof Model) { return ( this.schema ?? model.definition?.settings?.postgresql?.schema ?? `public` ); } _formatAndSanitize(param: string) { let result = param; while (result.endsWith('.')) { result = result.slice(0, -1); } return result .replace(/[!&|<>():'"]/g, ' ') // Remove PostgreSQL tsquery special chars .replace(/[^A-Za-z0-9.\s]/g, ' ') // Keep dots, remove other symbols .split(/\s+/) // Split on any whitespace (not just spaces) .filter(p => p && p !== '.') // Filter empty and standalone dots .map(p => `${p}:*`) // Add prefix match operator .join('<->'); // Join with followed-by operator } }