@symbo.ls/fetch 3.6.4 → 3.6.6

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 ADDED
@@ -0,0 +1,724 @@
1
+ # @symbo.ls/fetch
2
+
3
+ Declarative data fetching for DOMQL with pluggable adapters. Supports caching, stale-while-revalidate, pagination, infinite queries, retry, deduplication, optimistic updates, and more.
4
+
5
+ ## Setup
6
+
7
+ Add `db` to `config.js`:
8
+
9
+ ```js
10
+ // Supabase
11
+ db: { adapter: 'supabase', projectId: '...', key: '...' }
12
+
13
+ // Supabase — config from state
14
+ db: { adapter: 'supabase', state: 'supabase' } // merges root state.supabase
15
+
16
+ // REST
17
+ db: {
18
+ adapter: 'rest',
19
+ url: 'https://api.example.com',
20
+ headers: { Authorization: 'Bearer token' },
21
+ fetchOptions: { credentials: 'include', mode: 'cors' },
22
+ auth: {
23
+ baseUrl: 'https://api.example.com/auth',
24
+ sessionUrl: '/me',
25
+ signInUrl: '/login',
26
+ signOutUrl: '/logout'
27
+ }
28
+ }
29
+
30
+ // Local
31
+ db: { adapter: 'local', data: { articles: [] }, persist: true }
32
+ ```
33
+
34
+ ## Declarative `fetch`
35
+
36
+ ```js
37
+ // Minimal
38
+ { state: 'articles', fetch: true }
39
+
40
+ // With options
41
+ { state: 'articles', fetch: { params: { status: 'published' }, cache: '5m', order: { by: 'created_at', asc: false }, limit: 20 } }
42
+
43
+ // String shorthand
44
+ { state: 'data', fetch: 'blog_posts' }
45
+ ```
46
+
47
+ ### `as` — state key mapping
48
+
49
+ ```js
50
+ { state: { articles: [], loading: false }, fetch: { from: 'articles', as: 'articles' } }
51
+ ```
52
+
53
+ ### RPC
54
+
55
+ ```js
56
+ { state: { articles: [] }, fetch: { method: 'rpc', from: 'get_content_rows', params: { p_table: 'articles' }, as: 'articles', cache: '5m' } }
57
+ ```
58
+
59
+ ### `transform`
60
+
61
+ ```js
62
+ {
63
+ state: { featured: null, items: [] },
64
+ fetch: {
65
+ from: 'videos',
66
+ transform: (data) => ({
67
+ featured: data.find(v => v.is_featured) || data[0],
68
+ items: data.filter(v => !v.is_featured)
69
+ })
70
+ }
71
+ }
72
+ ```
73
+
74
+ ### `select` — data selector
75
+
76
+ Like TanStack's `select`, pick or reshape data before it hits state. Runs after cache read and before `transform`:
77
+
78
+ ```js
79
+ {
80
+ state: { titles: [] },
81
+ fetch: {
82
+ from: 'articles',
83
+ select: (data) => data.map(a => a.title)
84
+ }
85
+ }
86
+ ```
87
+
88
+ ### Dynamic params
89
+
90
+ ```js
91
+ {
92
+ state: { item: null },
93
+ fetch: {
94
+ method: 'rpc',
95
+ from: 'get_content_rows',
96
+ params: (el) => ({ p_table: 'articles', p_id: window.location.pathname.split('/').pop() }),
97
+ transform: (data) => ({ item: data && data[0] || null })
98
+ }
99
+ }
100
+ ```
101
+
102
+ ### Array fetch (parallel)
103
+
104
+ ```js
105
+ {
106
+ state: { articles: [], events: [] },
107
+ fetch: [
108
+ { method: 'rpc', from: 'get_content_rows', params: { p_table: 'articles' }, as: 'articles', cache: '5m' },
109
+ { method: 'rpc', from: 'get_content_rows', params: { p_table: 'events' }, as: 'events', cache: '5m' }
110
+ ]
111
+ }
112
+ ```
113
+
114
+ ### Triggers
115
+
116
+ ```js
117
+ { fetch: { from: 'articles' } } // on: 'create' (default)
118
+ { tag: 'form', fetch: { method: 'insert', from: 'contacts', on: 'submit' } } // on: 'submit'
119
+ { fetch: { method: 'delete', from: 'items', params: (el) => ({ id: el.state.itemId }), on: 'click' } }
120
+ { fetch: { from: 'articles', params: (el, s) => ({ title: { ilike: '%' + s.query + '%' } }), on: 'stateChange' } }
121
+ ```
122
+
123
+ ### Enabled / disabled queries
124
+
125
+ ```js
126
+ // Boolean
127
+ { fetch: { from: 'profile', enabled: false } }
128
+
129
+ // Function — resolves at fetch time
130
+ { fetch: { from: 'profile', enabled: (el, state) => !!state.userId } }
131
+ ```
132
+
133
+ ## Cache
134
+
135
+ Default: all queries cache with `staleTime: 1m`, `gcTime: 5m`.
136
+
137
+ ```js
138
+ cache: true // staleTime 1m, gcTime 5m (default)
139
+ cache: false // no caching
140
+ cache: '5m' // 5 min stale
141
+ cache: 30000 // 30s stale
142
+ cache: { stale: '1m', gc: '10m' }
143
+ cache: { staleTime: '30s', gcTime: '1h', key: 'custom-key' }
144
+ ```
145
+
146
+ ### Stale-while-revalidate
147
+
148
+ When cached data exists but is stale, it's served immediately while a background refetch happens. Fresh data replaces it once the refetch completes — no loading spinner for stale data.
149
+
150
+ ### Garbage collection
151
+
152
+ Unused cache entries (no active subscribers) are cleaned up after `gcTime` (default 5 minutes).
153
+
154
+ ## Retry
155
+
156
+ Failed queries automatically retry with exponential backoff.
157
+
158
+ ```js
159
+ // Default: 3 retries with exponential backoff (1s, 2s, 4s... max 30s)
160
+ { fetch: { from: 'articles' } }
161
+
162
+ // Disable retry
163
+ { fetch: { from: 'articles', retry: false } }
164
+
165
+ // Custom count
166
+ { fetch: { from: 'articles', retry: 5 } }
167
+
168
+ // Full control
169
+ {
170
+ fetch: {
171
+ from: 'articles',
172
+ retry: {
173
+ count: 3,
174
+ delay: (attempt, error) => Math.min(1000 * 2 ** attempt, 30000)
175
+ }
176
+ }
177
+ }
178
+ ```
179
+
180
+ ## Query deduplication
181
+
182
+ Multiple elements fetching the same query simultaneously share a single network request. The cache key is built from `from`, `method`, and `params`.
183
+
184
+ ```js
185
+ // Both share one request
186
+ { Header: { state: 'user', fetch: { from: 'profile', cache: '5m' } } }
187
+ { Sidebar: { state: 'user', fetch: { from: 'profile', cache: '5m' } } }
188
+ ```
189
+
190
+ ## Refetch on window focus
191
+
192
+ Stale queries automatically refetch when the user returns to the tab. Enabled by default.
193
+
194
+ ```js
195
+ // Disable
196
+ { fetch: { from: 'articles', refetchOnWindowFocus: false } }
197
+ ```
198
+
199
+ ## Refetch on reconnect
200
+
201
+ Queries refetch when the browser comes back online. Enabled by default.
202
+
203
+ ```js
204
+ // Disable
205
+ { fetch: { from: 'articles', refetchOnReconnect: false } }
206
+ ```
207
+
208
+ ## Polling / refetch interval
209
+
210
+ ```js
211
+ // Poll every 30 seconds
212
+ { fetch: { from: 'notifications', refetchInterval: 30000 } }
213
+ { fetch: { from: 'notifications', refetchInterval: '30s' } }
214
+
215
+ // Also poll when tab is in background
216
+ { fetch: { from: 'alerts', refetchInterval: '1m', refetchIntervalInBackground: true } }
217
+ ```
218
+
219
+ ## Placeholder data
220
+
221
+ Show temporary data immediately while the real query loads:
222
+
223
+ ```js
224
+ {
225
+ state: { articles: [] },
226
+ fetch: {
227
+ from: 'articles',
228
+ placeholderData: [] // show empty array instead of undefined while loading
229
+ }
230
+ }
231
+
232
+ // Function form
233
+ {
234
+ fetch: {
235
+ from: 'article_detail',
236
+ placeholderData: (el, state) => state.articles?.find(a => a.id === state.currentId)
237
+ }
238
+ }
239
+ ```
240
+
241
+ ## Initial data
242
+
243
+ Pre-populate the cache (counts as fresh data, won't trigger a refetch until stale):
244
+
245
+ ```js
246
+ {
247
+ fetch: {
248
+ from: 'settings',
249
+ initialData: { theme: 'dark', lang: 'en' }
250
+ }
251
+ }
252
+
253
+ // Function form
254
+ {
255
+ fetch: {
256
+ from: 'settings',
257
+ initialData: () => JSON.parse(localStorage.getItem('settings'))
258
+ }
259
+ }
260
+ ```
261
+
262
+ ## Keep previous data
263
+
264
+ Prevent UI flicker during page changes — keep showing current data while the next page loads:
265
+
266
+ ```js
267
+ {
268
+ state: { items: [], page: 1 },
269
+ fetch: {
270
+ from: 'articles',
271
+ page: (el, s) => s.page,
272
+ keepPreviousData: true
273
+ }
274
+ }
275
+ ```
276
+
277
+ ## Pagination
278
+
279
+ ### Offset-based
280
+
281
+ ```js
282
+ // Page number — auto-calculates offset from pageSize
283
+ {
284
+ state: { items: [], currentPage: 1 },
285
+ fetch: {
286
+ from: 'articles',
287
+ page: 1,
288
+ pageSize: 20, // default: limit or 20
289
+ keepPreviousData: true
290
+ }
291
+ }
292
+
293
+ // Manual offset/limit
294
+ {
295
+ fetch: {
296
+ from: 'articles',
297
+ page: { offset: 0, limit: 20 }
298
+ }
299
+ }
300
+ ```
301
+
302
+ ### Cursor-based
303
+
304
+ ```js
305
+ {
306
+ fetch: {
307
+ from: 'articles',
308
+ page: { cursor: 'abc123', limit: 20 }
309
+ }
310
+ }
311
+ ```
312
+
313
+ ## Infinite queries
314
+
315
+ Load pages incrementally with automatic page tracking:
316
+
317
+ ```js
318
+ {
319
+ state: { items: [] },
320
+ fetch: {
321
+ from: 'articles',
322
+ limit: 20,
323
+ infinite: true,
324
+ getNextPageParam: (lastPage, allPages) => {
325
+ if (lastPage.length < 20) return null // no more pages
326
+ return lastPage[lastPage.length - 1].id // cursor
327
+ }
328
+ }
329
+ }
330
+ ```
331
+
332
+ ### Fetching pages
333
+
334
+ After mount, use the imperative methods exposed on `element.__ref`:
335
+
336
+ ```js
337
+ // In an event handler or callback
338
+ el.__ref.fetchNextPage() // loads next page, appends to state
339
+ el.__ref.fetchPreviousPage() // loads previous page, prepends to state
340
+
341
+ // Status
342
+ el.__ref.__hasNextPage // boolean
343
+ el.__ref.__hasPreviousPage // boolean
344
+ el.__ref.__pages // array of page arrays
345
+ el.__ref.__nextPageParam // current next cursor
346
+ el.__ref.__prevPageParam // current previous cursor
347
+ ```
348
+
349
+ ### Bidirectional infinite scroll
350
+
351
+ ```js
352
+ {
353
+ fetch: {
354
+ from: 'messages',
355
+ infinite: true,
356
+ getNextPageParam: (lastPage) => lastPage[lastPage.length - 1]?.id,
357
+ getPreviousPageParam: (firstPage) => firstPage[0]?.id
358
+ }
359
+ }
360
+ ```
361
+
362
+ ## Mutations
363
+
364
+ Mutations (`insert`, `update`, `upsert`, `delete`) support optimistic updates, cache invalidation, and lifecycle callbacks.
365
+
366
+ ```js
367
+ { tag: 'form', fetch: { method: 'insert', from: 'articles', on: 'submit', fields: true } }
368
+ { tag: 'form', fetch: { method: 'insert', from: 'contacts', on: 'submit', fields: ['name', 'email'] } }
369
+ ```
370
+
371
+ ### Optimistic updates
372
+
373
+ Update the UI immediately, roll back if the mutation fails:
374
+
375
+ ```js
376
+ {
377
+ extends: 'Button',
378
+ text: 'Like',
379
+ fetch: {
380
+ method: 'update',
381
+ from: 'posts',
382
+ params: (el) => ({ id: el.state.postId }),
383
+ on: 'click',
384
+ optimistic: (mutationData, currentState) => ({
385
+ ...currentState,
386
+ likes: currentState.likes + 1
387
+ }),
388
+ invalidates: ['posts']
389
+ }
390
+ }
391
+ ```
392
+
393
+ ### Cache invalidation
394
+
395
+ After a mutation, invalidate related queries so they refetch:
396
+
397
+ ```js
398
+ {
399
+ fetch: {
400
+ method: 'insert',
401
+ from: 'articles',
402
+ on: 'submit',
403
+ fields: true,
404
+ invalidates: true // invalidates all "articles:*" cache keys
405
+ }
406
+ }
407
+
408
+ // Invalidate specific keys
409
+ { fetch: { method: 'delete', from: 'items', invalidates: ['items:select:'] } }
410
+
411
+ // Invalidate everything
412
+ { fetch: { method: 'update', from: 'settings', invalidates: ['*'] } }
413
+ ```
414
+
415
+ ### Mutation callbacks
416
+
417
+ ```js
418
+ {
419
+ fetch: {
420
+ method: 'insert',
421
+ from: 'contacts',
422
+ on: 'submit',
423
+ fields: true,
424
+ onMutate: (data, el) => console.log('Sending...', data),
425
+ onSuccess: (responseData, sentData, el) => console.log('Done!', responseData),
426
+ onError: (error, sentData, el) => console.error('Failed', error),
427
+ onSettled: (data, error, sentData, el) => console.log('Finished')
428
+ }
429
+ }
430
+ ```
431
+
432
+ ## Callbacks
433
+
434
+ ```js
435
+ {
436
+ fetch: true,
437
+ onFetchComplete: (data, el) => {},
438
+ onFetchError: (error, el) => {},
439
+ onFetchStart: (el) => {}
440
+ }
441
+ ```
442
+
443
+ ## Fetch status
444
+
445
+ Every fetch exposes status on `element.__ref.__fetchStatus`:
446
+
447
+ ```js
448
+ {
449
+ isFetching, // true while any request is in-flight (including background)
450
+ isLoading, // true only on first load (no cached data)
451
+ isStale, // true if data is past staleTime
452
+ isSuccess, // true after successful fetch
453
+ isError, // alias: !!error
454
+ error, // error object or null
455
+ status, // 'pending' | 'success' | 'error'
456
+ fetchStatus // 'fetching' | 'idle'
457
+ }
458
+ ```
459
+
460
+ Also available: `el.__ref.__fetching`, `el.__ref.__fetchError`.
461
+
462
+ ## Imperative refetch
463
+
464
+ ```js
465
+ // Refetch all queries on this element
466
+ el.__ref.refetch()
467
+
468
+ // Force (skip dedup)
469
+ el.__ref.refetch({ force: true })
470
+ ```
471
+
472
+ ## Query client
473
+
474
+ Global cache management, importable anywhere:
475
+
476
+ ```js
477
+ import { queryClient } from '@symbo.ls/fetch'
478
+ ```
479
+
480
+ ### Invalidate queries
481
+
482
+ ```js
483
+ queryClient.invalidateQueries('articles') // all keys containing "articles"
484
+ queryClient.invalidateQueries(['articles', 'select'])
485
+ queryClient.invalidateQueries() // invalidate everything
486
+ ```
487
+
488
+ ### Get / set cache
489
+
490
+ ```js
491
+ const articles = queryClient.getQueryData('articles:select:')
492
+
493
+ // Direct set
494
+ queryClient.setQueryData('articles:select:', newArticles)
495
+
496
+ // Updater function
497
+ queryClient.setQueryData('articles:select:', (old) => [...old, newArticle])
498
+ ```
499
+
500
+ ### Remove queries
501
+
502
+ ```js
503
+ queryClient.removeQueries('articles')
504
+ queryClient.removeQueries() // clear all
505
+ ```
506
+
507
+ ### Prefetch
508
+
509
+ Prefetch data before it's needed (e.g. on hover):
510
+
511
+ ```js
512
+ await queryClient.prefetchQuery({
513
+ from: 'article_detail',
514
+ method: 'select',
515
+ params: { id: 42 },
516
+ cache: '5m'
517
+ }, context)
518
+ ```
519
+
520
+ ## Auth guard
521
+
522
+ ```js
523
+ { fetch: { from: 'profile', auth: true } }
524
+ ```
525
+
526
+ ## Subscribe (realtime)
527
+
528
+ ```js
529
+ { state: 'messages', fetch: { method: 'subscribe', from: 'messages', subscribeOn: 'INSERT' } }
530
+ ```
531
+
532
+ ## Per-request overrides (REST)
533
+
534
+ ```js
535
+ { fetch: { from: '/users', baseUrl: 'https://api.example.com/auth', headers: { 'X-Custom': 'value' } } }
536
+ ```
537
+
538
+ ## State inheritance
539
+
540
+ When `state` is a string, the element inherits that key from the parent state. Fetch uses the same string as the default `from` (table name), so declaring `state` is often enough:
541
+
542
+ ```js
543
+ // Parent holds the data, child inherits and fetches into it
544
+ {
545
+ state: { articles: [], users: [] },
546
+ ArticleList: {
547
+ state: 'articles', // inherits parent.state.articles + fetches from "articles"
548
+ fetch: true,
549
+ children: '.'
550
+ },
551
+ UserList: {
552
+ state: 'users',
553
+ fetch: true,
554
+ children: '.'
555
+ }
556
+ }
557
+ ```
558
+
559
+ ### How it works
560
+
561
+ 1. `state: 'articles'` tells DOMQL to bind this element's state to `parent.state.articles`
562
+ 2. `fetch: true` resolves `from` using the same state key — equivalent to `fetch: { from: 'articles' }`
563
+ 3. Fetched data flows into `parent.state.articles`, and all elements inheriting that key update automatically
564
+
565
+ ### Nested paths
566
+
567
+ Use `/` to traverse deeper into the state tree:
568
+
569
+ ```js
570
+ {
571
+ state: { dashboard: { stats: {} } },
572
+ Stats: {
573
+ state: 'dashboard/stats',
574
+ fetch: { from: 'get_dashboard_stats', method: 'rpc' }
575
+ }
576
+ }
577
+ ```
578
+
579
+ ### Root and parent references
580
+
581
+ ```js
582
+ // ~/ resolves from root state
583
+ { state: '~/articles', fetch: true }
584
+
585
+ // ../ goes up one level in the state tree
586
+ { state: '../articles', fetch: true }
587
+ ```
588
+
589
+ ### Separate `from` and state key
590
+
591
+ When the table name differs from the state key, use `from` explicitly:
592
+
593
+ ```js
594
+ {
595
+ state: { posts: [] },
596
+ Posts: {
597
+ state: 'posts',
598
+ fetch: { from: 'blog_posts' } // fetches from "blog_posts", stores in state.posts
599
+ }
600
+ }
601
+ ```
602
+
603
+ ### `as` with inherited state
604
+
605
+ Use `as` to place fetched data at a specific key when the element has its own object state:
606
+
607
+ ```js
608
+ {
609
+ state: { articles: [], total: 0 },
610
+ Articles: {
611
+ state: 'articles',
612
+ fetch: true // replaces state.articles entirely
613
+ },
614
+ Dashboard: {
615
+ state: { items: [], loading: false },
616
+ fetch: { from: 'articles', as: 'items' } // sets state.items, preserves state.loading
617
+ }
618
+ }
619
+ ```
620
+
621
+ ## `getDB()`
622
+
623
+ ```js
624
+ const db = await this.getDB()
625
+ const { data, error } = await db.select({ from: 'articles' })
626
+ ```
627
+
628
+ ## Adapter interface
629
+
630
+ All return `{ data, error }`.
631
+
632
+ ```js
633
+ db.select({ from, select, params, limit, offset, order, single, headers, baseUrl })
634
+ db.insert({ from, data, select, headers, baseUrl })
635
+ db.update({ from, data, params, method, headers, baseUrl }) // method: 'PUT' | 'PATCH'
636
+ db.delete({ from, params, headers, baseUrl })
637
+ db.rpc({ from, params, headers, baseUrl })
638
+
639
+ // Auth
640
+ db.getSession()
641
+ db.signIn({ email, password })
642
+ db.signOut()
643
+ db.setToken(jwt) // REST
644
+ db.signUp({ email, password }) // Supabase
645
+ db.onAuthStateChange(callback) // Supabase
646
+
647
+ // Storage (Supabase)
648
+ db.upload({ bucket, path, file })
649
+ db.download({ bucket, path })
650
+ db.getPublicUrl({ bucket, path })
651
+ ```
652
+
653
+ ### Params
654
+
655
+ ```js
656
+ params: { status: 'published' } // eq
657
+ params: { age: { gt: 18 } } // gt, gte, lt, lte, neq
658
+ params: { title: { ilike: '%search%' } } // like, ilike
659
+ params: { id: [1, 2, 3] } // in
660
+ params: { deleted_at: null } // is null
661
+ ```
662
+
663
+ ### Order
664
+
665
+ ```js
666
+ order: 'created_at' // string
667
+ order: { by: 'created_at', asc: false } // object
668
+ order: [{ by: 'col1' }, { by: 'col2', asc: false }] // array
669
+ ```
670
+
671
+ ## Custom adapter
672
+
673
+ ```js
674
+ import { createAdapter } from '@symbo.ls/fetch'
675
+
676
+ const db = createAdapter({
677
+ name: 'custom',
678
+ select: async ({ from, params }) => { /* { data, error } */ },
679
+ insert: async ({ from, data }) => { /* { data, error } */ },
680
+ update: async ({ from, data, params }) => { /* { data, error } */ },
681
+ delete: async ({ from, params }) => { /* { data, error } */ }
682
+ })
683
+ ```
684
+
685
+ ## All `fetch` options
686
+
687
+ | Option | Type | Default | Description |
688
+ |--------|------|---------|-------------|
689
+ | `from` | string | state key or element key | Table/endpoint name |
690
+ | `method` | string | `'select'` | `select`, `rpc`, `insert`, `update`, `upsert`, `delete`, `subscribe` |
691
+ | `params` | object/function | — | Filter params or function `(el, state) => params` |
692
+ | `cache` | boolean/string/number/object | `true` (1m stale) | Cache configuration |
693
+ | `retry` | boolean/number/object | `3` | Retry on failure |
694
+ | `transform` | function | — | Reshape data before state update |
695
+ | `select` | function | — | Pick/reshape data (runs before transform) |
696
+ | `as` | string | — | Target state key |
697
+ | `on` | string | `'create'` | Trigger: `create`, `click`, `submit`, `stateChange` |
698
+ | `enabled` | boolean/function | `true` | Enable/disable query |
699
+ | `placeholderData` | any/function | — | Temporary data while loading |
700
+ | `initialData` | any/function | — | Pre-populate cache |
701
+ | `keepPreviousData` | boolean | `false` | Keep current data during refetch |
702
+ | `page` | number/object | — | Pagination: page number or `{ offset, limit, cursor }` |
703
+ | `pageSize` | number | `limit` or `20` | Items per page |
704
+ | `infinite` | boolean | `false` | Enable infinite query mode |
705
+ | `getNextPageParam` | function | — | `(lastPage, allPages) => cursor \| null` |
706
+ | `getPreviousPageParam` | function | — | `(firstPage, allPages) => cursor \| null` |
707
+ | `refetchInterval` | number/string | — | Polling interval |
708
+ | `refetchIntervalInBackground` | boolean | `false` | Poll when tab hidden |
709
+ | `refetchOnWindowFocus` | boolean | `true` | Refetch on tab focus |
710
+ | `refetchOnReconnect` | boolean | `true` | Refetch on online |
711
+ | `optimistic` | any/function | — | Optimistic update data |
712
+ | `invalidates` | string/array/boolean | — | Cache keys to invalidate after mutation |
713
+ | `onMutate` | function | — | Before mutation fires |
714
+ | `onSuccess` | function | — | After successful mutation |
715
+ | `onError` | function | — | After failed mutation |
716
+ | `onSettled` | function | — | After mutation completes (success or error) |
717
+ | `auth` | boolean | `false` | Require authentication |
718
+ | `fields` | boolean/array | — | Collect form fields for mutations |
719
+ | `single` | boolean | `false` | Return single row |
720
+ | `limit` | number | — | Row limit |
721
+ | `offset` | number | — | Row offset |
722
+ | `order` | string/object/array | — | Sort order |
723
+ | `headers` | object | — | Per-request headers (REST) |
724
+ | `baseUrl` | string | — | Per-request base URL (REST) |