@meilisearch/instant-meilisearch
Version:
The search client to use Meilisearch with InstantSearch.
167 lines (148 loc) • 4.99 kB
text/typescript
import type { Filter, SearchContext } from '../../types/index.js'
const filterEscapeRegExp = /([\\"])/g
function getValueWithEscapedBackslashesAndQuotes(value: string): string {
return value.replace(filterEscapeRegExp, '\\$1')
}
/**
* Transform InstantSearch [facet
* filter](https://www.algolia.com/doc/api-reference/api-parameters/facetFilters/)
* to Meilisearch compatible filter format. Change sign from `:` to `=`
* "facet:facetValue" becomes "facet=facetValue"
*
* Wrap both the facet and its facet value between quotes. This avoids
* formatting issues on facets containing multiple words. Escape backslash \ and
* quote " characters.
*
* 'My facet:My facet value' becomes '"My facet":"My facet value"'
*
* @param {string} filter
* @returns {string}
*/
function transformFacetFilter(filter: string): string {
const escapedFilter = getValueWithEscapedBackslashesAndQuotes(filter)
const colonIndex = escapedFilter.indexOf(':')
const attribute = escapedFilter.slice(0, colonIndex)
const value = escapedFilter.slice(colonIndex + 1)
return `"${attribute}"="${value}"`
}
/**
* Transform InstantSearch [numeric
* filter](https://www.algolia.com/doc/api-reference/api-parameters/numericFilters/)
* to Meilisearch compatible filter format.
*
* 'price:5.99 TO 100' becomes '"price" 5.99 TO 100'
*
* 'price = 5.99' becomes '"price"=5.99'
*
* Wrap the attribute between quotes. Escape backslash \ and quote " characters.
*
* @param {string} filter
* @returns {string}
*/
function transformNumericFilter(filter: string): string {
const splitNumericFilter = (): [string, string, string] => {
const attributeMatch = filter.match(/^([^<!>:=]*)([<!>:=]+)(.*)$/)
if (attributeMatch) {
const [attribute, dirtyOperator, valueEnd] = attributeMatch.slice(1)
const operatorMatch = dirtyOperator.match(/^([<!>]?=|<|>|:){1}(.*)/) || [
'',
'',
]
const [operator, valueStart] = operatorMatch.slice(1)
const cleanedValue = valueStart + valueEnd
return [attribute, operator, cleanedValue]
}
return [filter, '', '']
}
const [attribute, operator, value] = splitNumericFilter()
const escapedAttribute = getValueWithEscapedBackslashesAndQuotes(attribute)
return `"${escapedAttribute.trim()}"${
operator === ':' ? ' ' : operator
}${value.trim()}`
}
/**
* Iterate over all filters. Return the filters in a Meilisearch compatible
* format.
*
* @param {(filter: string) => string} transformCallback
* @param {SearchContext['facetFilters']} filters
* @returns {Filter}
*/
function transformFilters(
transformCallback: (filter: string) => string,
filters: NonNullable<SearchContext['facetFilters']>
): Filter {
return typeof filters === 'string'
? transformCallback(filters)
: filters.map((filter) =>
typeof filter === 'string'
? transformCallback(filter)
: filter.map((nestedFilter) => transformCallback(nestedFilter))
)
}
/**
* Return the filter in an array if it is a string If filter is array, return
* without change.
*
* @param {Filter} [filter]
* @returns {Array | undefined}
*/
function filterToArray(filter?: Filter): Array<string | string[]> | undefined {
return typeof filter === 'string' ? [filter] : filter
}
/**
* Merge filters, transformedNumericFilters and transformedFacetFilters
* together.
*
* @param {string} filters
* @param {Filter} transformedNumericFilters
* @param {Filter} transformedFacetFilters
* @returns {Filter}
*/
function mergeFilters(
filters?: string,
transformedNumericFilters?: Filter,
transformedFacetFilters?: Filter
): Filter {
const adaptedNumericFilters = filterToArray(transformedNumericFilters)
const adaptedFacetFilters = filterToArray(transformedFacetFilters)
const adaptedFilters: Filter = []
if (filters !== undefined) {
adaptedFilters.push(filters)
}
if (adaptedNumericFilters !== undefined) {
adaptedFilters.push(...adaptedNumericFilters)
}
if (adaptedFacetFilters !== undefined) {
adaptedFilters.push(...adaptedFacetFilters)
}
return adaptedFilters
}
/**
* Adapt instantsearch.js filters to Meilisearch filters by combining and
* transforming all provided filters.
*
* @param {string | undefined} filters
* @param {SearchContext['numericFilters']} numericFilters
* @param {SearchContext['facetFilters']} facetFilters
* @returns {Filter}
*/
export function adaptFilters(
filters: string | undefined,
numericFilters: SearchContext['numericFilters'],
facetFilters: SearchContext['facetFilters']
): Filter {
const transformedNumericFilters =
numericFilters !== undefined
? transformFilters(transformNumericFilter, numericFilters)
: numericFilters
const transformedFacetFilters =
facetFilters !== undefined
? transformFilters(transformFacetFilter, facetFilters)
: facetFilters
return mergeFilters(
filters,
transformedNumericFilters,
transformedFacetFilters
)
}