UNPKG

feathers-mongodb-fuzzy-search

Version:

hook which adds fuzzy search for mongodb through $search in find query

106 lines (98 loc) 3.35 kB
const utils = require('@feathersjs/commons') const errors = require('@feathersjs/errors') /** * Adds " around `str` and removes any " in `str`. * @param {string} str */ function escape (str) { return '"' + str.replace(/"/g, '') + '"' } /** * Escapes characters in the string that are not safe to use in a RegExp. * @param {*} s The string to escape. If not a string, * it will be casted to one. * @return {string} A RegExp safe, escaped copy of {@code s}. * from https://github.com/google/closure-library/blob/master/closure/goog/string/string.js#L1148 */ function regExpEscape (s) { return String(s) .replace(/([-()\[\]{}+?*.$\^|,:#<!\\])/g, '\\$1') .replace(/\x08/g, '\\x08') } function transformSearchFieldsInQuery (query, options, fieldName) { utils._.each(query, (value, key) => { if (key === '$text') { return } // Process current attribute or recurse if (value && typeof value === 'object') { // Searchable field ? if (!value.hasOwnProperty('$search')) { return transformSearchFieldsInQuery(value, options, key) } // Field should either be included, or not excluded if ((options.fields.length && !options.fields.includes(key)) || options.excludedFields.includes(key)) { throw new errors.BadRequest('You are not allowed to perform $search on field ' + key) } /** * { * field: { * $search: '' * } * } */ transformSearchFieldsInQuery(value, options, key) } else if (key === '$search') { // Default to case insensitive if not given // Sanitize when required if (!options.fieldsNotEscaped.includes(fieldName)) { value = regExpEscape(value) } // Update query query['$regex'] = query.$caseSensitive ? new RegExp(value) : new RegExp(value, 'i') // Delete unused field delete query['$search'] delete query['$caseSensitive'] } }) } module.exports = function (opts = {}) { // if escape is undefined -> escape = true opts.escape = opts.escape !== false opts.fieldsNotEscaped = Array.isArray(opts.fieldsNotEscaped) ? opts.fieldsNotEscaped : [] // hook for full-text search or field-based search ? if (Array.isArray(opts.fields) && opts.fields.length) { opts.excludedFields = [] // one should not both include and exclude fields return regexFieldSearch } else if (Array.isArray(opts.excludedFields) && opts.excludedFields.length) { opts.fields = [] return regexFieldSearch } return fullTextSearch function regexFieldSearch (hook) { transformSearchFieldsInQuery(hook.params.query, opts) } function fullTextSearch (hook) { if (hook.id || !hook.params.query || !hook.params.query.$search) { return } let query = hook.params.query query.$text = { $search: opts.escape ? escape(query.$search) : query.$search } delete query.$search if (query.$language) { query.$text.$language = query.$language delete query.$language } if (query.$caseSensitive) { query.$text.$caseSensitive = query.$caseSensitive delete query.$caseSensitive } if (query.$diacriticSensitive) { query.$text.$diacriticSensitive = query.$diacriticSensitive delete query.$diacriticSensitive } } }