Skip to content

Commit 3d39b85

Browse files
committed
docs: Custom Query Filters
1 parent 9ffd863 commit 3d39b85

File tree

5 files changed

+207
-3
lines changed

5 files changed

+207
-3
lines changed

docs/.vitepress/config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ function getGuideSidebar() {
111111
text: 'Feathers-Pinia Client',
112112
items: [
113113
{ text: 'Client API', link: '/guide/create-pinia-client' },
114+
{ text: 'Custom Query Filters', link: '/guide/custom-query-filters' },
114115
{ text: 'Common Pitfalls', link: '/guide/troubleshooting' },
115116
],
116117
},

docs/guide/custom-query-filters.md

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
---
2+
outline: deep
3+
---
4+
5+
<script setup>
6+
import Badge from '../components/Badge.vue'
7+
import BlockQuote from '../components/BlockQuote.vue'
8+
</script>
9+
10+
# Custom Query Filters
11+
12+
The [createPiniaClient](./create-pinia-client#createpiniaclient) function accepts a new `customFilters` option. Custom Filters let you define query superpowers for local queries using the `findInStore` method.
13+
14+
[[toc]]
15+
16+
## Why Use Custom Filters
17+
18+
Custom filters provide the following benefits:
19+
20+
- Custom filters let you attach functionality to your own, custom query parameters.
21+
- Custom filters run **before** the rest of the query operators, giving you access to the full list of stored items for a service.
22+
- Custom filters are easier to define than `customSiftOperators` because they need no prior knowledge of custom interfaces.
23+
24+
Note: You cannot override built-in query operators like `$limit`, `$skip`, `$sort`, or `$select` with custom filters.
25+
26+
## The uFuzzy Custom Filter
27+
28+
Feathers-Pinia 4.5+ ships with a built-in custom filter called `uFuzzy`. The `uFuzzy` filter lets you perform fuzzy searches on your local data. In order to use it, you need to install the [@leeoniya/ufuzzy](https://github.com/leeoniya/uFuzzy) package into your project:
29+
30+
```bash
31+
pnpm install @leeoniya/ufuzzy
32+
```
33+
34+
You can now import and use the `createUFuzzyFilter` function to create a custom filter. To keep the feathers-pinia package size smaller, the ufuzzy operator is not provided in the main exports and must be imported from `feathers-pinia/ufuzzy`. In this next example, we create a custom filter called `$fuzzy` that uses the `uFuzzy` operator:
35+
36+
```ts
37+
import { createUFuzzyFilter } from 'feathers-pinia/ufuzzy'
38+
39+
const api = createPiniaClient(feathersClient, {
40+
pinia,
41+
idField: 'id',
42+
customFilters: [
43+
{ key: '$fuzzy', operator: createCursorPaginationFilter() },
44+
],
45+
})
46+
```
47+
48+
You can rename `$fuzzy` to whatever you would like. It will only affect queries that use `findInStore`, including `useFind` when set to `paginateOn: 'client'`.
49+
50+
Now let's use the `$fuzzy` operator to search for messages that contain the phrase "hello world":
51+
52+
```ts
53+
const { data } = await api.service('messages').findInStore({
54+
query: {
55+
$fuzzy: {
56+
search: 'hello world',
57+
fields: ['text']
58+
}
59+
}
60+
})
61+
```
62+
63+
The `fields` property is an array of fields to search for the `search` term. The `search` term is the string to search for in the specified fields. The `uFuzzy` filter will return all items that fuzzy match the search term in any of the specified fields.
64+
65+
So we can also search across multiple fields. Let's search for users by first name, last name, or email:
66+
67+
```ts
68+
const { data } = await api.service('users').findInStore({
69+
query: {
70+
$fuzzy: {
71+
search: 'john',
72+
fields: ['firstName', 'lastName', 'email']
73+
}
74+
}
75+
})
76+
```
77+
78+
### Matched __ranges
79+
80+
Search results will have a non-enumerable property added to them called `__ranges`. This property contains the ranges of the matched characters in the search term. This can be useful for highlighting search results in the UI. The `__ranges` property is not enumerable, so it won't show up in JSON.stringify or Vue Devtools. It's an object keyed by field name, so it might have the following structure for the last example:
81+
82+
```ts
83+
const johnDoe = {
84+
firstName: 'John',
85+
lastName: 'Doe',
86+
87+
__ranges: {
88+
"firstName": [0, 3],
89+
"lastName": [],
90+
"email": [],
91+
}
92+
}
93+
```
94+
95+
Note that only the first 3 characters of the first name matched the search term, even though email also contained a match. This is because the `firstName` field was listed first in the `fields` array. Once a match is found in a field, the search stops and the result is returned. This keeps things fast.
96+
97+
Once you clear the search term, the `__ranges` property will be removed from the result.
98+
99+
### Why not Fuse.js?
100+
101+
Here are observations while working with [Fuse.js](https://www.fusejs.io/) and [uFuzzy](https://github.com/leeoniya/uFuzzy):
102+
103+
1. uFuzzy is **much** faster. We're talking 100x to 1000x faster than Fuse.js. See the benchmarks and compare for yourself. Since we're already working with Vue's reactivity system, there's some overhead involved. The fuzzy search implementation needs to be fast to keep the UI responsive.
104+
2. Fuse.js is more configurable, which looks great on the surface. In practice, it's only configurable because it doesn't return a great set of results by default. So you need to configure it for every new dataset in order to get good results. The algorithm in uFuzzy tends to return great results using the same configuration across datasets. uFuzzy is also lightly configurable.
105+
3. uFuzzy returns higher quality results. For example, it prioritizes exact matches before fuzzy matches. This tends to turn up relevant results faster.
106+
4. Fuse.js is much easier to setup. uFuzzy requires a bit more work to get started. It's more of a low-level API.
107+
5. Fuse.js supports searching across multiple fields by default. With uFuzzy this requires some extra work. The implementation in feathers-pinia supports searching across multiple fields, making it just as easy to use as Fuse.js.
108+
109+
## Create Your Own Custom Filter
110+
111+
Suppose we have a project that needs to use cursor-based pagination instead of `$skip`-based pagination. Since we can't override `$skip`, we can create a custom filter called `$paginate` that uses an `after` parameter to fetch the next set of items after a specific id. The example might be a bit contrived, but it demonstrates how to create a custom filter.
112+
113+
```ts
114+
// src/filter.cursor-pagination.ts
115+
116+
// define the shape of your custom operator's options
117+
export interface PaginateAfterOptions {
118+
defaultLimit?: number
119+
idField?: string
120+
}
121+
122+
// define the shape of the query params specific to your custom operator
123+
export type PaginateAfterQueryParams {
124+
after: string
125+
limit?: number
126+
}
127+
const defaultOptions: PaginateAfterOptions = {
128+
idField: 'id',
129+
defaultLimit: 10
130+
}
131+
132+
export function createCursorPaginationFilter = function (options = {}) {
133+
const { idField, defaultLimit } = { ...defaultOptions, ...options }
134+
return <M>(items: M[], queryParams: PaginateAfterQueryParams, query: Record<string, any>) => {
135+
const { after, limit = defaultLimit } = queryParams
136+
137+
// Find the index of the item with the provided id
138+
const index = items.findIndex((item: any) => item[idField] === after)
139+
140+
// If the item is not found, return an empty array
141+
if (index === -1) {
142+
return []
143+
}
144+
// otherwise return the next set of items after the provided id.
145+
return items.slice(index + 1, index + limit + 1)
146+
}
147+
}
148+
```
149+
150+
Now it's ready to import and use in your Feathers Pinia client:
151+
152+
```ts
153+
// src/feathers-client.ts
154+
155+
import { createCursorPaginationFilter } from './filter.cursor-pagination'
156+
157+
const api = createPiniaClient(feathersClient, {
158+
pinia,
159+
idField: 'id',
160+
customFilters: [
161+
{ key: '$paginate', operator: createCursorPaginationFilter() },
162+
],
163+
})
164+
```

docs/guide/whats-new.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,45 @@ import BlockQuote from '../components/BlockQuote.vue'
1414
Version 4 of Feathers-Pinia is about improving developer experience. It focuses on the `useFind`, `findInStore`, and
1515
`useGet` APIs.
1616

17+
## New in v4.5
18+
19+
### :gift: Custom Query Filters
20+
21+
Filters are top-level, custom parameters inside your `query` objects. You can now define custom operators **for local
22+
queries** (the `findInStore` method). This new capability powers the new, built-in fuzzy search operator. Read more about [Custom Query Filters](/guide/custom-query-filters).
23+
24+
### :gift: Fuzzy Search Filter
25+
26+
The `createPiniaClient` function now accepts a `customFilters` option. Custom operators are easier to define than
27+
`customSiftOperators` because they need no prior knowledge of custom interfaces. Custom operators run **before** the
28+
rest of the query operators. Read how to set up the [uFuzzy Custom Filter](/guide/custom-query-filters#the-ufuzzy-custom-filter).
29+
30+
```ts
31+
const { data } = await api.service('users').findInStore({
32+
query: {
33+
$fuzzy: {
34+
search: 'john',
35+
fields: ['firstName', 'lastName', 'email']
36+
}
37+
}
38+
})
39+
```
40+
41+
### :100: No Local Query Validation
42+
43+
There should be no more need for a local `whitelist` option, since query validation has been removed from local queries.
44+
You should be able to use `$regex` and other operators without seeing any message about invalid operators or filters. At
45+
least... not from the client side. They should still arrive from your Feathers server if you're properly implementing
46+
query validation there.
47+
48+
```ts
49+
createPiniaClient(feathersClient, {
50+
pinia,
51+
idField: '_id',
52+
// whitelist: ['$regex'], no need for whitelist anymore
53+
})
54+
```
55+
1756
## New in v4.2
1857

1958
### 🎁 useBackup

src/custom-operators/fuzzy-search-with-ufuzzy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ const defaultUFuzzyOptions: UFuzzy.Options = {
4343
interChars: '.',
4444
}
4545

46-
export function createUFuzzyOperator(ufOptions: UFuzzy.Options = {}) {
46+
export function createUFuzzyFilter(ufOptions: UFuzzy.Options = {}) {
4747
return <M>(items: M[], params: uFuzzyParams, _query: Record<string, any>) => {
4848
if (params.search == null || !params.fields)
4949
throw new Error('Missing required parameters \'search\' or \'fields\' for $fuzzy operator.')

tests/fixtures/feathers.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { computed, ref } from 'vue'
1010
import { vi } from 'vitest'
1111
import type { AdapterParams } from '@feathersjs/adapter-commons'
1212
import { createPiniaClient, defineGetters, defineSetters, useInstanceDefaults } from '../../src'
13-
import { createUFuzzyOperator } from '../../src/ufuzzy'
13+
import { createUFuzzyFilter } from '../../src/ufuzzy'
1414
import { timeout } from '../test-utils.js'
1515
import { makeContactsData } from './data.js'
1616
import type { Comments } from './schemas/comments'
@@ -144,7 +144,7 @@ function wrapPiniaClient<F extends Application>(feathersClient: F) {
144144
storage: localStorageMock,
145145
paramsForServer: [],
146146
customFilters: [
147-
{ key: '$fuzzy', operator: createUFuzzyOperator() },
147+
{ key: '$fuzzy', operator: createUFuzzyFilter() },
148148
],
149149
customizeStore() {
150150
return {

0 commit comments

Comments
 (0)