@cgarciagarcia/react-query-builder 1.20.2 → 1.20.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +210 -155
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
# @cgarciagarcia/react-query-builder
|
|
3
|
+
|
|
4
|
+
A TypeScript React hook that builds query strings compatible with [spatie/laravel-query-builder](https://github.com/spatie/laravel-query-builder).
|
|
4
5
|
|
|
5
6
|
[](https://coveralls.io/github/cgarciagarcia/react-query-builder?branch=main)
|
|
6
7
|
[](https://github.com/cgarciagarcia/react-query-builder/actions/workflows/test.yml)
|
|
@@ -8,253 +9,307 @@
|
|
|
8
9
|
[](https://app.codacy.com/gh/cgarciagarcia/react-query-builder/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
|
|
9
10
|
[](https://www.npmjs.com/package/@cgarciagarcia/react-query-builder)
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Table of Contents
|
|
12
15
|
|
|
13
|
-
|
|
16
|
+
- [Installation](#installation)
|
|
17
|
+
- [Quick Start](#quick-start)
|
|
18
|
+
- [Configuration](#configuration)
|
|
19
|
+
- [API Reference](#api-reference)
|
|
20
|
+
- [Filters](#filters)
|
|
21
|
+
- [Fields](#fields)
|
|
22
|
+
- [Sorts](#sorts)
|
|
23
|
+
- [Includes](#includes)
|
|
24
|
+
- [Params](#params)
|
|
25
|
+
- [Pagination](#pagination)
|
|
26
|
+
- [Utilities](#utilities)
|
|
27
|
+
- [Advanced: Conflicting Filters](#advanced-conflicting-filters)
|
|
28
|
+
- [Support](#support)
|
|
29
|
+
- [License](#license)
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
14
34
|
|
|
15
35
|
```bash
|
|
36
|
+
# npm
|
|
37
|
+
npm install @cgarciagarcia/react-query-builder
|
|
38
|
+
|
|
39
|
+
# yarn
|
|
16
40
|
yarn add @cgarciagarcia/react-query-builder
|
|
17
|
-
```
|
|
18
41
|
|
|
19
|
-
|
|
42
|
+
# pnpm
|
|
43
|
+
pnpm add @cgarciagarcia/react-query-builder
|
|
44
|
+
```
|
|
20
45
|
|
|
21
|
-
|
|
22
|
-
of `spatie/laravel-query-builder`
|
|
23
|
-
using your favorite frontend library, `React.js`. It includes a custom hook that you can use for seamless interaction.
|
|
46
|
+
**Peer dependencies:** React 17, 18, or 19.
|
|
24
47
|
|
|
25
|
-
|
|
48
|
+
---
|
|
26
49
|
|
|
27
|
-
|
|
50
|
+
## Quick Start
|
|
28
51
|
|
|
29
|
-
```
|
|
52
|
+
```ts
|
|
30
53
|
import { useQueryBuilder } from "@cgarciagarcia/react-query-builder";
|
|
31
54
|
|
|
32
|
-
const
|
|
33
|
-
aliases: {
|
|
34
|
-
"frontend_name": "backend_name",
|
|
35
|
-
},
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const builder = useQueryBuilder(baseConfig)
|
|
55
|
+
const builder = useQueryBuilder()
|
|
40
56
|
|
|
41
57
|
builder
|
|
42
|
-
.fields('user.name', 'user.last_name'
|
|
43
|
-
.filter('
|
|
44
|
-
.filter(
|
|
45
|
-
.sort(
|
|
46
|
-
.sort(
|
|
47
|
-
.
|
|
48
|
-
.
|
|
58
|
+
.fields('user.name', 'user.last_name')
|
|
59
|
+
.filter('age', 18)
|
|
60
|
+
.filter('salary', '>', 1000)
|
|
61
|
+
.sort('created_at')
|
|
62
|
+
.sort('age', 'desc')
|
|
63
|
+
.include('posts', 'comments')
|
|
64
|
+
.setParam('external_param', 123)
|
|
49
65
|
.page(1)
|
|
50
66
|
.limit(10)
|
|
51
67
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
68
|
+
// Use in fetch
|
|
69
|
+
fetch("https://myapi.com/api/users" + builder.build())
|
|
70
|
+
|
|
71
|
+
// builder.build() returns:
|
|
72
|
+
// ?fields[user]=name,last_name&filter[age]=18&filter[salary][gt]=1000&sort=created_at,-age&includes=posts,comments&external_param=123&page=1&limit=10
|
|
57
73
|
```
|
|
58
74
|
|
|
59
|
-
|
|
75
|
+
---
|
|
60
76
|
|
|
61
|
-
|
|
62
|
-
the delimiter for each action (fields, filters, includes, sorts), the global delimiter
|
|
63
|
-
will be overwritten with the specific delimiter
|
|
77
|
+
## Configuration
|
|
64
78
|
|
|
65
|
-
|
|
79
|
+
Pass an optional config object to `useQueryBuilder` to set initial state and customize behavior.
|
|
66
80
|
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
81
|
+
```ts
|
|
82
|
+
const builder = useQueryBuilder({
|
|
83
|
+
// Map frontend field names to backend names
|
|
84
|
+
aliases: {
|
|
85
|
+
"frontend_name": "backend_name",
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
// Pre-set initial state
|
|
70
89
|
filters: [],
|
|
71
90
|
includes: [],
|
|
72
91
|
sorts: [],
|
|
73
92
|
fields: [],
|
|
93
|
+
params: {},
|
|
94
|
+
|
|
95
|
+
// Define mutually exclusive filters (see Advanced section)
|
|
74
96
|
pruneConflictingFilters: {},
|
|
97
|
+
|
|
98
|
+
// Custom delimiters (default: ',')
|
|
75
99
|
delimiters: {
|
|
76
|
-
global: ',',
|
|
100
|
+
global: ',', // applies to all unless overridden
|
|
77
101
|
fields: null,
|
|
78
102
|
filters: null,
|
|
79
103
|
sorts: null,
|
|
80
104
|
includes: null,
|
|
81
105
|
params: null,
|
|
82
106
|
},
|
|
107
|
+
|
|
108
|
+
// Prepend '?' to the output of build()
|
|
83
109
|
useQuestionMark: false,
|
|
84
|
-
|
|
85
|
-
|
|
110
|
+
|
|
111
|
+
// Initial pagination state
|
|
112
|
+
pagination: {
|
|
113
|
+
page: 1,
|
|
114
|
+
limit: 10,
|
|
115
|
+
},
|
|
116
|
+
})
|
|
86
117
|
```
|
|
87
118
|
|
|
88
|
-
|
|
89
|
-
You can use the remove method in sort, includes, filter like this:
|
|
119
|
+
---
|
|
90
120
|
|
|
91
|
-
|
|
92
|
-
const builder = useQueryBuilder(baseConfig)
|
|
121
|
+
## API Reference
|
|
93
122
|
|
|
94
|
-
|
|
95
|
-
.removeField('field_1', 'field_2')
|
|
96
|
-
.removeFilter('name', 'last_name')
|
|
97
|
-
.removeSort('name', 'id')
|
|
98
|
-
.removeInclude('address', 'documents')
|
|
99
|
-
.removeParam('param1', 'param2')
|
|
100
|
-
```
|
|
123
|
+
All methods return the builder instance, so they are **chainable**.
|
|
101
124
|
|
|
102
|
-
|
|
125
|
+
### Filters
|
|
103
126
|
|
|
104
|
-
|
|
127
|
+
```ts
|
|
128
|
+
// Add a filter (appends values by default)
|
|
129
|
+
builder.filter('status', 'active')
|
|
105
130
|
|
|
106
|
-
|
|
107
|
-
|
|
131
|
+
// Add a filter with an operator
|
|
132
|
+
builder.filter('salary', '>', 1000)
|
|
133
|
+
builder.filter('age', '>=', 18)
|
|
108
134
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
135
|
+
// Override existing filter values instead of appending
|
|
136
|
+
builder.filter('status', 'inactive', true)
|
|
137
|
+
|
|
138
|
+
// Remove specific filters
|
|
139
|
+
builder.removeFilter('status', 'age')
|
|
140
|
+
|
|
141
|
+
// Remove all filters
|
|
142
|
+
builder.clearFilters()
|
|
143
|
+
|
|
144
|
+
// Check if filters exist
|
|
145
|
+
builder.hasFilter('status') // → boolean
|
|
146
|
+
builder.hasFilter('status', 'age') // → true only if ALL exist
|
|
115
147
|
```
|
|
116
148
|
|
|
117
|
-
|
|
149
|
+
Available operators: `=`, `<`, `>`, `<=`, `>=`, `<>`
|
|
118
150
|
|
|
119
|
-
|
|
120
|
-
have filters like `date` filter and `between_dates` filter in your backend, but you can not filter
|
|
121
|
-
by both at the same time, so you have to be sure to clear incompatibles filters
|
|
122
|
-
before to adding a new one. With this purpose the property `pruneConflictingFilters`
|
|
123
|
-
was created, you can define these incompatibilities in the base configuration and delegate
|
|
124
|
-
the humdrum action to the library.
|
|
151
|
+
You can also import `FilterOperator` for type-safe operators:
|
|
125
152
|
|
|
126
|
-
|
|
153
|
+
```ts
|
|
154
|
+
import { FilterOperator } from "@cgarciagarcia/react-query-builder"
|
|
127
155
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
pruneConflictingFilters: {
|
|
131
|
-
date: ['between_dates']
|
|
132
|
-
},
|
|
133
|
-
})
|
|
156
|
+
builder.filter('salary', FilterOperator.GreaterThan, 1000)
|
|
157
|
+
```
|
|
134
158
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
### Fields
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
builder.fields('name', 'email', 'user.avatar')
|
|
165
|
+
builder.removeField('email')
|
|
166
|
+
builder.clearFields()
|
|
167
|
+
builder.hasField('name') // → boolean
|
|
139
168
|
```
|
|
140
169
|
|
|
141
|
-
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
### Sorts
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
builder.sort('created_at') // default: asc
|
|
176
|
+
builder.sort('age', 'desc')
|
|
177
|
+
builder.removeSort('created_at', 'age')
|
|
178
|
+
builder.clearSorts()
|
|
179
|
+
builder.hasSort('created_at') // → boolean
|
|
180
|
+
```
|
|
142
181
|
|
|
143
|
-
|
|
144
|
-
the library define the bidirectional incompatibility for you. Too much magic? Don't
|
|
145
|
-
worry, you still could define manually the inverse incompatibility to have explicit
|
|
146
|
-
declaration from your side.
|
|
182
|
+
---
|
|
147
183
|
|
|
148
|
-
|
|
184
|
+
### Includes
|
|
149
185
|
|
|
150
|
-
|
|
151
|
-
|
|
186
|
+
```ts
|
|
187
|
+
builder.include('posts', 'comments')
|
|
188
|
+
builder.removeInclude('posts')
|
|
189
|
+
builder.clearIncludes()
|
|
190
|
+
builder.hasInclude('posts') // → boolean
|
|
191
|
+
```
|
|
152
192
|
|
|
153
|
-
|
|
193
|
+
---
|
|
154
194
|
|
|
155
|
-
|
|
195
|
+
### Params
|
|
156
196
|
|
|
157
|
-
|
|
158
|
-
|
|
197
|
+
```ts
|
|
198
|
+
builder.setParam('custom_key', 'value')
|
|
199
|
+
builder.setParam('ids', [1, 2, 3])
|
|
200
|
+
builder.removeParam('custom_key')
|
|
201
|
+
builder.clearParams()
|
|
202
|
+
builder.hasParam('custom_key') // → boolean
|
|
203
|
+
```
|
|
159
204
|
|
|
160
|
-
|
|
205
|
+
---
|
|
161
206
|
|
|
162
|
-
|
|
207
|
+
### Pagination
|
|
163
208
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
209
|
+
```ts
|
|
210
|
+
const builder = useQueryBuilder({
|
|
211
|
+
pagination: { page: 1, limit: 10 }
|
|
212
|
+
})
|
|
168
213
|
|
|
169
|
-
|
|
170
|
-
|
|
214
|
+
builder.page(3) // go to page 3
|
|
215
|
+
builder.nextPage() // page + 1
|
|
216
|
+
builder.previousPage() // page - 1 (stops at 1)
|
|
217
|
+
builder.limit(25) // change page size
|
|
171
218
|
|
|
219
|
+
builder.getCurrentPage() // → number | undefined
|
|
220
|
+
builder.getLimit() // → number | undefined
|
|
172
221
|
```
|
|
173
222
|
|
|
174
|
-
|
|
223
|
+
> **Note:** Changing filters, removing filters, or changing the limit automatically resets to page 1.
|
|
175
224
|
|
|
176
|
-
|
|
225
|
+
---
|
|
177
226
|
|
|
178
|
-
|
|
179
|
-
|
|
227
|
+
### Utilities
|
|
228
|
+
|
|
229
|
+
#### `build()`
|
|
180
230
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const builder = useQueryBuilder()
|
|
184
|
-
|
|
185
|
-
const useUserQuery = useQuery({
|
|
186
|
-
fnQuery: () => getApiUsers(builder),
|
|
187
|
-
queryKey: ['userQuery', ...builder.toArray()]
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
const onClickButton = (id: number|null) => {
|
|
191
|
-
|
|
192
|
-
builder.when(id !== null, (state) => {
|
|
193
|
-
console.log(state) // reveal internal state
|
|
194
|
-
})
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/* Rest of code */
|
|
198
|
-
}
|
|
231
|
+
Returns the final query string.
|
|
199
232
|
|
|
233
|
+
```ts
|
|
234
|
+
builder.build() // → "?filter[age]=18&sort=created_at"
|
|
200
235
|
```
|
|
201
236
|
|
|
202
|
-
|
|
237
|
+
#### `toArray()`
|
|
203
238
|
|
|
239
|
+
Returns the query state as a flat string array. Useful as a React Query `queryKey`.
|
|
204
240
|
|
|
205
|
-
```
|
|
206
|
-
|
|
241
|
+
```ts
|
|
242
|
+
import { useQuery } from "@tanstack/react-query"
|
|
207
243
|
|
|
208
|
-
builder
|
|
209
|
-
.hasInclude('include', 'include2', ...)
|
|
210
|
-
.hasParam('param1', 'param2', ...)
|
|
211
|
-
.hasField('field', ...)
|
|
212
|
-
.hasSort('sort', ...)
|
|
244
|
+
const builder = useQueryBuilder()
|
|
213
245
|
|
|
246
|
+
const { data } = useQuery({
|
|
247
|
+
queryFn: () => getUsers(builder.build()),
|
|
248
|
+
queryKey: ['users', ...builder.toArray()],
|
|
249
|
+
})
|
|
214
250
|
```
|
|
215
251
|
|
|
216
|
-
|
|
252
|
+
#### `tap(callback)`
|
|
217
253
|
|
|
218
|
-
|
|
254
|
+
Inspect the internal state without interrupting the chain.
|
|
219
255
|
|
|
220
|
-
```
|
|
256
|
+
```ts
|
|
257
|
+
builder
|
|
258
|
+
.filter('age', 18)
|
|
259
|
+
.tap((state) => console.log(state))
|
|
260
|
+
.sort('name')
|
|
261
|
+
```
|
|
221
262
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
263
|
+
#### `when(condition, callback)`
|
|
264
|
+
|
|
265
|
+
Conditionally execute a callback based on a boolean. The builder is returned regardless.
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
builder.when(isAdmin, (state) => {
|
|
269
|
+
console.log('Admin state:', state)
|
|
227
270
|
})
|
|
271
|
+
```
|
|
228
272
|
|
|
229
|
-
|
|
273
|
+
---
|
|
230
274
|
|
|
231
|
-
|
|
275
|
+
## Advanced: Conflicting Filters
|
|
232
276
|
|
|
233
|
-
|
|
234
|
-
const goToFirstPage = () => builder.page(1)
|
|
277
|
+
Some filters are mutually exclusive in your backend (e.g. `date` vs `between_dates`). Use `pruneConflictingFilters` to let the library handle this automatically.
|
|
235
278
|
|
|
236
|
-
|
|
279
|
+
```ts
|
|
280
|
+
const builder = useQueryBuilder({
|
|
281
|
+
pruneConflictingFilters: {
|
|
282
|
+
date: ['between_dates'],
|
|
283
|
+
// 'between_dates': ['date'] is added automatically (bidirectional)
|
|
284
|
+
},
|
|
285
|
+
})
|
|
237
286
|
|
|
238
|
-
|
|
239
|
-
|
|
287
|
+
builder.filter('date', '2024-08-13')
|
|
288
|
+
// → ?filter[date]=2024-08-13
|
|
240
289
|
|
|
290
|
+
builder.filter('between_dates', ['2024-08-06', '2024-08-13'])
|
|
291
|
+
// → ?filter[between_dates]=2024-08-06,2024-08-13
|
|
292
|
+
// (date filter was automatically removed)
|
|
241
293
|
```
|
|
242
294
|
|
|
295
|
+
The conflict is **bidirectional by default** — you only need to declare it once. You can also declare both directions explicitly if you prefer.
|
|
296
|
+
|
|
297
|
+
---
|
|
243
298
|
|
|
244
|
-
##
|
|
299
|
+
## Support
|
|
245
300
|
|
|
246
|
-
|
|
301
|
+
Have a question or need help? [Open a discussion](https://github.com/cgarciagarcia/react-query-builder/discussions) on GitHub.
|
|
247
302
|
|
|
303
|
+
---
|
|
248
304
|
|
|
249
|
-
## Consider
|
|
305
|
+
## Consider Supporting
|
|
250
306
|
|
|
251
|
-
|
|
252
|
-
not only help maintain and improve this package but also promote the continuous
|
|
253
|
-
development of quality tools and resources. You can also email me and let me know.
|
|
307
|
+
If this package helps you, consider supporting its creator:
|
|
254
308
|
|
|
255
|
-
|
|
309
|
+
**PayPal:** [@carlosgarciadev](https://paypal.me/carlosgarciadev?country.x=AR&locale.x=es_XC)
|
|
256
310
|
|
|
311
|
+
---
|
|
257
312
|
|
|
258
313
|
## License
|
|
259
314
|
|
|
260
|
-
The MIT License (MIT).
|
|
315
|
+
The MIT License (MIT). See [LICENSE](LICENSE) for more information.
|