diff --git a/package.json b/package.json index 6c2929f..601cf3d 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@vuex-orm/plugin-search", "version": "0.25.0", "description": "Vuex ORM plugin for adding fuzzy search feature through model entities.", - "main": "dist/vuex-orm.cjs.js", + "main": "dist/vuex-orm-search.cjs.js", "browser": "dist/vuex-orm-search.esm-browser.js", "module": "dist/vuex-orm-search.esm-bundler.js", "unpkg": "dist/vuex-orm-search.global.js", diff --git a/src/VuexORMSearch.ts b/src/VuexORMSearch.ts index 8e2adac..c025f24 100644 --- a/src/VuexORMSearch.ts +++ b/src/VuexORMSearch.ts @@ -1,7 +1,7 @@ import { Query } from '@vuex-orm/core' -import Components from './contracts/Components' -import Options from './contracts/Options' -import Collection from './contracts/Collection' +import { Components } from './contracts/Components' +import { Options } from './contracts/Options' +import { Collection } from './contracts/Collection' import DefaultOptions from './config/DefaultOptions' import QueryMixin from './mixins/Query' @@ -25,7 +25,7 @@ export default class VuexORMSearch { } /** - * Plug in features. + * Plugin features. */ plugin(): void { this.mixQuery() diff --git a/src/config/DefaultOptions.ts b/src/config/DefaultOptions.ts index daf160f..7fffba4 100644 --- a/src/config/DefaultOptions.ts +++ b/src/config/DefaultOptions.ts @@ -1,16 +1,35 @@ -import Options from '../contracts/Options' +import * as Options from '../contracts/Options' -export const DefaultOptions: Options = { - distance: 100, - location: 0, - maxPatternLength: 32, - minMatchCharLength: 1, - searchPrimaryKey: false, - shouldSort: false, - threshold: 0.3, - tokenize: false, +export const matchOptions: Options.MatchOptions = { + includeMatches: false, + findAllMatches: false, + minMatchCharLength: 1 +} + +export const basicOptions: Options.BasicOptions = { + isCaseSensitive: false, + includeScore: false, keys: [], - verbose: false + shouldSort: false +} + +export const fuzzyOptions: Options.FuzzyOptions = { + location: 0, + threshold: 0.5, + distance: 100 +} + +export const advancedOptions: Options.AdvancedOptions = { + useExtendedSearch: false, + ignoreLocation: false, + ignoreFieldNorm: false +} + +export const defaultOptions: Options.Options = { + ...matchOptions, + ...basicOptions, + ...fuzzyOptions, + ...advancedOptions } -export default DefaultOptions +export default defaultOptions diff --git a/src/contracts/Collection.ts b/src/contracts/Collection.ts index 5e1f789..8dce0ed 100644 --- a/src/contracts/Collection.ts +++ b/src/contracts/Collection.ts @@ -1,5 +1,3 @@ import { Model } from '@vuex-orm/core' -export type Collection = Model[] - -export default Collection +export type Collection = M[] diff --git a/src/contracts/Components.ts b/src/contracts/Components.ts index e2d3910..d7dfd65 100644 --- a/src/contracts/Components.ts +++ b/src/contracts/Components.ts @@ -3,5 +3,3 @@ import { Query } from '@vuex-orm/core' export interface Components { Query: typeof Query } - -export default Components diff --git a/src/contracts/Options.ts b/src/contracts/Options.ts index 71dcf1d..c12f1c8 100644 --- a/src/contracts/Options.ts +++ b/src/contracts/Options.ts @@ -1,14 +1,31 @@ -export interface Options { - distance?: number - location?: number - maxPatternLength?: number - minMatchCharLength?: number - searchPrimaryKey?: boolean +import Fuse from 'fuse.js' + +export interface BasicOptions { + isCaseSensitive?: boolean + includeScore?: boolean + keys?: Fuse.FuseOptionKey[] shouldSort?: boolean +} + +export interface MatchOptions { + includeMatches?: boolean + findAllMatches?: boolean + minMatchCharLength?: number +} + +export interface FuzzyOptions { + location?: number threshold?: number - tokenize?: boolean - keys?: string[] - verbose?: boolean + distance?: number +} + +export interface AdvancedOptions { + useExtendedSearch?: boolean + ignoreLocation?: boolean + ignoreFieldNorm?: boolean } -export default Options +export type Options = BasicOptions & + MatchOptions & + FuzzyOptions & + AdvancedOptions diff --git a/src/contracts/Query.ts b/src/contracts/Query.ts new file mode 100644 index 0000000..277a0c9 --- /dev/null +++ b/src/contracts/Query.ts @@ -0,0 +1,7 @@ +import Fuse from 'fuse.js' + +export type SearchExpression = string | Fuse.Expression + +export type SearchPattern = string | string[] | Fuse.Expression + +export type SearchResults = Fuse.FuseResult[] diff --git a/src/index.cjs.ts b/src/index.cjs.ts index 773f609..b01b378 100644 --- a/src/index.cjs.ts +++ b/src/index.cjs.ts @@ -1,7 +1,7 @@ import './types/vuex-orm' -import Components from './contracts/Components' -import Options from './contracts/Options' +import { Components } from './contracts/Components' +import { Options } from './contracts/Options' import VuexORMSearch from './VuexORMSearch' export default { diff --git a/src/index.ts b/src/index.ts index fc3c586..2214303 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,11 @@ import './types/vuex-orm' -import Components from './contracts/Components' -import Options from './contracts/Options' +import { Components } from './contracts/Components' +import { Options } from './contracts/Options' import VuexORMSearch from './VuexORMSearch' -export { Options } +export * from './contracts/Query' +export * from './contracts/Options' export default { install(components: Components, installOptions: Options): void { diff --git a/src/mixins/Query.ts b/src/mixins/Query.ts index a41dc74..80d2d59 100644 --- a/src/mixins/Query.ts +++ b/src/mixins/Query.ts @@ -1,7 +1,6 @@ import Fuse from 'fuse.js' import { Query as BaseQuery } from '@vuex-orm/core' -import Options from '../contracts/Options' -import Collection from '../contracts/Collection' +import { Options } from '../contracts/Options' export default function Query(query: typeof BaseQuery, options: Options): void { /** @@ -14,25 +13,26 @@ export default function Query(query: typeof BaseQuery, options: Options): void { */ query.prototype.searchOptions = options + /** + * The raw search results. + */ + query.prototype.searchResults = [] + /** * Add search configurations. */ - query.prototype.search = function ( - terms: string | string[], - options: Options = {} - ): BaseQuery { - // If `terms` is single string, convert it to an array so we can use it - // consistently afterward. - this.searchTerms = Array.isArray(terms) ? terms : [terms] - - // If a user didn't provide `keys` option, set all model fields as default. - if ((this.searchOptions.keys as string[]).length === 0) { - this.searchOptions.keys = Object.keys( - this.model.cachedFields[this.model.entity] - ) + query.prototype.search = function (pattern, options = {}) { + // For backward-compat, transform Array to string type. + this.searchTerms = Array.isArray(pattern) + ? pattern.filter(Boolean).join(' ') + : pattern || null + + // If a user didn't provide a `keys` option, set it to all model fields by default. + if (!this.searchOptions.keys || this.searchOptions.keys.length === 0) { + this.searchOptions.keys = Object.keys(this.model.getFields()) } - // Finally, merge default options with users options. + // Merge default options with query options. this.searchOptions = { ...this.searchOptions, ...options } return this @@ -41,20 +41,15 @@ export default function Query(query: typeof BaseQuery, options: Options): void { /** * Filter the given record with fuzzy search by Fuse.js. */ - query.prototype.filterSearch = function (collection: Collection): Collection { - if ( - this.searchTerms === null || - this.searchTerms.filter(Boolean).length === 0 - ) { + query.prototype.filterSearch = function (collection) { + if (this.searchTerms === null || this.searchTerms === '') { return collection } const fuse = new Fuse(collection, this.searchOptions) - return this.searchTerms.reduce((carry, term) => { - carry.push(...fuse.search(term).map((result) => result.item)) + this.searchResults = fuse.search(this.searchTerms) - return carry - }, []) + return this.searchResults.map((result) => result.item) } } diff --git a/src/types/vuex-orm.ts b/src/types/vuex-orm.ts index 9ca7d9a..241422e 100644 --- a/src/types/vuex-orm.ts +++ b/src/types/vuex-orm.ts @@ -1,26 +1,36 @@ -import Options from '../contracts/Options' -import Collection from '../contracts/Collection' +import { Options } from '../contracts/Options' +import { Collection } from '../contracts/Collection' +import { + SearchResults, + SearchExpression, + SearchPattern +} from '../contracts/Query' declare module '@vuex-orm/core' { - interface Query { + interface Query { /** * The search terms. */ - searchTerms: string[] | null + searchTerms: SearchExpression | null /** * The search options. */ searchOptions: Options + /** + * The raw search results. + */ + searchResults: SearchResults + /** * Add search configurations. */ - search(terms: string | string[], options?: Options): this + search(terms: SearchPattern, options?: Options): this /** * Filter the given record with fuzzy search by Fuse.js. */ - filterSearch(records: Collection): Collection + filterSearch(records: Collection): Collection } } diff --git a/test/feature/Search.spec.ts b/test/feature/Search.spec.ts index cf15530..6115989 100644 --- a/test/feature/Search.spec.ts +++ b/test/feature/Search.spec.ts @@ -34,7 +34,7 @@ describe('Feature – Search', () => { expect(result[0].id).toBe(1) }) - it('can fuzzy search records by many terms', async () => { + it('can fuzzy search records by many terms (compat)', async () => { createStore([User]) await User.insert({ @@ -63,7 +63,7 @@ describe('Feature – Search', () => { ] }) - const result = User.query().search('').orderBy('id').get() + const result = User.query().search('').get() expect(result.length).toBe(3) }) @@ -95,7 +95,7 @@ describe('Feature – Search', () => { ] }) - const result = User.query().search(['rin', 'mail']).orderBy('id').get() + const result = User.query().search('rin mail').get() expect(result.length).toBe(1) }) @@ -112,10 +112,35 @@ describe('Feature – Search', () => { }) const result = User.query() - .search(['rin', 'mail'], { keys: ['name'] }) - .orderBy('id') + .search('rin mail', { keys: ['name'] }) .get() expect(result.length).toBe(1) }) + + it('exposes raw search results on a query instance', async () => { + createStore([User]) + + await User.insert({ + data: [ + { id: 1, name: 'John Walker', email: 'john@example.com' }, + { id: 2, name: 'Bobby Banana', email: 'mail.mail@example.com' }, + { id: 3, name: 'Ringo Looper', email: 'ringo.looper@example.com' } + ] + }) + + const query = User.query().search('walker', { + includeScore: true, + includeMatches: true + }) + const result = query.get() + + expect(result).toHaveLength(1) + expect(Object.keys(query.searchResults[0]).sort()).toEqual([ + 'item', + 'matches', + 'refIndex', + 'score' + ]) + }) }) diff --git a/test/support/Helpers.ts b/test/support/Helpers.ts index c077a24..375aaa4 100644 --- a/test/support/Helpers.ts +++ b/test/support/Helpers.ts @@ -1,7 +1,7 @@ import Vue from 'vue' import Vuex, { Store } from 'vuex' import VuexORM, { Database, Model } from '@vuex-orm/core' -import Options from '@/contracts/Options' +import { Options } from '@/contracts/Options' import VuexORMSearch from '@/index' export function createStore(