const axios = require('axios') class TsApiFeatures { constructor(client, collectionName, queryString) { this.client = client this.collectionName = collectionName this.queryString = queryString this.queryOptions = {} } search() { // Set `q` to provided search term or default to `*` for all documents this.queryOptions.q = this.queryString.query || '*' // Set `query_by` to specified fields or default to `*` this.queryOptions.query_by = this.queryString.query_by || 'name' return this } filter() { let filters = [] let rawKeys = [] // Add any additional filter conditions from queryString, if needed Object.keys(this.queryString).forEach((key) => { let operatorConditions = [] let normalValues = [] if ( [ 'page', 'sort', 'limit', 'fields', 'per_page', 'filter', 'query', 'facet_by', 'query_by', 'highlight_full_fields', ].includes(key) ) return let value = this.queryString[key] rawKeys.push(key) if (!Array.isArray(value)) { value = [value] } if (/\b(gte|gt|lte|lt|ne|eq)\b/.test(value)) { const operatorMap = { gte: '>=', gt: '>', lte: '<=', lt: '<', ne: '!=', eq: '=' } const match = value.match(/\b(gte|gt|lte|lt|ne|eq)\b/) if (match) { const operator = operatorMap[match[0]] const cleanValue = value.replace(match[0], '').trim() operatorConditions.push(`${key}${operator}${cleanValue}`) } } else { normalValues.push(value) } if (operatorConditions.length > 0 && normalValues.length > 0) { // Add all operator conditions operatorConditions.forEach((cond) => filters.push(cond)) // Add the normal values condition if (normalValues.length === 1) { filters.push(`${key}:=${normalValues[0]}`) } else { filters.push(`${key}:=[${normalValues.join(',')}]`) } } else if (operatorConditions.length > 0) { // Only operators operatorConditions.forEach((cond) => filters.push(cond)) } else { // Only normal values if (normalValues.length === 1) { filters.push(`${key}:=${normalValues[0]}`) } else if (normalValues.length > 1) { filters.push(`${key}:=[${normalValues.join(',')}]`) } } }) // Join all conditions for TypeSense filter if (filters.length) { this.queryOptions.filters = filters.join(' && ') } const searchFilters = [] rawKeys.forEach((value, index) => { const tempArray = JSON.parse(JSON.stringify(filters)) tempArray.splice(index, 1) searchFilters.push(tempArray.join(' && ')) }) this.filterKeys = rawKeys this.searchFilters = searchFilters return this } sort() { if (this.queryString.sort) { this.queryOptions.sort_by = this.queryString.sort.split(',').join(',') } else { this.queryOptions.sort_by = 'createdAt:desc' } return this } limitFields() { if (this.queryString.fields) { this.queryOptions.include_fields = this.queryString.fields.split(',').join(',') } else { this.queryOptions.exclude_fields = '__v' } return this } facet() { // Set `facet_by` based on the queryString, if available if (this.queryString.facet_by) { this.queryOptions.facet_by = this.queryString.facet_by this.queryOptions.highlight_full_fields = this.queryString.highlight_full_fields this.queryOptions.exhaustive_search = true } return this } paginate() { const page = parseInt(this.queryString.page) || 1 const per_page = parseInt(this.queryString.per_page) || 20 this.queryOptions.page = page this.queryOptions.per_page = per_page return this } async execute() { const searches = [ { collection: this.collectionName, exhaustive_search: this.queryOptions.exhaustive_search, query_by: this.queryOptions.query_by, filter_by: this.queryOptions.filters, highlight_full_fields: this.queryOptions.highlight_full_fields, q: this.queryOptions.q, facet_by: this.queryOptions.facet_by, max_facet_values: 100, page: this.queryOptions.page, per_page: this.queryOptions.per_page, }, ] this.filterKeys.forEach((value, index) => { searches.push({ collection: this.collectionName, exhaustive_search: this.queryOptions.exhaustive_search, query_by: this.queryOptions.query_by, filter_by: this.searchFilters[index], highlight_full_fields: this.queryOptions.highlight_full_fields, q: this.queryOptions.q, facet_by: value, max_facet_values: 100, page: this.queryOptions.page, per_page: this.queryOptions.per_page, }) }) const config = { url: 'https://' + process.env.TYPE_SENSE_URL + '/multi_search?x-typesense-api-key=' + process.env.TYPE_SENSE_API_KEY, method: 'POST', data: { searches, }, } const request = await axios(config) const results = request.data.results[0] let facet_counts = results.facet_counts this.filterKeys.forEach((value, index) => { const objIndex = facet_counts.findIndex((item) => item.field_name === value) facet_counts[objIndex] = request.data.results[index + 1].facet_counts[0] }) return { facets: facet_counts, items: results.hits.map((hit) => hit.document), totalCount: results.found, pagesCount: Math.ceil(results.found / this.queryOptions.per_page), } } } module.exports = TsApiFeatures