├── .editorconfig
├── .gitattributes
├── .gitignore
├── .npmignore
├── .prettierrc
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── jest.config.js
├── package.json
├── rollup.config.js
├── scripts
├── build.js
└── release.js
├── src
├── VuexORMSearch.ts
├── config
│ └── DefaultOptions.ts
├── contracts
│ ├── Collection.ts
│ ├── Components.ts
│ └── Options.ts
├── index.cjs.ts
├── index.ts
├── mixins
│ └── Query.ts
└── types
│ ├── global.d.ts
│ └── vuex-orm.ts
├── test
├── feature
│ └── Search.spec.ts
└── support
│ └── Helpers.ts
├── tsconfig.json
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | insert_final_newline = true
6 | trim_trailing_whitespace = true
7 | indent_style = space
8 | indent_size = 2
9 |
10 | [*.md]
11 | trim_trailing_whitespace = false
12 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.nyc_output
2 | /.tmp
3 | /coverage
4 | /dist
5 | /lib
6 | /node_modules
7 | .idea/*
8 | .vscode/*
9 | yarn-error.log
10 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | /build
2 | /coverage
3 | /test
4 | .*
5 | logo-vuex-orm.png
6 | tsconfig.build.json
7 | tsconfig.json
8 | tslint.json
9 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "always",
3 | "endOfLine": "lf",
4 | "printWidth": 80,
5 | "semi": false,
6 | "singleQuote": true,
7 | "trailingComma": "none"
8 | }
9 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "10"
4 | - "12"
5 | script:
6 | - npm run lint:fail
7 | - npm run coverage
8 | - cat ./coverage/lcov.info | ./node_modules/.bin/codecov
9 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## [0.24.1](https://github.com/vuex-orm/plugin-search/compare/v0.24.0...v0.24.1) (2019-11-25)
2 |
3 | ## Improvements
4 |
5 | - Completely rewrite to adopt to latest Vuex ORM
6 |
7 |
8 | # [0.24.0](https://github.com/vuex-orm/plugin-search/compare/0.23.4...v0.24.0) (2019-07-30)
9 |
10 | - Fix default model keys to search
11 |
12 |
13 | ## [0.23.4](https://github.com/vuex-orm/plugin-search/compare/0.23.3...0.23.4) (2018-03-24)
14 |
15 | - Fix persisted query hooks
16 |
17 |
18 | ## [0.23.3](https://github.com/vuex-orm/plugin-search/compare/0.23.2...0.23.3) (2018-03-21)
19 |
20 | - Fix persisted query hooks
21 |
22 |
23 | ## [0.23.2](https://github.com/vuex-orm/plugin-search/compare/0.1.7...0.23.2) (2018-03-21)
24 |
25 | - Fixed breaking changes from core package
26 |
27 |
28 | ## [0.1.7](https://github.com/vuex-orm/plugin-search/compare/0.1.6...0.1.7) (2018-01-23)
29 |
30 | - Updated to use new @vuex-orm scope
31 | - Allow search term parameter to use an array of terms for more precise results
32 | - Updated readme docs for API update and provide another use example
33 |
34 |
35 | ## 0.1.6 (2018-01-18)
36 |
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Conan Crawford and Kia Ishii
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Vuex ORM Plugin: Search
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | Vuex ORM Search plugin adds a `search()` query chain modifier to easily filter matched records using fuzzy search logic from the [Fuse.js](http://fusejs.io/) library.
23 |
24 | A simple example to search for '_john_' within your query:
25 |
26 | ```js
27 | const users = User.query().search('john').get()
28 | ```
29 |
30 | Sponsors
31 |
32 | Vuex ORM is sponsored by awesome folks. Big love to all of them from whole Vuex ORM community :two_hearts:
33 |
34 | Super Love Sponsors
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | Big Love Sponsors
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 | A Love Sponsors
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 | ## Installation
89 |
90 | Install `@vuex-orm/plugin-search` alongside Vuex ORM.
91 |
92 | ```bash
93 | npm install @vuex-orm/core @vuex-orm/plugin-search --save
94 | # OR
95 | yarn add @vuex-orm/core @vuex-orm/plugin-search
96 | ```
97 |
98 | Then install the plugin using Vuex ORM `use()` method.
99 |
100 | ```js
101 | import VuexORM from '@vuex-orm/core'
102 | import VuexORMSearch from '@vuex-orm/plugin-search'
103 |
104 | VuexORM.use(VuexORMSearch, {
105 | // Configure default fuse.js options here (see "Configuration" section below).
106 | })
107 | ```
108 |
109 | ### API
110 |
111 | The search plugin method accepts two parameters.
112 |
113 | ```ts
114 | search(terms: string | string[], options: Object): Query
115 | ```
116 |
117 | - `terms` – any string or an array of strings.
118 | - `options` – see the [Configurations](#configurations) section below.
119 |
120 | > **NOTE:** If passing an array of search terms, the results assume some element in the array to be matched.
121 |
122 | ## Configurations
123 |
124 | The plugin provides opinionated default Fuse.js options for token-based matching for optimum performance. These options can be easily changed at two stages of the plugin lifecycle:
125 |
126 | - Plugin installation (sets the global default options).
127 | - Runtime within the **search()** query chain.
128 |
129 | See: [Fuse.js](http://fusejs.io/) for demo.
130 |
131 | | Property | Description | Default |
132 | | --- | --- | --- |
133 | | searchPrimaryKey | Also search the primary key | `false` |
134 | | location | Approximately where in the text is the pattern expected to be found | `0` |
135 | | distance | Determines how close the match must be to the fuzzy location | `100` |
136 | | threshold | **0.0** requires a perfect match, and a threshold of **1.0** would match anything | `0.3` |
137 | | maxPatternLength | Machine word size | `32` |
138 | | caseSensitive | Indicates whether comparisons should be case sensitive | `false` |
139 | | tokenSeparator | Regex used to separate words when searching. Only applicable when **tokenize** is **true** | `/ +/g` |
140 | | findAllMatches | When true, the algorithm continues searching to even if a perfect match is found | `false` |
141 | | minMatchCharLength | Minimum number of characters that must be matched before a result is considered a match | `1` |
142 | | keys | An array of fields (columns) to be included in the search | Keys from `Model.fields()` |
143 | | shouldSort | Whether to sort the result list, by score | `false` |
144 | | tokenize | When true, the search algorithm will search individual words **and** the full string, computing the final score as a function of both. **NOTE**: that when _tokenize_ is _true_, the **threshold**, **distance**, and **location** are inconsequential for individual tokens | `false` |
145 | | matchAllTokens | When true, the result set will only include records that match all tokens. Will only work if **tokenize** is also true. **NOTE**: It is better to use multiple **.search()** query chains if you have multiple terms that need to match. | `false` |
146 | | verbose | Will print to the console. Useful for debugging. | `false` |
147 |
148 | ## Option Examples
149 |
150 | Here are some examples on how to use the search plugin with case specific options.
151 |
152 | ### During Plugin Install
153 |
154 | For example, if we want to match based on case sensitive and no fuzzy search logic (perfect match).
155 |
156 | ```js
157 | VuexORM.use(VuexORMSearch, {
158 | caseSensitive: true,
159 | threshold: 0
160 | })
161 | ```
162 |
163 | ### During Query Chain
164 |
165 | The global install options will now default to case sensitive and no fuzzy logic, but for example we have a run-time case we need to ignore case and implement a slightly more strict fuzzy search threshold.
166 |
167 | Let's also specify the need to only search the `firstName` and `lastName` fields.
168 |
169 | ```js
170 | const users = User.query().search('john', {
171 | caseSensitive: false,
172 | threshold: 0.3,
173 | keys: ['firstName', 'lastName']
174 | }).get()
175 | ```
176 |
177 | ### Finding Results Matching Multiple Terms
178 |
179 | Let's find all matches where both `pat` and `male` exist in our records, and sort by the date added.
180 |
181 | ```javascript
182 | const data = User.query().search(['pat', 'male'], {
183 | keys: ['firstName', 'gender']
184 | }).get()
185 | ```
186 |
187 | ## Questions & Discussions
188 |
189 | Join us on our [Slack Channel](https://join.slack.com/t/vuex-orm/shared_invite/enQtNDQ0NjE3NTgyOTY2LTc1YTI2N2FjMGRlNGNmMzBkMGZlMmYxOTgzYzkzZDM2OTQ3OGExZDRkN2FmMGQ1MGJlOWM1NjU0MmRiN2VhYzQ) for any questions and discussions.
190 |
191 | Although there is the Slack Channel, do not hesitate to open an [issue](https://github.com/vuex-orm/plugin-search/issues) for any question you might have. We're always more than happy to hear any feedback, and we don't care what kind of form they are.
192 |
193 | ## Plugins
194 |
195 | Vuex ORM can be extended via plugins to add additional features. Here is a list of available plugins.
196 |
197 | - [Vuex ORM Axios](https://github.com/vuex-orm/plugin-axios) – The plugin to sync the store against a RESTful API.
198 | - [Vuex ORM GraphQL](https://github.com/vuex-orm/plugin-graphql) – The plugin to sync the store against a [GraphQL](https://graphql.org) API.
199 | - [Vuex ORM Change Flags](https://github.com/vuex-orm/plugin-change-flags) - Vuex ORM plugin for adding IsDirty / IsNew flags to model entities.
200 | - [Vuex ORM Soft Delete](https://github.com/vuex-orm/plugin-soft-delete) – Vuex ORM plugin for adding soft delete feature to model entities.
201 |
202 | ## Contribution
203 |
204 | We are excited that you are interested in contributing to Vuex ORM Plugin: Search! Anything from raising an issue, submitting an idea of a new feature, or making a pull request is welcome! Before submitting your contribution though, please make sure to take a moment and read through the following guidelines.
205 |
206 | ### Pull Request Guidelines
207 |
208 | When submitting a new pull request, please make sure to follow these guidelines:
209 |
210 | - **For feature requests:** Checkout a topic branch from `dev` branch, and merge back against `dev` branch.
211 | - **For bug fixes:** Checkout a topic branch from `master` branch, and merge back against `master` branch.
212 |
213 | These rules also apply to the documentation. If you're submitting documentation about a new feature that isn't released yet, you must checkout the `dev` branch, but for non-functional updates, such as fixing a typo, you may checkout and commit to the `master` branch.
214 |
215 | ### Scripts
216 |
217 | There are several scripts to help with development.
218 |
219 | ```bash
220 | yarn build
221 | ```
222 |
223 | Compile files and generate bundles in `dist` directory.
224 |
225 | ```bash
226 | yarn lint
227 | ```
228 |
229 | Lint files using [Prettier](https://prettier.io/).
230 |
231 | ```bash
232 | yarn test
233 | ```
234 |
235 | Run the test using [Jest](https://jestjs.io/).
236 |
237 | ```bash
238 | yarn test:watch
239 | ```
240 |
241 | Run the test in watch mode.
242 |
243 | ```bash
244 | yarn coverage
245 | ```
246 |
247 | Generate test coverage in `coverage` directory.
248 |
249 | ## License
250 |
251 | Vuex ORM Plugin: Search is open-sourced software licensed under the [MIT license](./LICENSE).
252 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest',
3 | rootDir: __dirname,
4 | globals: {
5 | __DEV__: true
6 | },
7 | moduleNameMapper: {
8 | '^@/(.*)$': '/src/$1',
9 | '^test/(.*)$': '/test/$1'
10 | },
11 | testMatch: ['/test/**/*.spec.ts'],
12 | testPathIgnorePatterns: ['/node_modules/'],
13 | coverageDirectory: 'coverage',
14 | coverageReporters: ['json', 'lcov', 'text-summary', 'clover'],
15 | collectCoverageFrom: ['src/**/*.ts', '!src/index.cjs.ts']
16 | }
17 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@vuex-orm/plugin-search",
3 | "version": "0.24.1",
4 | "description": "Vuex ORM plugin for adding fuzzy search feature through model entities.",
5 | "main": "dist/vuex-orm-search.cjs.js",
6 | "browser": "dist/vuex-orm-search.esm.js",
7 | "module": "dist/vuex-orm-search.esm-bundler.js",
8 | "unpkg": "dist/vuex-orm-search.global.js",
9 | "typings": "dist/src/index.d.ts",
10 | "files": [
11 | "dist"
12 | ],
13 | "scripts": {
14 | "build": "node scripts/build.js",
15 | "clean": "rm -rf dist && rm -rf dist && rm -rf coverage && rm -rf .nyc_output && rm -rf .tmp",
16 | "lint": "prettier --write --parser typescript \"{src,test}/**/*.ts\"",
17 | "lint:fail": "prettier --check --parser typescript \"{src,test}/**/*.ts\"",
18 | "test": "jest",
19 | "test:watch": "jest --watch",
20 | "coverage": "jest --collect-coverage",
21 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
22 | "release": "node scripts/release.js"
23 | },
24 | "repository": {
25 | "type": "git",
26 | "url": "git+https://github.com/vuex-orm/plugin-search.git"
27 | },
28 | "keywords": [
29 | "vue",
30 | "vuex",
31 | "vuex-plugin",
32 | "vuex-orm",
33 | "fuzzy search"
34 | ],
35 | "author": "Conan Crawford",
36 | "contributors": [
37 | "Kia Ishii"
38 | ],
39 | "license": "MIT",
40 | "bugs": {
41 | "url": "https://github.com/vuex-orm/plugin-search/issues"
42 | },
43 | "peerDependencies": {
44 | "@vuex-orm/core": ">=0.34.0"
45 | },
46 | "dependencies": {
47 | "fuse.js": "^3.4.6"
48 | },
49 | "devDependencies": {
50 | "@rollup/plugin-commonjs": "^11.0.2",
51 | "@rollup/plugin-node-resolve": "^7.1.1",
52 | "@rollup/plugin-replace": "^2.3.1",
53 | "@types/jest": "^24.0.23",
54 | "@vuex-orm/core": "^0.36.3",
55 | "brotli": "^1.3.2",
56 | "chalk": "^3.0.0",
57 | "codecov": "^3.6.5",
58 | "conventional-changelog-cli": "^2.0.31",
59 | "enquirer": "^2.3.4",
60 | "execa": "^4.0.0",
61 | "jest": "^25.2.4",
62 | "prettier": "1.19.1",
63 | "rollup": "^2.3.2",
64 | "rollup-plugin-terser": "^5.3.0",
65 | "rollup-plugin-typescript2": "^0.27.0",
66 | "semver": "^7.1.3",
67 | "ts-jest": "^25.3.0",
68 | "typescript": "^3.8.3",
69 | "vue": "^2.6.11",
70 | "vuex": "^3.1.3"
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import replace from '@rollup/plugin-replace'
3 | import resolve from '@rollup/plugin-node-resolve'
4 | import commonjs from '@rollup/plugin-commonjs'
5 | import ts from 'rollup-plugin-typescript2'
6 | import { terser } from 'rollup-plugin-terser'
7 |
8 | const configs = [
9 | { input: 'src/index.ts', file: 'dist/vuex-orm-search.esm.js', format: 'es', browser: true, env: 'development' },
10 | { input: 'src/index.ts', file: 'dist/vuex-orm-search.esm.prod.js', format: 'es', browser: true, env: 'production' },
11 | { input: 'src/index.ts', file: 'dist/vuex-orm-search.esm-bundler.js', format: 'es', env: 'development' },
12 | { input: 'src/index.cjs.ts', file: 'dist/vuex-orm-search.global.js', format: 'iife', env: 'development' },
13 | { input: 'src/index.cjs.ts', file: 'dist/vuex-orm-search.global.prod.js', format: 'iife', minify: true, env: 'production' },
14 | { input: 'src/index.cjs.ts', file: 'dist/vuex-orm-search.cjs.js', format: 'cjs', env: 'development' }
15 | ]
16 |
17 | function createEntries() {
18 | return configs.map((c) => createEntry(c))
19 | }
20 |
21 | function createEntry(config) {
22 | const c = {
23 | input: config.input,
24 | plugins: [],
25 | output: {
26 | file: config.file,
27 | format: config.format,
28 | globals: {
29 | vue: 'Vue'
30 | }
31 | },
32 | onwarn: (msg, warn) => {
33 | if (!/Circular/.test(msg)) {
34 | warn(msg)
35 | }
36 | }
37 | }
38 |
39 | if (config.format === 'iife') {
40 | c.output.name = 'VuexORMSearch'
41 | }
42 |
43 | c.plugins.push(replace({
44 | __DEV__: config.format === 'es' && !config.browser
45 | ? `(process.env.NODE_ENV !== 'production')`
46 | : config.env !== 'production'
47 | }))
48 |
49 | c.plugins.push(resolve())
50 | c.plugins.push(commonjs())
51 |
52 | c.plugins.push(ts({
53 | check: config.format === 'es' && config.browser && config.env === 'development',
54 | tsconfig: path.resolve(__dirname, 'tsconfig.json'),
55 | cacheRoot: path.resolve(__dirname, 'node_modules/.rts2_cache'),
56 | tsconfigOverride: {
57 | compilerOptions: {
58 | declaration: config.format === 'es' && config.browser && config.env === 'development',
59 | target: config.format === 'iife' || config.format === 'cjs' ? 'es5' : 'es2018'
60 | },
61 | exclude: ['test']
62 | }
63 | }))
64 |
65 | if (config.minify) {
66 | c.plugins.push(terser({ module: config.format === 'es' }))
67 | }
68 |
69 | return c
70 | }
71 |
72 | export default createEntries()
73 |
--------------------------------------------------------------------------------
/scripts/build.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs-extra')
2 | const chalk = require('chalk')
3 | const execa = require('execa')
4 | const { gzipSync } = require('zlib')
5 | const { compress } = require('brotli')
6 |
7 | const files = [
8 | 'dist/vuex-orm-search.esm.js',
9 | 'dist/vuex-orm-search.esm.prod.js',
10 | 'dist/vuex-orm-search.esm-bundler.js',
11 | 'dist/vuex-orm-search.global.js',
12 | 'dist/vuex-orm-search.global.prod.js',
13 | 'dist/vuex-orm-search.cjs.js'
14 | ]
15 |
16 | async function run() {
17 | await build()
18 | checkAllSizes()
19 | }
20 |
21 | async function build() {
22 | await fs.remove('dist')
23 |
24 | await execa('rollup', ['-c', 'rollup.config.js'], { stdio: 'inherit' })
25 | }
26 |
27 | function checkAllSizes() {
28 | console.log()
29 | files.map((f) => checkSize(f))
30 | console.log()
31 | }
32 |
33 | function checkSize(file) {
34 | const f = fs.readFileSync(file)
35 | const minSize = (f.length / 1024).toFixed(2) + 'kb'
36 | const gzipped = gzipSync(f)
37 | const gzippedSize = (gzipped.length / 1024).toFixed(2) + 'kb'
38 | const compressed = compress(f)
39 | const compressedSize = (compressed.length / 1024).toFixed(2) + 'kb'
40 | console.log(
41 | `${chalk.gray(
42 | chalk.bold(file)
43 | )} size:${minSize} / gzip:${gzippedSize} / brotli:${compressedSize}`
44 | )
45 | }
46 |
47 | run()
48 |
--------------------------------------------------------------------------------
/scripts/release.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const path = require('path')
3 | const chalk = require('chalk')
4 | const semver = require('semver')
5 | const { prompt } = require('enquirer')
6 | const execa = require('execa')
7 | const currentVersion = require('../package.json').version
8 |
9 | const versionIncrements = [
10 | 'patch',
11 | 'minor',
12 | 'major',
13 | 'prepatch',
14 | 'preminor',
15 | 'premajor',
16 | 'prerelease'
17 | ]
18 |
19 | const inc = (i) => semver.inc(currentVersion, i, 'beta')
20 | const bin = (name) => path.resolve(__dirname, `../node_modules/.bin/${name}`)
21 | const run = (bin, args, opts = {}) => execa(bin, args, { stdio: 'inherit', ...opts })
22 | const step = (msg) => console.log(chalk.cyan(msg))
23 |
24 | async function main() {
25 | let targetVersion
26 |
27 | const { release } = await prompt({
28 | type: 'select',
29 | name: 'release',
30 | message: 'Select release type',
31 | choices: versionIncrements.map(i => `${i} (${inc(i)})`).concat(['custom'])
32 | })
33 |
34 | if (release === 'custom') {
35 | targetVersion = (await prompt({
36 | type: 'input',
37 | name: 'version',
38 | message: 'Input custom version',
39 | initial: currentVersion
40 | })).version
41 | } else {
42 | targetVersion = release.match(/\((.*)\)/)[1]
43 | }
44 |
45 | if (!semver.valid(targetVersion)) {
46 | throw new Error(`Invalid target version: ${targetVersion}`)
47 | }
48 |
49 | const { yes } = await prompt({
50 | type: 'confirm',
51 | name: 'yes',
52 | message: `Releasing v${targetVersion}. Confirm?`
53 | })
54 |
55 | if (!yes) {
56 | return
57 | }
58 |
59 | // Run tests before release.
60 | step('\nRunning tests...')
61 | await run(bin('jest'), ['--clearCache'])
62 | await run('yarn', ['lint:fail'])
63 | await run('yarn', ['test'])
64 |
65 | // Update the package version.
66 | step('\nUpdating the package version...')
67 | updatePackage(targetVersion)
68 |
69 | // Build the package.
70 | step('\nBuilding the package...')
71 | await run('yarn', ['build'])
72 |
73 | // Generate the changelog.
74 | step('\nGenerating the changelog...')
75 | await run('yarn', ['changelog'])
76 |
77 | // Commit changes to the Git.
78 | step('\nCommitting changes...')
79 | await run('git', ['add', '-A'])
80 | await run('git', ['commit', '-m', `release: v${targetVersion}`])
81 |
82 | // Publish the package.
83 | step('\nPublishing the package...')
84 | await run('npm', ['publish'])
85 |
86 | // Push to GitHub.
87 | step('\nPushing to GitHub...')
88 | await run('git', ['tag', `v${targetVersion}`])
89 | await run('git', ['push', 'origin', `refs/tags/v${targetVersion}`])
90 | await run('git', ['push'])
91 | }
92 |
93 | function updatePackage(version) {
94 | const pkgPath = path.resolve(path.resolve(__dirname, '..'), 'package.json')
95 | const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
96 |
97 | pkg.version = version
98 |
99 | fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
100 | }
101 |
102 | main().catch((err) => console.error(err))
103 |
--------------------------------------------------------------------------------
/src/VuexORMSearch.ts:
--------------------------------------------------------------------------------
1 | import { Query } from '@vuex-orm/core'
2 | import Components from './contracts/Components'
3 | import Options from './contracts/Options'
4 | import Collection from './contracts/Collection'
5 | import DefaultOptions from './config/DefaultOptions'
6 | import QueryMixin from './mixins/Query'
7 |
8 | export default class VuexORMSearch {
9 | /**
10 | * The query object.
11 | */
12 | query: typeof Query
13 |
14 | /**
15 | * The options.
16 | */
17 | options: Options
18 |
19 | /**
20 | * Create a new Vuex ORM Search instance.
21 | */
22 | constructor(components: Components, options: Options) {
23 | this.query = components.Query
24 | this.options = { ...DefaultOptions, ...options }
25 | }
26 |
27 | /**
28 | * Plug in features.
29 | */
30 | plugin(): void {
31 | this.mixQuery()
32 |
33 | this.registerQueryHook()
34 | }
35 |
36 | /**
37 | * Apply query mixin.
38 | */
39 | mixQuery(): void {
40 | QueryMixin(this.query, this.options)
41 | }
42 |
43 | /**
44 | * Register global `afterWhere` hook to execute search filtering during the
45 | * select process.
46 | */
47 | registerQueryHook(): void {
48 | this.query.on('afterWhere', function(
49 | this: Query,
50 | collection: Collection
51 | ): Collection {
52 | return this.filterSearch(collection)
53 | })
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/config/DefaultOptions.ts:
--------------------------------------------------------------------------------
1 | import Options from '../contracts/Options'
2 |
3 | export const DefaultOptions: Options = {
4 | distance: 100,
5 | location: 0,
6 | maxPatternLength: 32,
7 | minMatchCharLength: 1,
8 | searchPrimaryKey: false,
9 | shouldSort: false,
10 | threshold: 0.3,
11 | tokenize: false,
12 | keys: [],
13 | verbose: false
14 | }
15 |
16 | export default DefaultOptions
17 |
--------------------------------------------------------------------------------
/src/contracts/Collection.ts:
--------------------------------------------------------------------------------
1 | import { Model } from '@vuex-orm/core'
2 |
3 | export type Collection = Model[]
4 |
5 | export default Collection
6 |
--------------------------------------------------------------------------------
/src/contracts/Components.ts:
--------------------------------------------------------------------------------
1 | import { Query } from '@vuex-orm/core'
2 |
3 | export interface Components {
4 | Query: typeof Query
5 | }
6 |
7 | export default Components
8 |
--------------------------------------------------------------------------------
/src/contracts/Options.ts:
--------------------------------------------------------------------------------
1 | export interface Options {
2 | distance?: number
3 | location?: number
4 | maxPatternLength?: number
5 | minMatchCharLength?: number
6 | searchPrimaryKey?: boolean
7 | shouldSort?: boolean
8 | threshold?: number
9 | tokenize?: boolean
10 | keys?: string[]
11 | verbose?: boolean
12 | }
13 |
14 | export default Options
15 |
--------------------------------------------------------------------------------
/src/index.cjs.ts:
--------------------------------------------------------------------------------
1 | import './types/vuex-orm'
2 |
3 | import Components from './contracts/Components'
4 | import Options from './contracts/Options'
5 | import VuexORMSearch from './VuexORMSearch'
6 |
7 | export default {
8 | install(components: Components, installOptions: Options): void {
9 | new VuexORMSearch(components, installOptions).plugin()
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import './types/vuex-orm'
2 |
3 | import Components from './contracts/Components'
4 | import Options from './contracts/Options'
5 | import VuexORMSearch from './VuexORMSearch'
6 |
7 | export { Options }
8 |
9 | export default {
10 | install(components: Components, installOptions: Options): void {
11 | new VuexORMSearch(components, installOptions).plugin()
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/mixins/Query.ts:
--------------------------------------------------------------------------------
1 | import Fuse from 'fuse.js'
2 | import { Query as BaseQuery } from '@vuex-orm/core'
3 | import Options from '../contracts/Options'
4 | import Collection from '../contracts/Collection'
5 |
6 | export default function Query(query: typeof BaseQuery, options: Options): void {
7 | /**
8 | * The search terms.
9 | */
10 | query.prototype.searchTerms = null
11 |
12 | /**
13 | * The search options.
14 | */
15 | query.prototype.searchOptions = options
16 |
17 | /**
18 | * Add search configurations.
19 | */
20 | query.prototype.search = function(
21 | terms: string | string[],
22 | options: Options = {}
23 | ): BaseQuery {
24 | // If `terms` is single string, convert it to an array so we can use it
25 | // consistently afterward.
26 | this.searchTerms = Array.isArray(terms) ? terms : [terms]
27 |
28 | // If a user didn't provide `keys` option, set all model fields as default.
29 | if ((this.searchOptions.keys as string[]).length === 0) {
30 | this.searchOptions.keys = Object.keys(
31 | this.model.cachedFields[this.model.entity]
32 | )
33 | }
34 |
35 | // Finally, merge default options with users options.
36 | this.searchOptions = { ...this.searchOptions, ...options }
37 |
38 | return this
39 | }
40 |
41 | /**
42 | * Filter the given record with fuzzy search by Fuse.js.
43 | */
44 | query.prototype.filterSearch = function(collection: Collection): Collection {
45 | if (
46 | this.searchTerms === null ||
47 | this.searchTerms.filter(Boolean).length === 0
48 | ) {
49 | return collection
50 | }
51 |
52 | const fuse = new Fuse(collection, this.searchOptions)
53 |
54 | return this.searchTerms.reduce((carry, term) => {
55 | carry.push(...fuse.search(term))
56 |
57 | return carry
58 | }, [])
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/types/global.d.ts:
--------------------------------------------------------------------------------
1 | declare var __DEV__: boolean
2 |
--------------------------------------------------------------------------------
/src/types/vuex-orm.ts:
--------------------------------------------------------------------------------
1 | import Options from '../contracts/Options'
2 | import Collection from '../contracts/Collection'
3 |
4 | declare module '@vuex-orm/core' {
5 | interface Query {
6 | /**
7 | * The search terms.
8 | */
9 | searchTerms: string[] | null
10 |
11 | /**
12 | * The search options.
13 | */
14 | searchOptions: Options
15 |
16 | /**
17 | * Add search configurations.
18 | */
19 | search(terms: string | string[], options?: Options): this
20 |
21 | /**
22 | * Filter the given record with fuzzy search by Fuse.js.
23 | */
24 | filterSearch(records: Collection): Collection
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/test/feature/Search.spec.ts:
--------------------------------------------------------------------------------
1 | import { createStore } from 'test/support/Helpers'
2 | import { Model, Fields } from '@vuex-orm/core'
3 |
4 | describe('Feature – Search', () => {
5 | class User extends Model {
6 | static entity = 'users'
7 |
8 | static fields(): Fields {
9 | return {
10 | id: this.attr(null),
11 | name: this.string(''),
12 | email: this.string('')
13 | }
14 | }
15 |
16 | id!: number
17 | name!: string
18 | email!: string
19 | }
20 |
21 | test('it can fuzzy search records by a single term', async () => {
22 | createStore([User])
23 |
24 | await User.insert({
25 | data: [
26 | { id: 1, name: 'John Walker', email: 'john@example.com' },
27 | { id: 2, name: 'Bobby Banana', email: 'walker.banana@example.com' }
28 | ]
29 | })
30 |
31 | const result = User.query()
32 | .search('John')
33 | .orderBy('id')
34 | .get()
35 |
36 | expect(result.length).toBe(1)
37 | expect(result[0].id).toBe(1)
38 | })
39 |
40 | test('it will do nothing if `search` term is empty', async () => {
41 | createStore([User])
42 |
43 | await User.insert({
44 | data: [
45 | { id: 1, name: 'John Walker', email: 'john@example.com' },
46 | { id: 2, name: 'Bobby Banana', email: 'walker.banana@example.com' },
47 | { id: 3, name: 'Ringo Looper', email: 'ringo.looper@example.com' }
48 | ]
49 | })
50 |
51 | const result = User.query()
52 | .search('')
53 | .orderBy('id')
54 | .get()
55 |
56 | expect(result.length).toBe(3)
57 | })
58 |
59 | test('it can fuzzy search records by many terms', async () => {
60 | createStore([User])
61 |
62 | await User.insert({
63 | data: [
64 | { id: 1, name: 'John Walker', email: 'john@example.com' },
65 | { id: 2, name: 'Bobby Banana', email: 'walker.banana@example.com' },
66 | { id: 3, name: 'Ringo Looper', email: 'ringo.looper@example.com' }
67 | ]
68 | })
69 |
70 | const result = User.query()
71 | .search(['rin', 'obby'])
72 | .orderBy('id')
73 | .get()
74 |
75 | expect(result.length).toBe(2)
76 | expect(result[0].id).toBe(2)
77 | expect(result[1].id).toBe(3)
78 | })
79 |
80 | test('it will do nothing if `search` is not set', async () => {
81 | createStore([User])
82 |
83 | await User.insert({
84 | data: [
85 | { id: 1, name: 'John Walker', email: 'john@example.com' },
86 | { id: 2, name: 'Bobby Banana', email: 'walker.banana@example.com' },
87 | { id: 3, name: 'Ringo Looper', email: 'ringo.looper@example.com' }
88 | ]
89 | })
90 |
91 | const result = User.query()
92 | .orderBy('id')
93 | .get()
94 |
95 | expect(result.length).toBe(3)
96 | })
97 |
98 | test('it can add global fuse options for the search', async () => {
99 | createStore([User], { keys: ['name'] })
100 |
101 | await User.insert({
102 | data: [
103 | { id: 1, name: 'John Walker', email: 'john@example.com' },
104 | { id: 2, name: 'Bobby Banana', email: 'mail.mail@example.com' },
105 | { id: 3, name: 'Ringo Looper', email: 'ringo.looper@example.com' }
106 | ]
107 | })
108 |
109 | const result = User.query()
110 | .search(['rin', 'mail'])
111 | .orderBy('id')
112 | .get()
113 |
114 | expect(result.length).toBe(1)
115 | })
116 |
117 | test('it can add local fuse options for the search', async () => {
118 | createStore([User])
119 |
120 | await User.insert({
121 | data: [
122 | { id: 1, name: 'John Walker', email: 'john@example.com' },
123 | { id: 2, name: 'Bobby Banana', email: 'mail.mail@example.com' },
124 | { id: 3, name: 'Ringo Looper', email: 'ringo.looper@example.com' }
125 | ]
126 | })
127 |
128 | const result = User.query()
129 | .search(['rin', 'mail'], { keys: ['name'] })
130 | .orderBy('id')
131 | .get()
132 |
133 | expect(result.length).toBe(1)
134 | })
135 | })
136 |
--------------------------------------------------------------------------------
/test/support/Helpers.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import * as Vuex from 'vuex'
3 | import VuexORM, { Database, Model } from '@vuex-orm/core'
4 | import Options from '@/contracts/Options'
5 | import VuexORMSearch from '@/index'
6 |
7 | export function createStore(
8 | models: typeof Model[],
9 | options?: Options
10 | ): Vuex.Store {
11 | Vue.use(Vuex)
12 |
13 | VuexORM.use(VuexORMSearch, options)
14 |
15 | const database = new Database()
16 |
17 | models.forEach((model) => {
18 | database.register(model)
19 | })
20 |
21 | return new Vuex.Store({
22 | plugins: [VuexORM.install(database)],
23 | strict: true
24 | })
25 | }
26 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2018",
4 | "module": "esnext",
5 | "strict": true,
6 | "importHelpers": true,
7 | "moduleResolution": "node",
8 | "esModuleInterop": true,
9 | "allowSyntheticDefaultImports": true,
10 | "baseUrl": ".",
11 | "rootDir": ".",
12 | "outDir": "dist",
13 | "declaration": true,
14 | "sourceMap": true,
15 | "removeComments": false,
16 | "noImplicitAny": true,
17 | "noImplicitReturns": true,
18 | "noImplicitThis": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "strictNullChecks": true,
22 | "suppressImplicitAnyIndexErrors": true,
23 | "types": ["node", "jest"],
24 | "lib": ["esnext", "dom", "es2017"],
25 | "paths": {
26 | "@/*": ["src/*"],
27 | "test/*": ["test/*"]
28 | }
29 | },
30 | "include": ["src", "test"],
31 | "exclude": ["node_modules"]
32 | }
33 |
--------------------------------------------------------------------------------