snapshot of https://github.com/TanStack/query.git (main, 2026-09-16; blobs > 131072B omitted)
This commit is contained in:
3
packages/svelte-query/.attw.json
Normal file
3
packages/svelte-query/.attw.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"ignoreRules": ["cjs-resolves-to-esm", "internal-resolution-error"]
|
||||
}
|
||||
478
packages/svelte-query/CHANGELOG.md
Normal file
478
packages/svelte-query/CHANGELOG.md
Normal file
@@ -0,0 +1,478 @@
|
||||
# @tanstack/svelte-query
|
||||
|
||||
## 6.1.48
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.102.8
|
||||
|
||||
## 6.1.47
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.102.7
|
||||
|
||||
## 6.1.46
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.102.6
|
||||
|
||||
## 6.1.45
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`578e5c2`](https://github.com/TanStack/query/commit/578e5c26e8ebd0d7351b4b8e2bafba695e672b8d)]:
|
||||
- @tanstack/query-core@5.102.5
|
||||
|
||||
## 6.1.44
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`a05df6a`](https://github.com/TanStack/query/commit/a05df6aefb0e2489ec2c879ae16e2ee7cb3123ec)]:
|
||||
- @tanstack/query-core@5.102.4
|
||||
|
||||
## 6.1.43
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.102.3
|
||||
|
||||
## 6.1.42
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#10892](https://github.com/TanStack/query/pull/10892) [`cafd540`](https://github.com/TanStack/query/commit/cafd540c02f78be260198c4e84d054390c02c6b8) - Fix `createQueries` crashing with `TypeError: 'deleteProperty' on proxy: trap returned falsish for property 'N'` when two or more items were removed from its reactive array in the same update.
|
||||
|
||||
- [#11127](https://github.com/TanStack/query/pull/11127) [`320ed25`](https://github.com/TanStack/query/commit/320ed25caca4e219b53f778c59cbb380543a21f4) - Fix `createQueries` staying inactive when it is initialized with an empty `queries` array.
|
||||
|
||||
- [#11153](https://github.com/TanStack/query/pull/11153) [`250050d`](https://github.com/TanStack/query/commit/250050d863d33fbc881c6e342c104d510403a9e1) - fix(svelte-query): remove mutations that no longer match the filter in `useMutationState`
|
||||
|
||||
## 6.1.41
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`80fbf73`](https://github.com/TanStack/query/commit/80fbf73e77892d702c107e14a84c219a8ed825dc)]:
|
||||
- @tanstack/query-core@5.102.2
|
||||
|
||||
## 6.1.40
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`134890d`](https://github.com/TanStack/query/commit/134890dbdc60e4fb0313e44b512d29284ca82f96)]:
|
||||
- @tanstack/query-core@5.102.1
|
||||
|
||||
## 6.1.39
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#10373](https://github.com/TanStack/query/pull/10373) [`6e3d521`](https://github.com/TanStack/query/commit/6e3d521fe54f78624e53c1c6f9cadd678504bee8) - fix(types): propagate generic type parameters to `useMutationState` select callback
|
||||
|
||||
- [#11224](https://github.com/TanStack/query/pull/11224) [`294d4e6`](https://github.com/TanStack/query/commit/294d4e62c4f7d674574a4903eef2a1bc3dd1413b) - Fix `queryOptions` and `infiniteQueryOptions` return types so exported inferred options can be emitted in declaration files without leaking internal data tag symbols.
|
||||
|
||||
- [#8737](https://github.com/TanStack/query/pull/8737) [`2215bb0`](https://github.com/TanStack/query/commit/2215bb031139cdc8a84751b37a485c38ca9d2b6e) - fix: make mutation variables optional when `undefined extends TVariables`
|
||||
|
||||
- [#11221](https://github.com/TanStack/query/pull/11221) [`1ef4208`](https://github.com/TanStack/query/commit/1ef42087c9a266c2137d2ef645cbcc662f60ac93) - Remove experimental render-time prefetching and the `promise` property from query results.
|
||||
|
||||
- Updated dependencies [[`34f7cee`](https://github.com/TanStack/query/commit/34f7ceed09c10e4a3aa2df31a106ddf02ec4e787), [`b4368c4`](https://github.com/TanStack/query/commit/b4368c43792349f6c29d1fb41f7ee1ef3a8bdd2c), [`5bb089d`](https://github.com/TanStack/query/commit/5bb089d153d33933983b7ea80f8d69b51d423698), [`ba4650c`](https://github.com/TanStack/query/commit/ba4650c05e61e33c609e051b948c9f7d31ce70d1), [`294d4e6`](https://github.com/TanStack/query/commit/294d4e62c4f7d674574a4903eef2a1bc3dd1413b), [`1f631b3`](https://github.com/TanStack/query/commit/1f631b3604aab6567fb3f6c90646a5a304641546), [`01a02bf`](https://github.com/TanStack/query/commit/01a02bfad32f9efbc17796be31d6399f1197a655), [`18c1c1e`](https://github.com/TanStack/query/commit/18c1c1ef94883e781ad36d36f9ea4a043ce4260b), [`5448063`](https://github.com/TanStack/query/commit/5448063f828d2270dccd41ae375e1afde35e91f2), [`2215bb0`](https://github.com/TanStack/query/commit/2215bb031139cdc8a84751b37a485c38ca9d2b6e), [`1ef4208`](https://github.com/TanStack/query/commit/1ef42087c9a266c2137d2ef645cbcc662f60ac93), [`5981771`](https://github.com/TanStack/query/commit/5981771abec9330b344a2617543eb05ba7c99e24), [`4a9bef6`](https://github.com/TanStack/query/commit/4a9bef6cf19b1cd6b014032d696129f12a848185), [`bef4bc7`](https://github.com/TanStack/query/commit/bef4bc780ce7cca32b7e3dea85f77d92f82a62a2), [`9656dc4`](https://github.com/TanStack/query/commit/9656dc4e5fef5f8c502579b52459f9e8c72e787c), [`326aaf1`](https://github.com/TanStack/query/commit/326aaf1333e5d9cbc46569c53f218d31684162d2), [`3e83601`](https://github.com/TanStack/query/commit/3e836010032c2586e9f1c66a271fa9114a6401f9), [`c6fc17c`](https://github.com/TanStack/query/commit/c6fc17cfad6e452261c585fabfb8c210e60531ed)]:
|
||||
- @tanstack/query-core@5.102.0
|
||||
|
||||
## 6.1.38
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.101.4
|
||||
|
||||
## 6.1.37
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`7e3c822`](https://github.com/TanStack/query/commit/7e3c822a10896f41a8f1031c16b85096277af677)]:
|
||||
- @tanstack/query-core@5.101.3
|
||||
|
||||
## 6.1.36
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.101.2
|
||||
|
||||
## 6.1.35
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`9eff92e`](https://github.com/TanStack/query/commit/9eff92ed86e284ec0125b3a3539d028688235bd1)]:
|
||||
- @tanstack/query-core@5.101.1
|
||||
|
||||
## 6.1.34
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.101.0
|
||||
|
||||
## 6.1.33
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.100.14
|
||||
|
||||
## 6.1.32
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`d423168`](https://github.com/TanStack/query/commit/d423168f6261a5cb3d353e53b27c8150cc271151)]:
|
||||
- @tanstack/query-core@5.100.13
|
||||
|
||||
## 6.1.31
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.100.12
|
||||
|
||||
## 6.1.30
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.100.11
|
||||
|
||||
## 6.1.29
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.100.10
|
||||
|
||||
## 6.1.28
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`fcee7bd`](https://github.com/TanStack/query/commit/fcee7bdc429385ae8ffa224fa8a7a9ec7b8ee380)]:
|
||||
- @tanstack/query-core@5.100.9
|
||||
|
||||
## 6.1.27
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.100.8
|
||||
|
||||
## 6.1.26
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.100.7
|
||||
|
||||
## 6.1.25
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.100.6
|
||||
|
||||
## 6.1.24
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`a53ef97`](https://github.com/TanStack/query/commit/a53ef97f87decb8ea2431710f5199431d3c94c8d)]:
|
||||
- @tanstack/query-core@5.100.5
|
||||
|
||||
## 6.1.23
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.100.4
|
||||
|
||||
## 6.1.22
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`f85d825`](https://github.com/TanStack/query/commit/f85d825e02efbbff02e2081528ed28f5e5382f7a)]:
|
||||
- @tanstack/query-core@5.100.3
|
||||
|
||||
## 6.1.21
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`ea4497e`](https://github.com/TanStack/query/commit/ea4497e8aa00d8c1c3a36fb1e17563a889d6ab31), [`d6a7bf3`](https://github.com/TanStack/query/commit/d6a7bf3e3e024c1a77d0536813238cc8007a5fa7), [`645d5d1`](https://github.com/TanStack/query/commit/645d5d130f5e8017cb1bf1a37987f7b980aed705)]:
|
||||
- @tanstack/query-core@5.100.2
|
||||
|
||||
## 6.1.20
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`1bb0d23`](https://github.com/TanStack/query/commit/1bb0d234280fd4ae1725c439088426a20593a8df)]:
|
||||
- @tanstack/query-core@5.100.1
|
||||
|
||||
## 6.1.19
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`6540a41`](https://github.com/TanStack/query/commit/6540a4126b1c087d86d64525e78f32d9920dcd31)]:
|
||||
- @tanstack/query-core@5.100.0
|
||||
|
||||
## 6.1.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.99.2
|
||||
|
||||
## 6.1.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.99.1
|
||||
|
||||
## 6.1.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.99.0
|
||||
|
||||
## 6.1.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.98.0
|
||||
|
||||
## 6.1.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`2bfb12c`](https://github.com/TanStack/query/commit/2bfb12cc44f1d8495106136e4ddacb817135f8f9)]:
|
||||
- @tanstack/query-core@5.97.0
|
||||
|
||||
## 6.1.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.96.2
|
||||
|
||||
## 6.1.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.96.1
|
||||
|
||||
## 6.1.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.96.0
|
||||
|
||||
## 6.1.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`cd5a35b`](https://github.com/TanStack/query/commit/cd5a35b328837781aa4f9305bb2bd7877ca934e9)]:
|
||||
- @tanstack/query-core@5.95.2
|
||||
|
||||
## 6.1.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`1f1775c`](https://github.com/TanStack/query/commit/1f1775ca92f2b6c035682947ff3b3424804ff31a)]:
|
||||
- @tanstack/query-core@5.95.1
|
||||
|
||||
## 6.1.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tanstack/query-core@5.95.0
|
||||
|
||||
## 6.1.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix(\*): resolve issue about excluded build directory ([#10312](https://github.com/TanStack/query/pull/10312))
|
||||
|
||||
- Updated dependencies [[`4b6536d`](https://github.com/TanStack/query/commit/4b6536dfce99036f4e37f52943c6fed3ad0e0a18)]:
|
||||
- @tanstack/query-core@5.94.5
|
||||
|
||||
## 6.1.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- chore: fixed version ([#10064](https://github.com/TanStack/query/pull/10064))
|
||||
|
||||
- Updated dependencies [[`4c75210`](https://github.com/TanStack/query/commit/4c75210ce8235fe3d39b67e1029eff11278927cc)]:
|
||||
- @tanstack/query-core@5.94.4
|
||||
|
||||
## 6.1.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix(streamedQuery): maintain error state on reset refetch with initialData defined ([#10287](https://github.com/TanStack/query/pull/10287))
|
||||
|
||||
- Updated dependencies [[`248975e`](https://github.com/TanStack/query/commit/248975e896f585f6eaa505c796e73fcf7bfd1eec)]:
|
||||
- @tanstack/query-core@5.91.2
|
||||
|
||||
## 6.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`a89aab9`](https://github.com/TanStack/query/commit/a89aab975581c25c113a26c8af486b4cafad272a)]:
|
||||
- @tanstack/query-core@5.91.1
|
||||
|
||||
## 6.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`6fa901b`](https://github.com/TanStack/query/commit/6fa901b97a22a80d0fca3f6ed86237ff0cbdd13b)]:
|
||||
- @tanstack/query-core@5.91.0
|
||||
|
||||
## 6.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- feat(svelte-query): add 'mutationOptions' ([#10175](https://github.com/TanStack/query/pull/10175))
|
||||
|
||||
## 6.0.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`e7258c5`](https://github.com/TanStack/query/commit/e7258c5cb30cafa456cdb4e6bc75b43bf619954d)]:
|
||||
- @tanstack/query-core@5.90.20
|
||||
|
||||
## 6.0.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`53fc74e`](https://github.com/TanStack/query/commit/53fc74ebb16730bd3317f039a69c6821386bae93)]:
|
||||
- @tanstack/query-core@5.90.19
|
||||
|
||||
## 6.0.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`dea1614`](https://github.com/TanStack/query/commit/dea1614aaad5c572cf43cea54b64ac09dc4d5b41)]:
|
||||
- @tanstack/query-core@5.90.18
|
||||
|
||||
## 6.0.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`269351b`](https://github.com/TanStack/query/commit/269351b8ce4b4846da3d320ac5b850ee6aada0d6)]:
|
||||
- @tanstack/query-core@5.90.17
|
||||
|
||||
## 6.0.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`7f47906`](https://github.com/TanStack/query/commit/7f47906eaccc3f3aa5ce24b77a83bd7a620a237b)]:
|
||||
- @tanstack/query-core@5.90.16
|
||||
|
||||
## 6.0.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`fccef79`](https://github.com/TanStack/query/commit/fccef797d57d4a9566517bba87c8377f363920f2)]:
|
||||
- @tanstack/query-core@5.90.15
|
||||
|
||||
## 6.0.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`d576092`](https://github.com/TanStack/query/commit/d576092e2ece4ca3936add3eb0da5234c1d82ed4)]:
|
||||
- @tanstack/query-core@5.90.14
|
||||
|
||||
## 6.0.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`4a0a78a`](https://github.com/TanStack/query/commit/4a0a78afbc2432f8cb6828035965853fa98c86a0)]:
|
||||
- @tanstack/query-core@5.90.13
|
||||
|
||||
## 6.0.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`72d8ac5`](https://github.com/TanStack/query/commit/72d8ac5c592004b8f9c3ee086fcb9c3cd615ca05)]:
|
||||
- @tanstack/query-core@5.90.12
|
||||
|
||||
## 6.0.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`c01b150`](https://github.com/TanStack/query/commit/c01b150e3673e11d6533768529a5e6fe3ebee68c)]:
|
||||
- @tanstack/query-core@5.90.11
|
||||
|
||||
## 6.0.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`8e2e174`](https://github.com/TanStack/query/commit/8e2e174e9fd2e7b94cd232041e49c9d014d74e26), [`eb559a6`](https://github.com/TanStack/query/commit/eb559a66dc0d77dd46435f624fa64fc068bef9ae)]:
|
||||
- @tanstack/query-core@5.90.10
|
||||
|
||||
## 6.0.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`08b211f`](https://github.com/TanStack/query/commit/08b211f8aa475e05d2f13a36517fc556861ef962)]:
|
||||
- @tanstack/query-core@5.90.9
|
||||
|
||||
## 6.0.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`c0ec9fe`](https://github.com/TanStack/query/commit/c0ec9fe0d1426fe3f233adda3ebf23989ffaa110)]:
|
||||
- @tanstack/query-core@5.90.8
|
||||
|
||||
## 6.0.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`b4cd121`](https://github.com/TanStack/query/commit/b4cd121a39d07cefaa3a3411136d342cc54ce8fb)]:
|
||||
- @tanstack/query-core@5.90.7
|
||||
|
||||
## 6.0.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`1638c02`](https://github.com/TanStack/query/commit/1638c028df55648995d04431179904371a189772)]:
|
||||
- @tanstack/query-core@5.90.6
|
||||
|
||||
## 6.0.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`e42ddfe`](https://github.com/TanStack/query/commit/e42ddfe919f34f847ca101aeef162c69845f9a1e)]:
|
||||
- @tanstack/query-core@5.90.5
|
||||
|
||||
## 6.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`20ef922`](https://github.com/TanStack/query/commit/20ef922a0a7c3aee00150bf69123c338b0922922)]:
|
||||
- @tanstack/query-core@5.90.4
|
||||
|
||||
## 6.0.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`4e1c433`](https://github.com/TanStack/query/commit/4e1c4338a72f7384600bbda99e44bc1891695df4)]:
|
||||
- @tanstack/query-core@5.90.3
|
||||
|
||||
## 6.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
- BREAKING: Migrate to svelte runes (signals). Requires [Svelte v5.25.0](https://github.com/sveltejs/svelte/releases/tag/svelte%405.25.0) or newer. Please see the [migration guide](https://tanstack.com/query/latest/docs/framework/svelte/migrate-from-v5-to-v6). ([#9694](https://github.com/TanStack/query/pull/9694))
|
||||
32
packages/svelte-query/eslint.config.js
Normal file
32
packages/svelte-query/eslint.config.js
Normal file
@@ -0,0 +1,32 @@
|
||||
// @ts-check
|
||||
|
||||
import tsParser from '@typescript-eslint/parser'
|
||||
import pluginSvelte from 'eslint-plugin-svelte'
|
||||
import rootConfig from './root.eslint.config.js'
|
||||
import svelteConfig from './svelte.config.js'
|
||||
|
||||
export default [
|
||||
...rootConfig,
|
||||
...pluginSvelte.configs['recommended'],
|
||||
{
|
||||
files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
parser: tsParser,
|
||||
extraFileExtensions: ['.svelte'],
|
||||
svelteConfig,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
// Svelte runes and proxy state produce false positives for this rule.
|
||||
'@typescript-eslint/no-unnecessary-condition': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'svelte/block-lang': ['error', { script: ['ts'] }],
|
||||
'svelte/no-svelte-internal': 'error',
|
||||
'svelte/valid-compile': 'off',
|
||||
},
|
||||
},
|
||||
]
|
||||
68
packages/svelte-query/package.json
Normal file
68
packages/svelte-query/package.json
Normal file
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"name": "@tanstack/svelte-query",
|
||||
"version": "6.1.48",
|
||||
"description": "Primitives for managing, caching and syncing asynchronous and remote data in Svelte",
|
||||
"author": "Lachlan Collins",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/TanStack/query.git",
|
||||
"directory": "packages/svelte-query"
|
||||
},
|
||||
"homepage": "https://tanstack.com/query",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"keywords": [
|
||||
"tanstack",
|
||||
"query",
|
||||
"svelte"
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "premove ./dist ./coverage ./.svelte-kit ./dist-ts",
|
||||
"compile": "tsc --project tsconfig.json",
|
||||
"test:types": "svelte-check --tsconfig ./tsconfig.json",
|
||||
"test:eslint": "eslint --concurrency=auto ./src ./tests",
|
||||
"test:lib": "vitest",
|
||||
"test:lib:dev": "pnpm run test:lib --watch",
|
||||
"test:build": "publint --strict && attw --pack",
|
||||
"build": "svelte-package --input ./src --output ./dist"
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/index.d.ts",
|
||||
"module": "dist/index.js",
|
||||
"svelte": "./dist/index.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"@tanstack/custom-condition": "./src/index.ts",
|
||||
"types": "./dist/index.d.ts",
|
||||
"svelte": "./dist/index.js",
|
||||
"import": "./dist/index.js"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
"dist",
|
||||
"src",
|
||||
"!src/__tests__"
|
||||
],
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/package": "^2.5.8",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.1.1",
|
||||
"@tanstack/query-test-utils": "workspace:*",
|
||||
"@testing-library/svelte": "^5.2.8",
|
||||
"@typescript-eslint/parser": "^8.48.0",
|
||||
"eslint-plugin-svelte": "^3.11.0",
|
||||
"svelte": "^5.39.3",
|
||||
"svelte-check": "^4.4.5",
|
||||
"typescript": "5.9.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^5.25.0"
|
||||
}
|
||||
}
|
||||
1
packages/svelte-query/root.eslint.config.js
Symbolic link
1
packages/svelte-query/root.eslint.config.js
Symbolic link
@@ -0,0 +1 @@
|
||||
../../eslint.config.js
|
||||
27
packages/svelte-query/src/HydrationBoundary.svelte
Normal file
27
packages/svelte-query/src/HydrationBoundary.svelte
Normal file
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { useHydrate } from './useHydrate.js'
|
||||
import type { Snippet } from 'svelte'
|
||||
import type {
|
||||
DehydratedState,
|
||||
HydrateOptions,
|
||||
QueryClient,
|
||||
} from '@tanstack/query-core'
|
||||
|
||||
type Props = {
|
||||
children: Snippet
|
||||
state: DehydratedState
|
||||
options: HydrateOptions | undefined
|
||||
queryClient: QueryClient | undefined
|
||||
}
|
||||
|
||||
const {
|
||||
children,
|
||||
state,
|
||||
options = undefined,
|
||||
queryClient = undefined,
|
||||
}: Props = $props()
|
||||
|
||||
useHydrate(state, options, queryClient)
|
||||
</script>
|
||||
|
||||
{@render children()}
|
||||
21
packages/svelte-query/src/QueryClientProvider.svelte
Normal file
21
packages/svelte-query/src/QueryClientProvider.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import { QueryClient } from '@tanstack/query-core'
|
||||
import { setQueryClientContext } from './context.js'
|
||||
import type { QueryClientProviderProps } from './types.js'
|
||||
|
||||
const { client = new QueryClient(), children }: QueryClientProviderProps =
|
||||
$props()
|
||||
|
||||
onMount(() => {
|
||||
client.mount()
|
||||
})
|
||||
|
||||
setQueryClientContext(client)
|
||||
|
||||
onDestroy(() => {
|
||||
client.unmount()
|
||||
})
|
||||
</script>
|
||||
|
||||
{@render children()}
|
||||
149
packages/svelte-query/src/containers.svelte.ts
Normal file
149
packages/svelte-query/src/containers.svelte.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { SvelteSet, createSubscriber } from 'svelte/reactivity'
|
||||
|
||||
type VoidFn = () => void
|
||||
type Subscriber = (update: VoidFn) => void | VoidFn
|
||||
|
||||
export type Box<T> = { current: T }
|
||||
|
||||
export class ReactiveValue<T> implements Box<T> {
|
||||
#fn
|
||||
#subscribe
|
||||
|
||||
constructor(fn: () => T, onSubscribe: Subscriber) {
|
||||
this.#fn = fn
|
||||
this.#subscribe = createSubscriber((update) => onSubscribe(update))
|
||||
}
|
||||
|
||||
get current() {
|
||||
this.#subscribe()
|
||||
return this.#fn()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes all of the top-level keys of an object into $state.raw fields whose initial values
|
||||
* are the same as in the original object. Does not mutate the original object. Provides an `update`
|
||||
* function that _can_ (but does not have to be) be used to replace all of the object's top-level keys
|
||||
* with the values of the new object, while maintaining the original root object's reference.
|
||||
*/
|
||||
export function createRawRef<T extends {} | Array<unknown>>(
|
||||
init: T,
|
||||
): [T, (newValue: T) => void] {
|
||||
const refObj = (Array.isArray(init) ? [] : {}) as T
|
||||
const hiddenKeys = new SvelteSet<PropertyKey>()
|
||||
// Absent keys have no `$state.raw` field to subscribe to, and `length` is a
|
||||
// plain array property, so reads of either are tracked through this instead.
|
||||
// Without it, anything observed while the ref is empty never re-runs.
|
||||
let keyVersion = $state.raw(0)
|
||||
const trackKeys = () => keyVersion
|
||||
const out = new Proxy(refObj, {
|
||||
get(target, prop, receiver) {
|
||||
if (
|
||||
hiddenKeys.has(prop) ||
|
||||
!(prop in target) ||
|
||||
(Array.isArray(target) && prop === 'length')
|
||||
) {
|
||||
trackKeys()
|
||||
}
|
||||
return Reflect.get(target, prop, receiver)
|
||||
},
|
||||
set(target, prop, value, receiver) {
|
||||
hiddenKeys.delete(prop)
|
||||
if (prop in target) {
|
||||
return Reflect.set(target, prop, value, receiver)
|
||||
}
|
||||
let state = $state.raw(value)
|
||||
Object.defineProperty(target, prop, {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get: () => {
|
||||
// If this is a lazy value, we need to call it.
|
||||
// We can't do something like typeof state === 'function'
|
||||
// because the value could actually be a function that we don't want to call.
|
||||
return state && isBranded(state) ? state() : state
|
||||
},
|
||||
set: (v) => {
|
||||
state = v
|
||||
},
|
||||
})
|
||||
return true
|
||||
},
|
||||
has: (target, prop) => {
|
||||
if (hiddenKeys.has(prop)) {
|
||||
return false
|
||||
}
|
||||
return prop in target
|
||||
},
|
||||
ownKeys(target) {
|
||||
return Reflect.ownKeys(target).filter((key) => !hiddenKeys.has(key))
|
||||
},
|
||||
getOwnPropertyDescriptor(target, prop) {
|
||||
if (hiddenKeys.has(prop)) {
|
||||
return undefined
|
||||
}
|
||||
return Reflect.getOwnPropertyDescriptor(target, prop)
|
||||
},
|
||||
deleteProperty(target, prop) {
|
||||
if (prop in target) {
|
||||
// @ts-expect-error
|
||||
// We need to set the value to undefined to signal to the listeners that the value has changed.
|
||||
// If we just deleted it, the reactivity system wouldn't have any idea that the value was gone.
|
||||
target[prop] = undefined
|
||||
hiddenKeys.add(prop)
|
||||
if (Array.isArray(target)) {
|
||||
target.length--
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
})
|
||||
|
||||
function update(newValue: T) {
|
||||
const existingKeys = Object.keys(out)
|
||||
const newKeys = Object.keys(newValue)
|
||||
const keysToRemove = existingKeys.filter((key) => !newKeys.includes(key))
|
||||
// Arrays: delete in descending index order so each `deleteProperty` trap
|
||||
// sees the slot it is removing as the current tail (length-- stays valid).
|
||||
// Forward iteration would shrink the array under our feet and the next
|
||||
// index would no longer be `in target`, tripping the trap.
|
||||
if (Array.isArray(newValue)) {
|
||||
keysToRemove.sort((a, b) => Number(b) - Number(a))
|
||||
}
|
||||
const keysAdded = newKeys.some((key) => !existingKeys.includes(key))
|
||||
for (const key of keysToRemove) {
|
||||
// @ts-expect-error
|
||||
delete out[key]
|
||||
}
|
||||
for (const key of newKeys) {
|
||||
// @ts-expect-error
|
||||
// This craziness is required because TanStack Query defines getters for all of the keys on the object.
|
||||
// These getters track property access, so if we access all of them here, we'll end up tracking everything.
|
||||
// So we wrap the property access in a special function that we can identify later to lazily access the value.
|
||||
// (See above)
|
||||
out[key] = brand(() => newValue[key])
|
||||
}
|
||||
if (keysAdded || keysToRemove.length > 0) {
|
||||
keyVersion++
|
||||
}
|
||||
}
|
||||
|
||||
// we can't pass `init` directly into the proxy because it'll never set the state fields
|
||||
// (because (prop in target) will always be true)
|
||||
update(init)
|
||||
|
||||
return [out, update]
|
||||
}
|
||||
|
||||
const lazyBrand = Symbol('LazyValue')
|
||||
type Branded<T extends () => unknown> = T & { [lazyBrand]: true }
|
||||
|
||||
function brand<T extends () => unknown>(fn: T): Branded<T> {
|
||||
// @ts-expect-error
|
||||
fn[lazyBrand] = true
|
||||
return fn as Branded<T>
|
||||
}
|
||||
|
||||
function isBranded<T extends () => unknown>(fn: T): fn is Branded<T> {
|
||||
return Boolean((fn as Branded<T>)[lazyBrand])
|
||||
}
|
||||
66
packages/svelte-query/src/context.ts
Normal file
66
packages/svelte-query/src/context.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { getContext, setContext } from 'svelte'
|
||||
import type { QueryClient } from '@tanstack/query-core'
|
||||
import type { Box } from './containers.svelte'
|
||||
|
||||
const _contextKey = Symbol('QueryClient')
|
||||
|
||||
/**
|
||||
* Retrieves the `QueryClient` set on Svelte's context by `QueryClientProvider` (or by
|
||||
* {@link setQueryClientContext} directly). This is what {@link useQueryClient} calls internally.
|
||||
*
|
||||
* @throws If no `QueryClient` was found in context.
|
||||
* @returns The `QueryClient` set on context, whether by `QueryClientProvider` or {@link setQueryClientContext}.
|
||||
*/
|
||||
export const getQueryClientContext = (): QueryClient => {
|
||||
const client = getContext<QueryClient | undefined>(_contextKey)
|
||||
if (!client) {
|
||||
throw new Error(
|
||||
'No QueryClient was found in Svelte context. Did you forget to wrap your component with QueryClientProvider?',
|
||||
)
|
||||
}
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a `QueryClient` on Svelte's context, so it can be read with {@link getQueryClientContext} (or
|
||||
* {@link useQueryClient}) from any descendant component. `QueryClientProvider` wraps this — use it directly
|
||||
* only if you need to set the client from your own component instead.
|
||||
*
|
||||
* @param client - The `QueryClient` to make available to descendant components.
|
||||
*
|
||||
* @example
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { QueryClient, QueryClientProvider } from '@tanstack/svelte-query'
|
||||
*
|
||||
* const queryClient = new QueryClient()
|
||||
* </script>
|
||||
*
|
||||
* <QueryClientProvider client={queryClient}>
|
||||
* ...
|
||||
* </QueryClientProvider>
|
||||
* ```
|
||||
*/
|
||||
export const setQueryClientContext = (client: QueryClient): void => {
|
||||
setContext(_contextKey, client)
|
||||
}
|
||||
|
||||
const _isRestoringContextKey = Symbol('isRestoring')
|
||||
|
||||
/** Retrieves a `isRestoring` from Svelte's context */
|
||||
export const getIsRestoringContext = (): Box<boolean> => {
|
||||
try {
|
||||
const isRestoring = getContext<Box<boolean> | undefined>(
|
||||
_isRestoringContextKey,
|
||||
)
|
||||
return isRestoring ?? { current: false }
|
||||
} catch (error) {
|
||||
return { current: false }
|
||||
}
|
||||
}
|
||||
|
||||
/** Sets a `isRestoring` on Svelte's context */
|
||||
export const setIsRestoringContext = (isRestoring: Box<boolean>): void => {
|
||||
setContext(_isRestoringContextKey, isRestoring)
|
||||
}
|
||||
107
packages/svelte-query/src/createBaseQuery.svelte.ts
Normal file
107
packages/svelte-query/src/createBaseQuery.svelte.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { useIsRestoring } from './useIsRestoring.js'
|
||||
import { useQueryClient } from './useQueryClient.js'
|
||||
import { createRawRef } from './containers.svelte.js'
|
||||
import { watchChanges } from './utils.svelte.js'
|
||||
import type { QueryClient, QueryKey, QueryObserver } from '@tanstack/query-core'
|
||||
import type {
|
||||
Accessor,
|
||||
CreateBaseQueryOptions,
|
||||
CreateBaseQueryResult,
|
||||
} from './types.js'
|
||||
|
||||
/**
|
||||
* Base implementation for `createQuery` and `createInfiniteQuery`
|
||||
* @param options - A function that returns query options
|
||||
* @param Observer - The observer from query-core
|
||||
* @param queryClient - Custom query client which overrides provider
|
||||
*/
|
||||
export function createBaseQuery<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryData,
|
||||
TQueryKey extends QueryKey,
|
||||
>(
|
||||
options: Accessor<
|
||||
CreateBaseQueryOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>
|
||||
>,
|
||||
Observer: typeof QueryObserver,
|
||||
queryClient?: Accessor<QueryClient>,
|
||||
): CreateBaseQueryResult<TData, TError> {
|
||||
/** Load query client */
|
||||
const client = $derived(useQueryClient(queryClient?.()))
|
||||
const isRestoring = useIsRestoring()
|
||||
|
||||
const resolvedOptions = $derived.by(() => {
|
||||
const opts = client.defaultQueryOptions(options())
|
||||
opts._optimisticResults = isRestoring.current ? 'isRestoring' : 'optimistic'
|
||||
return opts
|
||||
})
|
||||
|
||||
/** Creates the observer */
|
||||
// svelte-ignore state_referenced_locally - intentional, initial value
|
||||
let observer = $state(
|
||||
new Observer<TQueryFnData, TError, TData, TQueryData, TQueryKey>(
|
||||
client,
|
||||
resolvedOptions,
|
||||
),
|
||||
)
|
||||
watchChanges(
|
||||
() => client,
|
||||
'pre',
|
||||
() => {
|
||||
observer = new Observer<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryData,
|
||||
TQueryKey
|
||||
>(client, resolvedOptions)
|
||||
},
|
||||
)
|
||||
|
||||
function createResult() {
|
||||
const result = observer.getOptimisticResult(resolvedOptions)
|
||||
return !resolvedOptions.notifyOnChangeProps
|
||||
? observer.trackResult(result)
|
||||
: result
|
||||
}
|
||||
const [query, update] = createRawRef(
|
||||
// svelte-ignore state_referenced_locally - intentional, initial value
|
||||
createResult(),
|
||||
)
|
||||
|
||||
$effect(() => {
|
||||
const unsubscribe = isRestoring.current
|
||||
? () => undefined
|
||||
: observer.subscribe(() => update(createResult()))
|
||||
observer.updateResult()
|
||||
return unsubscribe
|
||||
})
|
||||
|
||||
watchChanges(
|
||||
() => resolvedOptions,
|
||||
'pre',
|
||||
() => {
|
||||
observer.setOptions(resolvedOptions)
|
||||
},
|
||||
)
|
||||
watchChanges(
|
||||
() => [resolvedOptions, observer],
|
||||
'pre',
|
||||
() => {
|
||||
// The only reason this is necessary is because of `isRestoring`.
|
||||
// Because we don't subscribe while restoring, the following can occur:
|
||||
// - `isRestoring` is true
|
||||
// - `isRestoring` becomes false
|
||||
// - `observer.subscribe` and `observer.updateResult` is called in the above effect,
|
||||
// but the subsequent `fetch` has already completed
|
||||
// - `result` misses the intermediate restored-but-not-fetched state
|
||||
//
|
||||
// this could technically be its own effect but that doesn't seem necessary
|
||||
update(createResult())
|
||||
},
|
||||
)
|
||||
|
||||
return query
|
||||
}
|
||||
180
packages/svelte-query/src/createInfiniteQuery.ts
Normal file
180
packages/svelte-query/src/createInfiniteQuery.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import { InfiniteQueryObserver } from '@tanstack/query-core'
|
||||
import { createBaseQuery } from './createBaseQuery.svelte.js'
|
||||
import type {
|
||||
DefaultError,
|
||||
InfiniteData,
|
||||
QueryClient,
|
||||
QueryKey,
|
||||
QueryObserver,
|
||||
} from '@tanstack/query-core'
|
||||
import type {
|
||||
Accessor,
|
||||
CreateInfiniteQueryOptions,
|
||||
CreateInfiniteQueryResult,
|
||||
DefinedCreateInfiniteQueryResult,
|
||||
} from './types.js'
|
||||
import type {
|
||||
DefinedInitialDataInfiniteOptions,
|
||||
UndefinedInitialDataInfiniteOptions,
|
||||
} from './infiniteQueryOptions.js'
|
||||
|
||||
export function createInfiniteQuery<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = InfiniteData<TQueryFnData>,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
>(
|
||||
options: Accessor<
|
||||
DefinedInitialDataInfiniteOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>
|
||||
>,
|
||||
queryClient?: Accessor<QueryClient>,
|
||||
): DefinedCreateInfiniteQueryResult<TData, TError>
|
||||
|
||||
export function createInfiniteQuery<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = InfiniteData<TQueryFnData>,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
>(
|
||||
options: Accessor<
|
||||
UndefinedInitialDataInfiniteOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>
|
||||
>,
|
||||
queryClient?: Accessor<QueryClient>,
|
||||
): CreateInfiniteQueryResult<TData, TError>
|
||||
|
||||
/**
|
||||
* @see {@link infiniteQueryOptions} to share these options between `createInfiniteQuery` and imperative APIs
|
||||
* like `queryClient.infiniteQuery`.
|
||||
* @param options - The {@link CreateInfiniteQueryOptions} to use — everything you can pass to
|
||||
* `createInfiniteQuery`, wrapped in an {@link Accessor} so options can be reactive.
|
||||
* @param queryClient - Use this to use a custom `QueryClient`. Otherwise, the one from the nearest context will
|
||||
* be used.
|
||||
* @returns The current query result, plus `fetchNextPage`/`fetchPreviousPage`/`hasNextPage`/`hasPreviousPage`
|
||||
* to page through the query.
|
||||
*
|
||||
* @example
|
||||
* Fetching the next page from a "Load More" button click:
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { createInfiniteQuery } from '@tanstack/svelte-query'
|
||||
*
|
||||
* const query = createInfiniteQuery(() => ({
|
||||
* queryKey: ['projects'],
|
||||
* queryFn: ({ pageParam }) => fetchProjects(pageParam),
|
||||
* initialPageParam: 0,
|
||||
* getNextPageParam: (lastPage) => lastPage.nextId,
|
||||
* }))
|
||||
* </script>
|
||||
*
|
||||
* {#if query.isPending}
|
||||
* Loading...
|
||||
* {:else if query.isError}
|
||||
* <span>Error: {query.error.message}</span>
|
||||
* {:else}
|
||||
* <ul>
|
||||
* {#each query.data.pages as page}
|
||||
* {#each page.projects as project (project.id)}
|
||||
* <li>{project.name}</li>
|
||||
* {/each}
|
||||
* {/each}
|
||||
* </ul>
|
||||
* <button
|
||||
* onclick={() => query.fetchNextPage()}
|
||||
* disabled={!query.hasNextPage || query.isFetching}
|
||||
* >
|
||||
* {query.isFetchingNextPage
|
||||
* ? 'Loading more...'
|
||||
* : query.hasNextPage
|
||||
* ? 'Load More'
|
||||
* : 'Nothing more to load'}
|
||||
* </button>
|
||||
* {/if}
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* Fetching the next page automatically as the user scrolls, using an `IntersectionObserver` on a
|
||||
* sentinel element after the list:
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { createInfiniteQuery } from '@tanstack/svelte-query'
|
||||
*
|
||||
* const query = createInfiniteQuery(() => ({
|
||||
* queryKey: ['projects'],
|
||||
* queryFn: ({ pageParam }) => fetchProjects(pageParam),
|
||||
* initialPageParam: 0,
|
||||
* getNextPageParam: (lastPage) => lastPage.nextId,
|
||||
* }))
|
||||
*
|
||||
* let sentinel: HTMLDivElement | undefined = $state()
|
||||
*
|
||||
* $effect(() => {
|
||||
* if (sentinel == null || !query.hasNextPage || query.isFetching) return
|
||||
*
|
||||
* const observer = new IntersectionObserver(([entry]) => {
|
||||
* if (entry?.isIntersecting) query.fetchNextPage()
|
||||
* })
|
||||
* observer.observe(sentinel)
|
||||
*
|
||||
* return () => observer.disconnect()
|
||||
* })
|
||||
* </script>
|
||||
*
|
||||
* {#if query.isPending}
|
||||
* Loading...
|
||||
* {:else if query.isError}
|
||||
* <span>Error: {query.error.message}</span>
|
||||
* {:else}
|
||||
* <ul>
|
||||
* {#each query.data.pages as page}
|
||||
* {#each page.projects as project (project.id)}
|
||||
* <li>{project.name}</li>
|
||||
* {/each}
|
||||
* {/each}
|
||||
* </ul>
|
||||
* <div bind:this={sentinel}></div>
|
||||
* {/if}
|
||||
* ```
|
||||
*/
|
||||
export function createInfiniteQuery<
|
||||
TQueryFnData,
|
||||
TError = DefaultError,
|
||||
TData = InfiniteData<TQueryFnData>,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
>(
|
||||
options: Accessor<
|
||||
CreateInfiniteQueryOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>
|
||||
>,
|
||||
queryClient?: Accessor<QueryClient>,
|
||||
): CreateInfiniteQueryResult<TData, TError>
|
||||
|
||||
export function createInfiniteQuery(
|
||||
options: Accessor<CreateInfiniteQueryOptions>,
|
||||
queryClient?: Accessor<QueryClient>,
|
||||
) {
|
||||
return createBaseQuery(
|
||||
options,
|
||||
InfiniteQueryObserver as typeof QueryObserver,
|
||||
queryClient,
|
||||
)
|
||||
}
|
||||
244
packages/svelte-query/src/createMutation.svelte.ts
Normal file
244
packages/svelte-query/src/createMutation.svelte.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
import { MutationObserver, noop, notifyManager } from '@tanstack/query-core'
|
||||
import { useQueryClient } from './useQueryClient.js'
|
||||
import { watchChanges } from './utils.svelte.js'
|
||||
import type {
|
||||
Accessor,
|
||||
CreateMutateFunction,
|
||||
CreateMutationOptions,
|
||||
CreateMutationResult,
|
||||
} from './types.js'
|
||||
|
||||
import type { DefaultError, QueryClient } from '@tanstack/query-core'
|
||||
|
||||
/**
|
||||
* Unlike queries, mutations are typically used to create/update/delete data or perform server side-effects.
|
||||
* `createMutation` is the function for that.
|
||||
*
|
||||
* @see {@link mutationOptions} to share these options across multiple `createMutation` call sites, or to look
|
||||
* the mutation up elsewhere via its `mutationKey` (e.g. with `useMutationState`).
|
||||
* @param options - The {@link CreateMutationOptions} to use, wrapped in an {@link Accessor} so options can be
|
||||
* reactive.
|
||||
* @param queryClient - Use this to use a custom `QueryClient`. Otherwise, the one from the nearest context will
|
||||
* be used.
|
||||
* @returns `mutate`/`mutateAsync` also accept per-call `onSuccess`/`onError`/`onSettled` callbacks as a second
|
||||
* argument, useful for triggering call-site side effects (e.g. navigation) without coupling them to the shared
|
||||
* mutation definition. If you make multiple requests, `onSuccess` will fire only after the latest call you've
|
||||
* made.
|
||||
*
|
||||
* @example
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { createMutation, useQueryClient } from '@tanstack/svelte-query'
|
||||
*
|
||||
* const queryClient = useQueryClient()
|
||||
*
|
||||
* const addMutation = createMutation(() => ({
|
||||
* mutationFn: addTodo,
|
||||
* onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
|
||||
* }))
|
||||
* </script>
|
||||
*
|
||||
* <button
|
||||
* onclick={() =>
|
||||
* addMutation.mutate('Item', {
|
||||
* onError: (error) => console.error('Failed to add item:', error),
|
||||
* })
|
||||
* }
|
||||
* >
|
||||
* Add
|
||||
* </button>
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* Rendering the mutation's own state, rather than just firing it off:
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { createMutation, useQueryClient } from '@tanstack/svelte-query'
|
||||
*
|
||||
* const queryClient = useQueryClient()
|
||||
*
|
||||
* const addMutation = createMutation(() => ({
|
||||
* mutationFn: addTodo,
|
||||
* onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
|
||||
* }))
|
||||
* </script>
|
||||
*
|
||||
* {#if addMutation.isPending}
|
||||
* Adding todo...
|
||||
* {:else}
|
||||
* {#if addMutation.isError}
|
||||
* <div>An error occurred: {addMutation.error.message}</div>
|
||||
* {/if}
|
||||
* <button onclick={() => addMutation.mutate('Item')}>Add</button>
|
||||
* {/if}
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* Optimistic update via `onMutate`, rolling back on `onError`:
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { createMutation, useQueryClient } from '@tanstack/svelte-query'
|
||||
*
|
||||
* const queryClient = useQueryClient()
|
||||
*
|
||||
* const addMutation = createMutation(() => ({
|
||||
* mutationFn: addTodo,
|
||||
* onMutate: async (newTodo: string) => {
|
||||
* await queryClient.cancelQueries({ queryKey: ['todos'] })
|
||||
* const previousTodos = queryClient.getQueryData<Array<string>>(['todos'])
|
||||
*
|
||||
* queryClient.setQueryData<Array<string>>(['todos'], (old) => [
|
||||
* ...(old ?? []),
|
||||
* newTodo,
|
||||
* ])
|
||||
*
|
||||
* // Passed to `onError` as `onMutateResult` if the mutation fails.
|
||||
* return { previousTodos }
|
||||
* },
|
||||
* onError: (_err, _newTodo, onMutateResult) => {
|
||||
* queryClient.setQueryData(['todos'], onMutateResult?.previousTodos)
|
||||
* },
|
||||
* onSettled: () => {
|
||||
* queryClient.invalidateQueries({ queryKey: ['todos'] })
|
||||
* },
|
||||
* }))
|
||||
* </script>
|
||||
*
|
||||
* <button onclick={() => addMutation.mutate('Item')}>Add</button>
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* Callbacks passed per call to `mutate` only fire for the last call — `mutateAsync` gives you a
|
||||
* promise per call instead, so you can wait for all of them when they succeed:
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { createMutation, useQueryClient } from '@tanstack/svelte-query'
|
||||
*
|
||||
* const queryClient = useQueryClient()
|
||||
*
|
||||
* const addMutation = createMutation(() => ({
|
||||
* mutationFn: addTodo,
|
||||
* onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
|
||||
* }))
|
||||
*
|
||||
* async function handleAddAll(todos: Array<string>) {
|
||||
* try {
|
||||
* await Promise.all(todos.map((todo) => addMutation.mutateAsync(todo)))
|
||||
* } catch (error) {
|
||||
* console.error('Failed to add todos:', error)
|
||||
* }
|
||||
* }
|
||||
* </script>
|
||||
*
|
||||
* <button onclick={() => handleAddAll(['Todo 1', 'Todo 2', 'Todo 3'])}>
|
||||
* Add all
|
||||
* </button>
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* If some of the mutations above can fail independently of the others, and you want to know which ones
|
||||
* did — rather than losing that information the moment the first one rejects — swap `Promise.all` for
|
||||
* `Promise.allSettled`:
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { createMutation, useQueryClient } from '@tanstack/svelte-query'
|
||||
*
|
||||
* const queryClient = useQueryClient()
|
||||
*
|
||||
* const addMutation = createMutation(() => ({
|
||||
* mutationFn: addTodo,
|
||||
* onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
|
||||
* }))
|
||||
*
|
||||
* async function handleAddAll(todos: Array<string>) {
|
||||
* const addResults = await Promise.allSettled(
|
||||
* todos.map((todo) => addMutation.mutateAsync(todo)),
|
||||
* )
|
||||
*
|
||||
* addResults.forEach((addResult, index) => {
|
||||
* if (addResult.status === 'rejected') {
|
||||
* console.error(`Failed to add "${todos[index]}":`, addResult.reason)
|
||||
* }
|
||||
* })
|
||||
* }
|
||||
* </script>
|
||||
*
|
||||
* <button onclick={() => handleAddAll(['Todo 1', 'Todo 2', 'Todo 3'])}>
|
||||
* Add all
|
||||
* </button>
|
||||
* ```
|
||||
*/
|
||||
export function createMutation<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
TVariables = void,
|
||||
TContext = unknown,
|
||||
>(
|
||||
options: Accessor<CreateMutationOptions<TData, TError, TVariables, TContext>>,
|
||||
queryClient?: Accessor<QueryClient>,
|
||||
): CreateMutationResult<TData, TError, TVariables, TContext> {
|
||||
const client = $derived(useQueryClient(queryClient?.()))
|
||||
|
||||
// svelte-ignore state_referenced_locally - intentional, initial value
|
||||
let observer = $state(
|
||||
// svelte-ignore state_referenced_locally - intentional, initial value
|
||||
new MutationObserver<TData, TError, TVariables, TContext>(
|
||||
client,
|
||||
options(),
|
||||
),
|
||||
)
|
||||
|
||||
watchChanges(
|
||||
() => client,
|
||||
'pre',
|
||||
() => {
|
||||
observer = new MutationObserver(client, options())
|
||||
},
|
||||
)
|
||||
|
||||
$effect.pre(() => {
|
||||
observer.setOptions(options())
|
||||
})
|
||||
|
||||
const mutate: CreateMutateFunction<TData, TError, TVariables, TContext> = (
|
||||
...args
|
||||
) => {
|
||||
observer.mutate(args[0] as TVariables, args[1]).catch(noop)
|
||||
}
|
||||
|
||||
let result = $state(observer.getCurrentResult())
|
||||
watchChanges(
|
||||
() => observer,
|
||||
'pre',
|
||||
() => {
|
||||
result = observer.getCurrentResult()
|
||||
},
|
||||
)
|
||||
|
||||
$effect.pre(() => {
|
||||
const unsubscribe = observer.subscribe((val) => {
|
||||
notifyManager.batchCalls(() => {
|
||||
Object.assign(result, val)
|
||||
})()
|
||||
})
|
||||
return unsubscribe
|
||||
})
|
||||
|
||||
const resultProxy = $derived(
|
||||
new Proxy(result, {
|
||||
get: (_, prop) => {
|
||||
const r = {
|
||||
...result,
|
||||
mutate,
|
||||
mutateAsync: result.mutate,
|
||||
}
|
||||
if (prop == 'value') return r
|
||||
// @ts-expect-error
|
||||
return r[prop]
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
// @ts-expect-error
|
||||
return resultProxy
|
||||
}
|
||||
325
packages/svelte-query/src/createQueries.svelte.ts
Normal file
325
packages/svelte-query/src/createQueries.svelte.ts
Normal file
@@ -0,0 +1,325 @@
|
||||
import { QueriesObserver } from '@tanstack/query-core'
|
||||
import { useIsRestoring } from './useIsRestoring.js'
|
||||
import { createRawRef } from './containers.svelte.js'
|
||||
import { useQueryClient } from './useQueryClient.js'
|
||||
import type {
|
||||
Accessor,
|
||||
CreateQueryOptions,
|
||||
CreateQueryResult,
|
||||
DefinedCreateQueryResult,
|
||||
} from './types.js'
|
||||
import type {
|
||||
DefaultError,
|
||||
OmitKeyof,
|
||||
QueriesObserverOptions,
|
||||
QueriesPlaceholderDataFunction,
|
||||
QueryClient,
|
||||
QueryFunction,
|
||||
QueryKey,
|
||||
ThrowOnError,
|
||||
} from '@tanstack/query-core'
|
||||
|
||||
// This defines the `CreateQueryOptions` that are accepted in `QueriesOptions` & `GetOptions`.
|
||||
// `placeholderData` function always gets undefined passed
|
||||
type CreateQueryOptionsForCreateQueries<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
> = OmitKeyof<
|
||||
CreateQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
|
||||
'placeholderData'
|
||||
> & {
|
||||
placeholderData?: TQueryFnData | QueriesPlaceholderDataFunction<TQueryFnData>
|
||||
}
|
||||
|
||||
// Avoid TS depth-limit error in case of large array literal
|
||||
type MAXIMUM_DEPTH = 20
|
||||
|
||||
// Widen the type of the symbol to enable type inference even if skipToken is not immutable.
|
||||
type SkipTokenForCreateQueries = symbol
|
||||
|
||||
type GetCreateQueryOptionsForCreateQueries<T> =
|
||||
// Part 1: responsible for applying explicit type parameter to function arguments, if object { queryFnData: TQueryFnData, error: TError, data: TData }
|
||||
T extends {
|
||||
queryFnData: infer TQueryFnData
|
||||
error?: infer TError
|
||||
data: infer TData
|
||||
}
|
||||
? CreateQueryOptionsForCreateQueries<TQueryFnData, TError, TData>
|
||||
: T extends { queryFnData: infer TQueryFnData; error?: infer TError }
|
||||
? CreateQueryOptionsForCreateQueries<TQueryFnData, TError>
|
||||
: T extends { data: infer TData; error?: infer TError }
|
||||
? CreateQueryOptionsForCreateQueries<unknown, TError, TData>
|
||||
: // Part 2: responsible for applying explicit type parameter to function arguments, if tuple [TQueryFnData, TError, TData]
|
||||
T extends [infer TQueryFnData, infer TError, infer TData]
|
||||
? CreateQueryOptionsForCreateQueries<TQueryFnData, TError, TData>
|
||||
: T extends [infer TQueryFnData, infer TError]
|
||||
? CreateQueryOptionsForCreateQueries<TQueryFnData, TError>
|
||||
: T extends [infer TQueryFnData]
|
||||
? CreateQueryOptionsForCreateQueries<TQueryFnData>
|
||||
: // Part 3: responsible for inferring and enforcing type if no explicit parameter was provided
|
||||
T extends {
|
||||
queryFn?:
|
||||
| QueryFunction<infer TQueryFnData, infer TQueryKey>
|
||||
| SkipTokenForCreateQueries
|
||||
select?: (data: any) => infer TData
|
||||
throwOnError?: ThrowOnError<any, infer TError, any, any>
|
||||
}
|
||||
? CreateQueryOptionsForCreateQueries<
|
||||
TQueryFnData,
|
||||
unknown extends TError ? DefaultError : TError,
|
||||
unknown extends TData ? TQueryFnData : TData,
|
||||
TQueryKey
|
||||
>
|
||||
: // Fallback
|
||||
CreateQueryOptionsForCreateQueries
|
||||
|
||||
// A defined initialData setting should return a DefinedCreateQueryResult rather than CreateQueryResult
|
||||
type GetDefinedOrUndefinedQueryResult<T, TData, TError = unknown> = T extends {
|
||||
initialData?: infer TInitialData
|
||||
}
|
||||
? unknown extends TInitialData
|
||||
? CreateQueryResult<TData, TError>
|
||||
: TInitialData extends TData
|
||||
? DefinedCreateQueryResult<TData, TError>
|
||||
: TInitialData extends () => infer TInitialDataResult
|
||||
? unknown extends TInitialDataResult
|
||||
? CreateQueryResult<TData, TError>
|
||||
: TInitialDataResult extends TData
|
||||
? DefinedCreateQueryResult<TData, TError>
|
||||
: CreateQueryResult<TData, TError>
|
||||
: CreateQueryResult<TData, TError>
|
||||
: CreateQueryResult<TData, TError>
|
||||
|
||||
type GetCreateQueryResult<T> =
|
||||
// Part 1: responsible for mapping explicit type parameter to function result, if object
|
||||
T extends { queryFnData: any; error?: infer TError; data: infer TData }
|
||||
? GetDefinedOrUndefinedQueryResult<T, TData, TError>
|
||||
: T extends { queryFnData: infer TQueryFnData; error?: infer TError }
|
||||
? GetDefinedOrUndefinedQueryResult<T, TQueryFnData, TError>
|
||||
: T extends { data: infer TData; error?: infer TError }
|
||||
? GetDefinedOrUndefinedQueryResult<T, TData, TError>
|
||||
: // Part 2: responsible for mapping explicit type parameter to function result, if tuple
|
||||
T extends [any, infer TError, infer TData]
|
||||
? GetDefinedOrUndefinedQueryResult<T, TData, TError>
|
||||
: T extends [infer TQueryFnData, infer TError]
|
||||
? GetDefinedOrUndefinedQueryResult<T, TQueryFnData, TError>
|
||||
: T extends [infer TQueryFnData]
|
||||
? GetDefinedOrUndefinedQueryResult<T, TQueryFnData>
|
||||
: // Part 3: responsible for mapping inferred type to results, if no explicit parameter was provided
|
||||
T extends {
|
||||
queryFn?:
|
||||
| QueryFunction<infer TQueryFnData, any>
|
||||
| SkipTokenForCreateQueries
|
||||
select?: (data: any) => infer TData
|
||||
throwOnError?: ThrowOnError<any, infer TError, any, any>
|
||||
}
|
||||
? GetDefinedOrUndefinedQueryResult<
|
||||
T,
|
||||
unknown extends TData ? TQueryFnData : TData,
|
||||
unknown extends TError ? DefaultError : TError
|
||||
>
|
||||
: // Fallback
|
||||
CreateQueryResult
|
||||
|
||||
/**
|
||||
* QueriesOptions reducer recursively unwraps function arguments to infer/enforce type param
|
||||
*/
|
||||
export type QueriesOptions<
|
||||
T extends Array<any>,
|
||||
TResults extends Array<any> = [],
|
||||
TDepth extends ReadonlyArray<number> = [],
|
||||
> = TDepth['length'] extends MAXIMUM_DEPTH
|
||||
? Array<CreateQueryOptionsForCreateQueries>
|
||||
: T extends []
|
||||
? []
|
||||
: T extends [infer Head]
|
||||
? [...TResults, GetCreateQueryOptionsForCreateQueries<Head>]
|
||||
: T extends [infer Head, ...infer Tails]
|
||||
? QueriesOptions<
|
||||
[...Tails],
|
||||
[...TResults, GetCreateQueryOptionsForCreateQueries<Head>],
|
||||
[...TDepth, 1]
|
||||
>
|
||||
: ReadonlyArray<unknown> extends T
|
||||
? T
|
||||
: // If T is *some* array but we couldn't assign unknown[] to it, then it must hold some known/homogeneous type!
|
||||
// use this to infer the param types in the case of Array.map() argument
|
||||
T extends Array<
|
||||
CreateQueryOptionsForCreateQueries<
|
||||
infer TQueryFnData,
|
||||
infer TError,
|
||||
infer TData,
|
||||
infer TQueryKey
|
||||
>
|
||||
>
|
||||
? Array<
|
||||
CreateQueryOptionsForCreateQueries<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey
|
||||
>
|
||||
>
|
||||
: // Fallback
|
||||
Array<CreateQueryOptionsForCreateQueries>
|
||||
|
||||
/**
|
||||
* QueriesResults reducer recursively maps type param to results
|
||||
*/
|
||||
export type QueriesResults<
|
||||
T extends Array<any>,
|
||||
TResults extends Array<any> = [],
|
||||
TDepth extends ReadonlyArray<number> = [],
|
||||
> = TDepth['length'] extends MAXIMUM_DEPTH
|
||||
? Array<CreateQueryResult>
|
||||
: T extends []
|
||||
? []
|
||||
: T extends [infer Head]
|
||||
? [...TResults, GetCreateQueryResult<Head>]
|
||||
: T extends [infer Head, ...infer Tails]
|
||||
? QueriesResults<
|
||||
[...Tails],
|
||||
[...TResults, GetCreateQueryResult<Head>],
|
||||
[...TDepth, 1]
|
||||
>
|
||||
: { [K in keyof T]: GetCreateQueryResult<T[K]> }
|
||||
|
||||
/**
|
||||
* @param createQueriesOptions - The `queries` array to run, and an optional `combine` function, wrapped in an
|
||||
* {@link Accessor} so options can be reactive.
|
||||
* @param queryClient - Use this to use a custom `QueryClient`. Otherwise, the one from the nearest context
|
||||
* will be used.
|
||||
* @returns An array with one result per query, in the same order as `queries` — or, if `combine` is provided,
|
||||
* whatever `combine` returns.
|
||||
*
|
||||
* @example
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { createQueries } from '@tanstack/svelte-query'
|
||||
*
|
||||
* let { ids }: { ids: Array<number> } = $props()
|
||||
*
|
||||
* const postQueries = createQueries(() => ({
|
||||
* queries: ids.map((id) => ({
|
||||
* queryKey: ['post', id],
|
||||
* queryFn: () => fetchPost(id),
|
||||
* staleTime: Infinity,
|
||||
* })),
|
||||
* }))
|
||||
* </script>
|
||||
*
|
||||
* <ul>
|
||||
* {#each postQueries as query, index (ids[index])}
|
||||
* {#if query.isPending}
|
||||
* <li>Loading...</li>
|
||||
* {:else if query.isError}
|
||||
* <li>Error: {query.error.message}</li>
|
||||
* {:else}
|
||||
* <li>{query.data.title}</li>
|
||||
* {/if}
|
||||
* {/each}
|
||||
* </ul>
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* Combining results into a single value:
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { createQueries } from '@tanstack/svelte-query'
|
||||
*
|
||||
* let { ids }: { ids: Array<number> } = $props()
|
||||
*
|
||||
* const combined = createQueries(() => ({
|
||||
* queries: ids.map((id) => ({
|
||||
* queryKey: ['post', id],
|
||||
* queryFn: () => fetchPost(id),
|
||||
* })),
|
||||
* combine: (postQueries) => ({
|
||||
* data: postQueries.map((query) => query.data),
|
||||
* isPending: postQueries.some((query) => query.isPending),
|
||||
* isError: postQueries.some((query) => query.isError),
|
||||
* }),
|
||||
* }))
|
||||
* </script>
|
||||
*
|
||||
* {#if combined.isPending}
|
||||
* Loading...
|
||||
* {:else if combined.isError}
|
||||
* Error loading posts
|
||||
* {:else}
|
||||
* <ul>
|
||||
* {#each combined.data as post}
|
||||
* <li>{post?.title}</li>
|
||||
* {/each}
|
||||
* </ul>
|
||||
* {/if}
|
||||
* ```
|
||||
*/
|
||||
export function createQueries<
|
||||
T extends Array<any>,
|
||||
TCombinedResult = QueriesResults<T>,
|
||||
>(
|
||||
createQueriesOptions: Accessor<{
|
||||
queries:
|
||||
| readonly [...QueriesOptions<T>]
|
||||
| readonly [
|
||||
...{ [K in keyof T]: GetCreateQueryOptionsForCreateQueries<T[K]> },
|
||||
]
|
||||
combine?: (result: QueriesResults<T>) => TCombinedResult
|
||||
}>,
|
||||
queryClient?: Accessor<QueryClient>,
|
||||
): TCombinedResult {
|
||||
const client = $derived(useQueryClient(queryClient?.()))
|
||||
const isRestoring = useIsRestoring()
|
||||
|
||||
const { queries, combine } = $derived.by(createQueriesOptions)
|
||||
const resolvedQueryOptions = $derived(
|
||||
queries.map((opts) => {
|
||||
const resolvedOptions = client.defaultQueryOptions(opts)
|
||||
// Make sure the results are already in fetching state before subscribing or updating options
|
||||
resolvedOptions._optimisticResults = isRestoring.current
|
||||
? 'isRestoring'
|
||||
: 'optimistic'
|
||||
return resolvedOptions
|
||||
}),
|
||||
)
|
||||
|
||||
// can't do same as createMutation, as QueriesObserver has no `setOptions` method
|
||||
const observer = $derived(
|
||||
new QueriesObserver<TCombinedResult>(
|
||||
client,
|
||||
resolvedQueryOptions,
|
||||
combine as QueriesObserverOptions<TCombinedResult>,
|
||||
),
|
||||
)
|
||||
|
||||
function createResult() {
|
||||
const [_, getCombinedResult, trackResult] = observer.getOptimisticResult(
|
||||
resolvedQueryOptions,
|
||||
combine as QueriesObserverOptions<TCombinedResult>['combine'],
|
||||
)
|
||||
return getCombinedResult(trackResult())
|
||||
}
|
||||
|
||||
// @ts-expect-error - the crazy-complex TCombinedResult type doesn't like being called an array
|
||||
// svelte-ignore state_referenced_locally
|
||||
const [results, update] = createRawRef<TCombinedResult>(createResult())
|
||||
|
||||
$effect(() => {
|
||||
const unsubscribe = isRestoring.current
|
||||
? () => undefined
|
||||
: observer.subscribe(() => update(createResult()))
|
||||
return unsubscribe
|
||||
})
|
||||
|
||||
$effect.pre(() => {
|
||||
observer.setQueries(resolvedQueryOptions, {
|
||||
combine,
|
||||
} as QueriesObserverOptions<TCombinedResult>)
|
||||
update(createResult())
|
||||
})
|
||||
|
||||
return results
|
||||
}
|
||||
263
packages/svelte-query/src/createQuery.ts
Normal file
263
packages/svelte-query/src/createQuery.ts
Normal file
@@ -0,0 +1,263 @@
|
||||
import { QueryObserver } from '@tanstack/query-core'
|
||||
import { createBaseQuery } from './createBaseQuery.svelte.js'
|
||||
import type { DefaultError, QueryClient, QueryKey } from '@tanstack/query-core'
|
||||
import type {
|
||||
Accessor,
|
||||
CreateQueryOptions,
|
||||
CreateQueryResult,
|
||||
DefinedCreateQueryResult,
|
||||
} from './types.js'
|
||||
import type {
|
||||
DefinedInitialDataOptions,
|
||||
UndefinedInitialDataOptions,
|
||||
} from './queryOptions.js'
|
||||
|
||||
/**
|
||||
* @see {@link queryOptions} to share these options between `createQuery` and imperative APIs like `queryClient.query`.
|
||||
* @param options - The {@link UndefinedInitialDataOptions} to use — everything you can pass to `createQuery`,
|
||||
* wrapped in an {@link Accessor} so options can be reactive.
|
||||
* @param queryClient - Use this to use a custom `QueryClient`. Otherwise, the one from the nearest context will
|
||||
* be used.
|
||||
* @returns The current query result. `status` is `pending` if there is no cached data to display, `error` if
|
||||
* the last fetch attempt failed, or `success` if the query has data to display. `isPending`/`isSuccess`/`isError`
|
||||
* are derived booleans for convenience.
|
||||
*
|
||||
* @example
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { createQuery } from '@tanstack/svelte-query'
|
||||
*
|
||||
* const query = createQuery(() => ({
|
||||
* queryKey: ['posts'],
|
||||
* queryFn: fetchPosts,
|
||||
* }))
|
||||
* </script>
|
||||
*
|
||||
* {#if query.status === 'pending'}
|
||||
* Loading...
|
||||
* {:else if query.status === 'error'}
|
||||
* <span>Error: {query.error.message}</span>
|
||||
* {:else}
|
||||
* <ul>
|
||||
* {#each query.data as post (post.id)}
|
||||
* <li>{post.title}</li>
|
||||
* {/each}
|
||||
* </ul>
|
||||
* {/if}
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* The same query, checking `isPending`/`isError` instead of `status` — pick whichever reads better to you:
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { createQuery } from '@tanstack/svelte-query'
|
||||
*
|
||||
* const query = createQuery(() => ({
|
||||
* queryKey: ['posts'],
|
||||
* queryFn: fetchPosts,
|
||||
* }))
|
||||
* </script>
|
||||
*
|
||||
* {#if query.isPending}
|
||||
* Loading...
|
||||
* {:else if query.isError}
|
||||
* <span>Error: {query.error.message}</span>
|
||||
* {:else}
|
||||
* <ul>
|
||||
* {#each query.data as post (post.id)}
|
||||
* <li>{post.title}</li>
|
||||
* {/each}
|
||||
* </ul>
|
||||
* {/if}
|
||||
* ```
|
||||
*/
|
||||
export function createQuery<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
>(
|
||||
options: Accessor<
|
||||
UndefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>
|
||||
>,
|
||||
queryClient?: Accessor<QueryClient>,
|
||||
): CreateQueryResult<TData, TError>
|
||||
|
||||
/**
|
||||
* This overload is selected when `initialData` is set, so the resulting `data` is never `undefined`.
|
||||
*
|
||||
* @see {@link queryOptions} to share these options between `createQuery` and imperative APIs like `queryClient.query`.
|
||||
* @param options - The {@link DefinedInitialDataOptions} to use — everything you can pass to `createQuery`,
|
||||
* with `initialData` set, wrapped in an {@link Accessor} so options can be reactive.
|
||||
* @param queryClient - Use this to use a custom `QueryClient`. Otherwise, the one from the nearest context will
|
||||
* be used.
|
||||
* @returns The current query result, typed so that `status` is `success` — or `error` if a fetch attempt
|
||||
* fails while keeping the existing data (`status` never resolves to `pending` in this overload's type,
|
||||
* since `initialData` guarantees data upfront). `isSuccess`/`isError` are derived booleans for convenience.
|
||||
*
|
||||
* @example
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { createQuery } from '@tanstack/svelte-query'
|
||||
*
|
||||
* // `data` is `Post[]`, never `undefined`, thanks to `initialData` — even if a refetch fails,
|
||||
* // so the list stays visible alongside the error.
|
||||
* const query = createQuery(() => ({
|
||||
* queryKey: ['posts'],
|
||||
* queryFn: fetchPosts,
|
||||
* initialData: [],
|
||||
* }))
|
||||
* </script>
|
||||
*
|
||||
* {#if query.isError}
|
||||
* <span>Error: {query.error.message}</span>
|
||||
* {/if}
|
||||
* <ul>
|
||||
* {#each query.data as post (post.id)}
|
||||
* <li>{post.title}</li>
|
||||
* {/each}
|
||||
* </ul>
|
||||
* ```
|
||||
*/
|
||||
export function createQuery<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
>(
|
||||
options: Accessor<
|
||||
DefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>
|
||||
>,
|
||||
queryClient?: Accessor<QueryClient>,
|
||||
): DefinedCreateQueryResult<TData, TError>
|
||||
|
||||
/**
|
||||
* @see {@link queryOptions} to share these options between `createQuery` and imperative APIs like `queryClient.query`.
|
||||
* @param options - The {@link CreateQueryOptions} to use — everything you can pass to `createQuery`, wrapped
|
||||
* in an {@link Accessor} so options can be reactive.
|
||||
* @param queryClient - Use this to use a custom `QueryClient`. Otherwise, the one from the nearest context will
|
||||
* be used.
|
||||
* @returns The current query result. `status` is `pending` if there is no cached data to display, `error` if
|
||||
* the last fetch attempt failed, or `success` if the query has data to display. `isPending`/`isSuccess`/`isError`
|
||||
* are derived booleans for convenience.
|
||||
*
|
||||
* @example
|
||||
* `select` derives whatever `data` a component needs from the cached value, without changing what's
|
||||
* actually stored in the cache — the cache still holds the full `Post[]`, but `data` here is a `number`:
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { createQuery } from '@tanstack/svelte-query'
|
||||
*
|
||||
* const query = createQuery(() => ({
|
||||
* queryKey: ['posts'],
|
||||
* queryFn: fetchPosts,
|
||||
* select: (posts) => posts.length,
|
||||
* }))
|
||||
* </script>
|
||||
*
|
||||
* {#if query.isPending}
|
||||
* Loading...
|
||||
* {:else if query.isError}
|
||||
* <span>Error: {query.error.message}</span>
|
||||
* {:else}
|
||||
* <span>{query.data} posts</span>
|
||||
* {/if}
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* A dependent query, only enabled once `postId` is set — use `isLoading`, not `isPending`, so the
|
||||
* loading state doesn't show while the query is disabled:
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { createQuery } from '@tanstack/svelte-query'
|
||||
*
|
||||
* let { postId }: { postId: number | undefined } = $props()
|
||||
*
|
||||
* const query = createQuery(() => ({
|
||||
* queryKey: ['post', postId],
|
||||
* queryFn: () => fetchPost(postId!),
|
||||
* enabled: postId != null,
|
||||
* }))
|
||||
* </script>
|
||||
*
|
||||
* {#if postId == null}
|
||||
* Select a post
|
||||
* {:else if query.isLoading}
|
||||
* Loading...
|
||||
* {:else if query.isError}
|
||||
* <span>Error: {query.error.message}</span>
|
||||
* {:else}
|
||||
* <h1>{query.data?.title}</h1>
|
||||
* {/if}
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* Seeding a detail query from an already-cached list, to skip the loading state:
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { createQuery, useQueryClient } from '@tanstack/svelte-query'
|
||||
*
|
||||
* let { postId }: { postId: number } = $props()
|
||||
*
|
||||
* const queryClient = useQueryClient()
|
||||
*
|
||||
* const query = createQuery(() => ({
|
||||
* queryKey: ['post', postId],
|
||||
* queryFn: () => fetchPost(postId),
|
||||
* initialData: () =>
|
||||
* queryClient
|
||||
* .getQueryData<Array<Post>>(['posts'])
|
||||
* ?.find((post) => post.id === postId),
|
||||
* }))
|
||||
* </script>
|
||||
*
|
||||
* {#if query.isError}
|
||||
* <span>Error: {query.error.message}</span>
|
||||
* {/if}
|
||||
* <h1>{query.data?.title}</h1>
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* Paginated data, keeping the previous page's data visible while the next page loads:
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { createQuery, keepPreviousData } from '@tanstack/svelte-query'
|
||||
*
|
||||
* let page = $state(0)
|
||||
*
|
||||
* const query = createQuery(() => ({
|
||||
* queryKey: ['posts', page],
|
||||
* queryFn: () => fetchPosts(page),
|
||||
* placeholderData: keepPreviousData,
|
||||
* }))
|
||||
* </script>
|
||||
*
|
||||
* {#if query.isError}
|
||||
* <span>Error: {query.error.message}</span>
|
||||
* {/if}
|
||||
* <ul>
|
||||
* {#each query.data ?? [] as post (post.id)}
|
||||
* <li>{post.title}</li>
|
||||
* {/each}
|
||||
* </ul>
|
||||
* <button disabled={query.isPlaceholderData} onclick={() => page++}>
|
||||
* Next Page
|
||||
* </button>
|
||||
* ```
|
||||
*/
|
||||
export function createQuery<
|
||||
TQueryFnData,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
>(
|
||||
options: Accessor<CreateQueryOptions<TQueryFnData, TError, TData, TQueryKey>>,
|
||||
queryClient?: Accessor<QueryClient>,
|
||||
): CreateQueryResult<TData, TError>
|
||||
|
||||
export function createQuery(
|
||||
options: Accessor<CreateQueryOptions>,
|
||||
queryClient?: Accessor<QueryClient>,
|
||||
) {
|
||||
return createBaseQuery(options, QueryObserver, queryClient)
|
||||
}
|
||||
33
packages/svelte-query/src/index.ts
Normal file
33
packages/svelte-query/src/index.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/* istanbul ignore file */
|
||||
|
||||
// Re-export core
|
||||
export * from '@tanstack/query-core'
|
||||
|
||||
// Svelte Query
|
||||
export * from './types.js'
|
||||
export * from './context.js'
|
||||
|
||||
export { createQuery } from './createQuery.js'
|
||||
export type { QueriesResults, QueriesOptions } from './createQueries.svelte.js'
|
||||
export type {
|
||||
DefinedInitialDataOptions,
|
||||
UndefinedInitialDataOptions,
|
||||
} from './queryOptions.js'
|
||||
export { queryOptions } from './queryOptions.js'
|
||||
export { createQueries } from './createQueries.svelte.js'
|
||||
export { createInfiniteQuery } from './createInfiniteQuery.js'
|
||||
export type {
|
||||
DefinedInitialDataInfiniteOptions,
|
||||
UndefinedInitialDataInfiniteOptions,
|
||||
} from './infiniteQueryOptions.js'
|
||||
export { infiniteQueryOptions } from './infiniteQueryOptions.js'
|
||||
export { mutationOptions } from './mutationOptions.js'
|
||||
export { createMutation } from './createMutation.svelte.js'
|
||||
export { useMutationState } from './useMutationState.svelte.js'
|
||||
export { useQueryClient } from './useQueryClient.js'
|
||||
export { useIsFetching } from './useIsFetching.svelte.js'
|
||||
export { useIsMutating } from './useIsMutating.svelte.js'
|
||||
export { useIsRestoring } from './useIsRestoring.js'
|
||||
export { useHydrate } from './useHydrate.js'
|
||||
export { default as HydrationBoundary } from './HydrationBoundary.svelte'
|
||||
export { default as QueryClientProvider } from './QueryClientProvider.svelte'
|
||||
184
packages/svelte-query/src/infiniteQueryOptions.ts
Normal file
184
packages/svelte-query/src/infiniteQueryOptions.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
import type {
|
||||
DefaultError,
|
||||
InfiniteData,
|
||||
InitialDataFunction,
|
||||
NonUndefinedGuard,
|
||||
QueryKey,
|
||||
QueryKeyWithDataTag,
|
||||
} from '@tanstack/query-core'
|
||||
import type { CreateInfiniteQueryOptions } from './types.js'
|
||||
|
||||
export type UndefinedInitialDataInfiniteOptions<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = InfiniteData<TQueryFnData>,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
> = CreateInfiniteQueryOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
> & {
|
||||
initialData?:
|
||||
| undefined
|
||||
| NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>>
|
||||
| InitialDataFunction<
|
||||
NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>>
|
||||
>
|
||||
}
|
||||
|
||||
export type DefinedInitialDataInfiniteOptions<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = InfiniteData<TQueryFnData>,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
> = CreateInfiniteQueryOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
> & {
|
||||
initialData:
|
||||
| NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>>
|
||||
| (() => NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>>)
|
||||
}
|
||||
|
||||
/**
|
||||
* You can generally pass everything to `infiniteQueryOptions` that you can also pass to `createInfiniteQuery`.
|
||||
* These options can be shared across `createInfiniteQuery` calls and imperative APIs such as
|
||||
* `queryClient.infiniteQuery`. `options.queryKey` is required and is the query key to generate options for.
|
||||
*
|
||||
* This overload is selected when `initialData` is set.
|
||||
*
|
||||
* @see {@link createInfiniteQuery} to run an infinite query with these options.
|
||||
* @param options - The {@link DefinedInitialDataInfiniteOptions} to use — everything you can pass to
|
||||
* `createInfiniteQuery`, with `initialData` set.
|
||||
* @returns The same options object, typed so that `queryKey` carries the inferred data type.
|
||||
*
|
||||
* @example
|
||||
* `initialData` skips the loading state on first render — even if a refetch fails, the list stays
|
||||
* visible alongside the error:
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { infiniteQueryOptions, createInfiniteQuery } from '@tanstack/svelte-query'
|
||||
*
|
||||
* const projectsOptions = infiniteQueryOptions({
|
||||
* queryKey: ['projects'],
|
||||
* queryFn: ({ pageParam }) => fetchProjects(pageParam),
|
||||
* initialPageParam: 0,
|
||||
* getNextPageParam: (lastPage) => lastPage.nextId,
|
||||
* initialData: { pages: [], pageParams: [] },
|
||||
* })
|
||||
*
|
||||
* const query = createInfiniteQuery(() => projectsOptions)
|
||||
* </script>
|
||||
*
|
||||
* {#if query.isError}
|
||||
* <span>Error: {query.error.message}</span>
|
||||
* {/if}
|
||||
* <ul>
|
||||
* {#each query.data.pages as page}
|
||||
* {#each page.projects as project (project.id)}
|
||||
* <li>{project.name}</li>
|
||||
* {/each}
|
||||
* {/each}
|
||||
* </ul>
|
||||
* ```
|
||||
*/
|
||||
export function infiniteQueryOptions<
|
||||
TQueryFnData,
|
||||
TError = DefaultError,
|
||||
TData = InfiniteData<TQueryFnData>,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
>(
|
||||
options: DefinedInitialDataInfiniteOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>,
|
||||
): DefinedInitialDataInfiniteOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
> &
|
||||
QueryKeyWithDataTag<TQueryKey, InfiniteData<TQueryFnData>, TError>
|
||||
|
||||
/**
|
||||
* You can generally pass everything to `infiniteQueryOptions` that you can also pass to `createInfiniteQuery`.
|
||||
* These options can be shared across `createInfiniteQuery` calls and imperative APIs such as
|
||||
* `queryClient.infiniteQuery`. `options.queryKey` is required and is the query key to generate options for.
|
||||
*
|
||||
* @see {@link createInfiniteQuery} to run an infinite query with these options.
|
||||
* @param options - The {@link UndefinedInitialDataInfiniteOptions} to use — everything you can pass to
|
||||
* `createInfiniteQuery`.
|
||||
* @returns The same options object, typed so that `queryKey` carries the inferred data type.
|
||||
*
|
||||
* @example
|
||||
* A parameterized factory, so the same options object can be reused per `postId`:
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { infiniteQueryOptions, createInfiniteQuery } from '@tanstack/svelte-query'
|
||||
*
|
||||
* let { postId }: { postId: string } = $props()
|
||||
*
|
||||
* const commentsOptions = (postId: string) =>
|
||||
* infiniteQueryOptions({
|
||||
* queryKey: ['post', postId, 'comments'],
|
||||
* queryFn: ({ pageParam }) => fetchComments(postId, pageParam),
|
||||
* initialPageParam: 0,
|
||||
* getNextPageParam: (lastPage) => lastPage.nextId,
|
||||
* })
|
||||
*
|
||||
* const query = createInfiniteQuery(() => commentsOptions(postId))
|
||||
* </script>
|
||||
*
|
||||
* {#if query.isPending}
|
||||
* Loading...
|
||||
* {:else if query.isError}
|
||||
* <span>Error: {query.error.message}</span>
|
||||
* {:else}
|
||||
* <ul>
|
||||
* {#each query.data.pages as page}
|
||||
* {#each page.comments as comment (comment.id)}
|
||||
* <li>{comment.text}</li>
|
||||
* {/each}
|
||||
* {/each}
|
||||
* </ul>
|
||||
* {/if}
|
||||
* ```
|
||||
*/
|
||||
export function infiniteQueryOptions<
|
||||
TQueryFnData,
|
||||
TError = DefaultError,
|
||||
TData = InfiniteData<TQueryFnData>,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
>(
|
||||
options: UndefinedInitialDataInfiniteOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>,
|
||||
): UndefinedInitialDataInfiniteOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
> &
|
||||
QueryKeyWithDataTag<TQueryKey, InfiniteData<TQueryFnData>, TError>
|
||||
|
||||
export function infiniteQueryOptions(options: unknown) {
|
||||
return options
|
||||
}
|
||||
94
packages/svelte-query/src/mutationOptions.ts
Normal file
94
packages/svelte-query/src/mutationOptions.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import type { DefaultError, WithRequired } from '@tanstack/query-core'
|
||||
import type { CreateMutationOptions } from './types.js'
|
||||
|
||||
/**
|
||||
* You can generally pass everything to `mutationOptions` that you can also pass to `createMutation`. This
|
||||
* overload requires `mutationKey`, so the resulting options can be looked up elsewhere (e.g. with
|
||||
* `useMutationState`).
|
||||
*
|
||||
* @see {@link createMutation} to run a mutation with these options.
|
||||
* @param options - The options to use — everything you can pass to `createMutation`, with `mutationKey` set.
|
||||
* @returns The same options object.
|
||||
*
|
||||
* @example
|
||||
* Looking the mutation up elsewhere via its `mutationKey`, e.g. for a global "saving…" indicator:
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { mutationOptions, useMutationState } from '@tanstack/svelte-query'
|
||||
*
|
||||
* const createPostOptions = mutationOptions({
|
||||
* mutationKey: ['posts', 'create'],
|
||||
* mutationFn: createPost,
|
||||
* })
|
||||
*
|
||||
* const pending = useMutationState({
|
||||
* filters: { mutationKey: createPostOptions.mutationKey, status: 'pending' },
|
||||
* })
|
||||
* </script>
|
||||
*
|
||||
* {#if pending.length > 0}
|
||||
* <span>Saving…</span>
|
||||
* {/if}
|
||||
* ```
|
||||
*/
|
||||
export function mutationOptions<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
TVariables = void,
|
||||
TOnMutateResult = unknown,
|
||||
>(
|
||||
options: WithRequired<
|
||||
CreateMutationOptions<TData, TError, TVariables, TOnMutateResult>,
|
||||
'mutationKey'
|
||||
>,
|
||||
): WithRequired<
|
||||
CreateMutationOptions<TData, TError, TVariables, TOnMutateResult>,
|
||||
'mutationKey'
|
||||
>
|
||||
|
||||
/**
|
||||
* You can generally pass everything to `mutationOptions` that you can also pass to `createMutation`.
|
||||
*
|
||||
* @see {@link createMutation} to run a mutation with these options.
|
||||
* @param options - The options to use — everything you can pass to `createMutation`.
|
||||
* @returns The same options object.
|
||||
*
|
||||
* @example
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { mutationOptions, createMutation } from '@tanstack/svelte-query'
|
||||
*
|
||||
* const createPostOptions = mutationOptions({
|
||||
* mutationFn: createPost,
|
||||
* })
|
||||
*
|
||||
* const mutation = createMutation(() => createPostOptions)
|
||||
* </script>
|
||||
*
|
||||
* <button onclick={() => mutation.mutate({ title: 'Hello' })}>Create</button>
|
||||
* ```
|
||||
*/
|
||||
export function mutationOptions<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
TVariables = void,
|
||||
TOnMutateResult = unknown,
|
||||
>(
|
||||
options: Omit<
|
||||
CreateMutationOptions<TData, TError, TVariables, TOnMutateResult>,
|
||||
'mutationKey'
|
||||
>,
|
||||
): Omit<
|
||||
CreateMutationOptions<TData, TError, TVariables, TOnMutateResult>,
|
||||
'mutationKey'
|
||||
>
|
||||
export function mutationOptions<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
TVariables = void,
|
||||
TOnMutateResult = unknown,
|
||||
>(
|
||||
options: CreateMutationOptions<TData, TError, TVariables, TOnMutateResult>,
|
||||
): CreateMutationOptions<TData, TError, TVariables, TOnMutateResult> {
|
||||
return options
|
||||
}
|
||||
125
packages/svelte-query/src/queryOptions.ts
Normal file
125
packages/svelte-query/src/queryOptions.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import type {
|
||||
DefaultError,
|
||||
InitialDataFunction,
|
||||
NonUndefinedGuard,
|
||||
QueryKey,
|
||||
QueryKeyWithDataTag,
|
||||
} from '@tanstack/query-core'
|
||||
import type { CreateQueryOptions } from './types.js'
|
||||
|
||||
export type UndefinedInitialDataOptions<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
> = CreateQueryOptions<TQueryFnData, TError, TData, TQueryKey> & {
|
||||
initialData?: undefined | InitialDataFunction<NonUndefinedGuard<TQueryFnData>>
|
||||
}
|
||||
|
||||
export type DefinedInitialDataOptions<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
> = CreateQueryOptions<TQueryFnData, TError, TData, TQueryKey> & {
|
||||
initialData:
|
||||
| NonUndefinedGuard<TQueryFnData>
|
||||
| (() => NonUndefinedGuard<TQueryFnData>)
|
||||
}
|
||||
|
||||
/**
|
||||
* You can generally pass everything to `queryOptions` that you can also pass to `createQuery`. These options
|
||||
* can be shared across `createQuery` calls and imperative APIs such as `queryClient.query`. `options.queryKey`
|
||||
* is required and is the query key to generate options for.
|
||||
*
|
||||
* This overload is selected when `initialData` is set, so the resulting `data` is never `undefined`.
|
||||
*
|
||||
* @see {@link createQuery} to run a query with these options.
|
||||
* @param options - The {@link DefinedInitialDataOptions} to use — everything you can pass to `createQuery`,
|
||||
* with `initialData` set.
|
||||
* @returns The same options object, typed so that `queryKey` carries the inferred data type.
|
||||
*
|
||||
* @example
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { queryOptions, createQuery } from '@tanstack/svelte-query'
|
||||
*
|
||||
* const postsOptions = queryOptions({
|
||||
* queryKey: ['posts'],
|
||||
* queryFn: fetchPosts,
|
||||
* initialData: [],
|
||||
* })
|
||||
*
|
||||
* // `data` is `Post[]`, never `undefined`, thanks to `initialData` — even if a refetch fails,
|
||||
* // so the list stays visible alongside the error.
|
||||
* const query = createQuery(() => postsOptions)
|
||||
* </script>
|
||||
*
|
||||
* {#if query.isError}
|
||||
* <span>Error: {query.error.message}</span>
|
||||
* {/if}
|
||||
* <ul>
|
||||
* {#each query.data as post (post.id)}
|
||||
* <li>{post.title}</li>
|
||||
* {/each}
|
||||
* </ul>
|
||||
* ```
|
||||
*/
|
||||
export function queryOptions<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
>(
|
||||
options: DefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>,
|
||||
): DefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey> &
|
||||
QueryKeyWithDataTag<TQueryKey, TQueryFnData, TError>
|
||||
|
||||
/**
|
||||
* You can generally pass everything to `queryOptions` that you can also pass to `createQuery`. These options
|
||||
* can be shared across `createQuery` calls and imperative APIs such as `queryClient.query`. `options.queryKey`
|
||||
* is required and is the query key to generate options for.
|
||||
*
|
||||
* @see {@link createQuery} to run a query with these options.
|
||||
* @param options - The {@link UndefinedInitialDataOptions} to use — everything you can pass to `createQuery`.
|
||||
* @returns The same options object, typed so that `queryKey` carries the inferred data type.
|
||||
*
|
||||
* @example
|
||||
* A parameterized factory, so the same options object can be reused per `id`:
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { queryOptions, createQuery } from '@tanstack/svelte-query'
|
||||
*
|
||||
* let { id }: { id: string } = $props()
|
||||
*
|
||||
* const postOptions = (id: string) =>
|
||||
* queryOptions({
|
||||
* queryKey: ['post', id],
|
||||
* queryFn: () => fetchPost(id),
|
||||
* })
|
||||
*
|
||||
* const query = createQuery(() => postOptions(id))
|
||||
* </script>
|
||||
*
|
||||
* {#if query.isPending}
|
||||
* Loading...
|
||||
* {:else if query.isError}
|
||||
* <span>Error: {query.error.message}</span>
|
||||
* {:else}
|
||||
* <h1>{query.data.title}</h1>
|
||||
* {/if}
|
||||
* ```
|
||||
*/
|
||||
export function queryOptions<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
>(
|
||||
options: UndefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>,
|
||||
): UndefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey> &
|
||||
QueryKeyWithDataTag<TQueryKey, TQueryFnData, TError>
|
||||
|
||||
export function queryOptions(options: unknown) {
|
||||
return options
|
||||
}
|
||||
170
packages/svelte-query/src/types.ts
Normal file
170
packages/svelte-query/src/types.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import type { Snippet } from 'svelte'
|
||||
import type {
|
||||
DefaultError,
|
||||
DefinedInfiniteQueryObserverResult,
|
||||
DefinedQueryObserverResult,
|
||||
InfiniteQueryObserverOptions,
|
||||
InfiniteQueryObserverResult,
|
||||
MutateFunction,
|
||||
Mutation,
|
||||
MutationFilters,
|
||||
MutationObserverOptions,
|
||||
MutationObserverResult,
|
||||
MutationState,
|
||||
OmitKeyof,
|
||||
Override,
|
||||
QueryClient,
|
||||
QueryKey,
|
||||
QueryObserverOptions,
|
||||
QueryObserverResult,
|
||||
} from '@tanstack/query-core'
|
||||
|
||||
export type Accessor<T> = () => T
|
||||
|
||||
/** Options for createBaseQuery */
|
||||
export type CreateBaseQueryOptions<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
> = QueryObserverOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>
|
||||
|
||||
/** Result from createBaseQuery */
|
||||
export type CreateBaseQueryResult<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
> = QueryObserverResult<TData, TError>
|
||||
|
||||
/** Options for createQuery */
|
||||
export type CreateQueryOptions<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
> = CreateBaseQueryOptions<TQueryFnData, TError, TData, TQueryFnData, TQueryKey>
|
||||
|
||||
/** Result from createQuery */
|
||||
export type CreateQueryResult<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
> = CreateBaseQueryResult<TData, TError>
|
||||
|
||||
/** Options for createInfiniteQuery */
|
||||
export type CreateInfiniteQueryOptions<
|
||||
TQueryFnData = unknown,
|
||||
TError = DefaultError,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
TPageParam = unknown,
|
||||
> = InfiniteQueryObserverOptions<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey,
|
||||
TPageParam
|
||||
>
|
||||
|
||||
/** Result from createInfiniteQuery */
|
||||
export type CreateInfiniteQueryResult<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
> = InfiniteQueryObserverResult<TData, TError>
|
||||
|
||||
/** Result from createInfiniteQuery with initialData */
|
||||
export type DefinedCreateInfiniteQueryResult<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
> = DefinedInfiniteQueryObserverResult<TData, TError>
|
||||
|
||||
/** Options for createBaseQuery with initialData */
|
||||
export type DefinedCreateBaseQueryResult<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
> = DefinedQueryObserverResult<TData, TError>
|
||||
|
||||
/** Options for createQuery with initialData */
|
||||
export type DefinedCreateQueryResult<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
> = DefinedCreateBaseQueryResult<TData, TError>
|
||||
|
||||
/** Options for createMutation */
|
||||
export type CreateMutationOptions<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
TVariables = void,
|
||||
TOnMutateResult = unknown,
|
||||
> = OmitKeyof<
|
||||
MutationObserverOptions<TData, TError, TVariables, TOnMutateResult>,
|
||||
'_defaulted'
|
||||
>
|
||||
|
||||
export type CreateMutateFunction<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
TVariables = void,
|
||||
TOnMutateResult = unknown,
|
||||
> = (
|
||||
...args: Parameters<
|
||||
MutateFunction<TData, TError, TVariables, TOnMutateResult>
|
||||
>
|
||||
) => void
|
||||
|
||||
export type CreateMutateAsyncFunction<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
TVariables = void,
|
||||
TOnMutateResult = unknown,
|
||||
> = MutateFunction<TData, TError, TVariables, TOnMutateResult>
|
||||
|
||||
export type CreateBaseMutationResult<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
TVariables = unknown,
|
||||
TOnMutateResult = unknown,
|
||||
> = Override<
|
||||
MutationObserverResult<TData, TError, TVariables, TOnMutateResult>,
|
||||
{ mutate: CreateMutateFunction<TData, TError, TVariables, TOnMutateResult> }
|
||||
> & {
|
||||
mutateAsync: CreateMutateAsyncFunction<
|
||||
TData,
|
||||
TError,
|
||||
TVariables,
|
||||
TOnMutateResult
|
||||
>
|
||||
}
|
||||
|
||||
/** Result from createMutation */
|
||||
export type CreateMutationResult<
|
||||
TData = unknown,
|
||||
TError = DefaultError,
|
||||
TVariables = unknown,
|
||||
TOnMutateResult = unknown,
|
||||
> = CreateBaseMutationResult<TData, TError, TVariables, TOnMutateResult>
|
||||
|
||||
export type MutationTypeFromResult<TResult> = [TResult] extends [
|
||||
MutationState<
|
||||
infer TData,
|
||||
infer TError,
|
||||
infer TVariables,
|
||||
infer TOnMutateResult
|
||||
>,
|
||||
]
|
||||
? Mutation<TData, TError, TVariables, TOnMutateResult>
|
||||
: Mutation
|
||||
|
||||
/** Options for useMutationState */
|
||||
export type MutationStateOptions<
|
||||
TResult = MutationState,
|
||||
TMutation extends Mutation<any, any, any, any> =
|
||||
MutationTypeFromResult<TResult>,
|
||||
> = {
|
||||
filters?: MutationFilters
|
||||
select?: (mutation: TMutation) => TResult
|
||||
}
|
||||
|
||||
export type QueryClientProviderProps = {
|
||||
client: QueryClient
|
||||
children: Snippet
|
||||
}
|
||||
43
packages/svelte-query/src/useHydrate.ts
Normal file
43
packages/svelte-query/src/useHydrate.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { hydrate } from '@tanstack/query-core'
|
||||
import { useQueryClient } from './useQueryClient.js'
|
||||
import type { HydrateOptions, QueryClient } from '@tanstack/query-core'
|
||||
|
||||
/**
|
||||
* Adds a previously dehydrated `state` into the `queryClient` (from the nearest context, or the one
|
||||
* passed explicitly). If the client already contains data, the new queries will be intelligently merged based
|
||||
* on update timestamp. `HydrationBoundary` wraps this — use it directly only if you need to hydrate from your
|
||||
* own component instead.
|
||||
*
|
||||
* @param state - The dehydrated state to hydrate into the cache, as produced by `dehydrate`.
|
||||
* @param options - {@link HydrateOptions} to control the hydration.
|
||||
* @param queryClient - Use this to use a custom `QueryClient`. Otherwise, the one from the nearest context will
|
||||
* be used.
|
||||
*
|
||||
* @example
|
||||
* Server-side prefetch handed off to the client via `dehydrate` — `dehydratedState` would typically come
|
||||
* from a server load function that prefetched with `queryClient.query` and called `dehydrate(queryClient)`:
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { HydrationBoundary } from '@tanstack/svelte-query'
|
||||
* import type { DehydratedState } from '@tanstack/svelte-query'
|
||||
* import Posts from './Posts.svelte'
|
||||
*
|
||||
* let { dehydratedState }: { dehydratedState: DehydratedState } = $props()
|
||||
* </script>
|
||||
*
|
||||
* <HydrationBoundary state={dehydratedState}>
|
||||
* <Posts />
|
||||
* </HydrationBoundary>
|
||||
* ```
|
||||
*/
|
||||
export function useHydrate(
|
||||
state?: unknown,
|
||||
options?: HydrateOptions,
|
||||
queryClient?: QueryClient,
|
||||
) {
|
||||
const client = useQueryClient(queryClient)
|
||||
|
||||
if (state) {
|
||||
hydrate(client, state, options)
|
||||
}
|
||||
}
|
||||
51
packages/svelte-query/src/useIsFetching.svelte.ts
Normal file
51
packages/svelte-query/src/useIsFetching.svelte.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { ReactiveValue } from './containers.svelte.js'
|
||||
import { useQueryClient } from './useQueryClient.js'
|
||||
import type { QueryClient, QueryFilters } from '@tanstack/query-core'
|
||||
|
||||
/**
|
||||
* @param filters - {@link QueryFilters} to narrow down which queries to count. Omit to count every fetching
|
||||
* query.
|
||||
* @param queryClient - Use this to use a custom `QueryClient`. Otherwise, the one from the nearest context will
|
||||
* be used.
|
||||
* @returns A reactive value — read `.current` to get how many matching queries are currently fetching.
|
||||
*
|
||||
* @example
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { useIsFetching } from '@tanstack/svelte-query'
|
||||
*
|
||||
* // How many queries matching the posts prefix are fetching?
|
||||
* const isFetchingPosts = useIsFetching({ queryKey: ['posts'] })
|
||||
* </script>
|
||||
*
|
||||
* {#if isFetchingPosts.current}
|
||||
* <span>Refreshing posts...</span>
|
||||
* {/if}
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* A global loading indicator for any query fetching in the background, not just the ones on screen:
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { useIsFetching } from '@tanstack/svelte-query'
|
||||
*
|
||||
* const isFetching = useIsFetching()
|
||||
* </script>
|
||||
*
|
||||
* {#if isFetching.current}
|
||||
* <div>Queries are fetching in the background...</div>
|
||||
* {/if}
|
||||
* ```
|
||||
*/
|
||||
export function useIsFetching(
|
||||
filters?: QueryFilters,
|
||||
queryClient?: QueryClient,
|
||||
): ReactiveValue<number> {
|
||||
const client = useQueryClient(queryClient)
|
||||
const queryCache = client.getQueryCache()
|
||||
|
||||
return new ReactiveValue(
|
||||
() => client.isFetching(filters),
|
||||
(update) => queryCache.subscribe(update),
|
||||
)
|
||||
}
|
||||
39
packages/svelte-query/src/useIsMutating.svelte.ts
Normal file
39
packages/svelte-query/src/useIsMutating.svelte.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { useQueryClient } from './useQueryClient.js'
|
||||
import { ReactiveValue } from './containers.svelte.js'
|
||||
import type { MutationFilters, QueryClient } from '@tanstack/query-core'
|
||||
|
||||
/**
|
||||
* `useIsMutating` is an optional hook that returns the `number` of mutations that your application is
|
||||
* running (useful for app-wide loading indicators).
|
||||
*
|
||||
* @param filters - {@link MutationFilters} to narrow down which mutations to count.
|
||||
* @param queryClient - Use this to use a custom `QueryClient`. Otherwise, the one from the nearest context will
|
||||
* be used.
|
||||
* @returns A reactive value — read `.current` to get how many matching mutations are currently running.
|
||||
*
|
||||
* @example
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { useIsMutating } from '@tanstack/svelte-query'
|
||||
*
|
||||
* // How many mutations matching the posts prefix are in progress?
|
||||
* const isMutatingPosts = useIsMutating({ mutationKey: ['posts'] })
|
||||
* </script>
|
||||
*
|
||||
* {#if isMutatingPosts.current}
|
||||
* <span>Saving posts...</span>
|
||||
* {/if}
|
||||
* ```
|
||||
*/
|
||||
export function useIsMutating(
|
||||
filters?: MutationFilters,
|
||||
queryClient?: QueryClient,
|
||||
): ReactiveValue<number> {
|
||||
const client = useQueryClient(queryClient)
|
||||
const cache = client.getMutationCache()
|
||||
|
||||
return new ReactiveValue(
|
||||
() => client.isMutating(filters),
|
||||
(update) => cache.subscribe(update),
|
||||
)
|
||||
}
|
||||
6
packages/svelte-query/src/useIsRestoring.ts
Normal file
6
packages/svelte-query/src/useIsRestoring.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { getIsRestoringContext } from './context.js'
|
||||
import type { Box } from './containers.svelte.js'
|
||||
|
||||
export function useIsRestoring(): Box<boolean> {
|
||||
return getIsRestoringContext()
|
||||
}
|
||||
137
packages/svelte-query/src/useMutationState.svelte.ts
Normal file
137
packages/svelte-query/src/useMutationState.svelte.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import { replaceEqualDeep } from '@tanstack/query-core'
|
||||
import { useQueryClient } from './useQueryClient.js'
|
||||
import type {
|
||||
Mutation,
|
||||
MutationCache,
|
||||
MutationState,
|
||||
QueryClient,
|
||||
} from '@tanstack/query-core'
|
||||
import type { MutationStateOptions, MutationTypeFromResult } from './types.js'
|
||||
|
||||
function getResult<
|
||||
TResult = MutationState,
|
||||
TMutation extends Mutation<any, any, any, any> =
|
||||
MutationTypeFromResult<TResult>,
|
||||
>(
|
||||
mutationCache: MutationCache,
|
||||
options: MutationStateOptions<TResult, TMutation>,
|
||||
): Array<TResult> {
|
||||
return mutationCache
|
||||
.findAll(options.filters)
|
||||
.map(
|
||||
(mutation): TResult =>
|
||||
(options.select
|
||||
? options.select(mutation as TMutation)
|
||||
: mutation.state) as TResult,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* `useMutationState` gives you access to all mutations (matching the given `filters`), including ones that
|
||||
* were created by a different component or hook instance, or even ones no longer mounted.
|
||||
*
|
||||
* @param options - The `filters` to narrow down matched mutations, and an optional `select` to transform the
|
||||
* mutation state.
|
||||
* @param queryClient - Use this to use a custom `QueryClient`. Otherwise, the one from the nearest context will
|
||||
* be used.
|
||||
* @returns An Array of whatever `select` returns for each matching mutation.
|
||||
*
|
||||
* @example
|
||||
* Get all variables of all running mutations:
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { useMutationState } from '@tanstack/svelte-query'
|
||||
*
|
||||
* const pendingVariables = useMutationState({
|
||||
* filters: { status: 'pending' },
|
||||
* select: (mutation) => mutation.state.variables,
|
||||
* })
|
||||
* </script>
|
||||
*
|
||||
* {pendingVariables.length} posts saving...
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* Get all data for specific mutations via the `mutationKey`:
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { createMutation, useMutationState } from '@tanstack/svelte-query'
|
||||
*
|
||||
* const mutationKey = ['posts']
|
||||
*
|
||||
* // Some mutation that we want to get the state for
|
||||
* const mutation = createMutation(() => ({
|
||||
* mutationKey,
|
||||
* mutationFn: createPosts,
|
||||
* }))
|
||||
*
|
||||
* const savedPosts = useMutationState({
|
||||
* // this mutation key needs to match the mutation key of the given mutation (see above)
|
||||
* filters: { mutationKey, status: 'success' },
|
||||
* select: (mutation) => mutation.state.data,
|
||||
* })
|
||||
* </script>
|
||||
*
|
||||
* <button onclick={() => mutation.mutate(['New Post'])}>
|
||||
* Create post ({savedPosts.length} saved so far)
|
||||
* </button>
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* Access the latest mutation data via the `mutationKey`. Each invocation of `mutate` adds a new entry to the
|
||||
* mutation cache for `gcTime` milliseconds — check the last item that `useMutationState` returns to get the
|
||||
* latest successful mutation (the `status: 'success'` filter above excludes pending/errored ones):
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { useMutationState } from '@tanstack/svelte-query'
|
||||
*
|
||||
* const savedPosts = useMutationState({
|
||||
* filters: { mutationKey: ['posts'], status: 'success' },
|
||||
* select: (mutation) => mutation.state.data,
|
||||
* })
|
||||
*
|
||||
* const latestSavedPost = $derived(savedPosts[savedPosts.length - 1])
|
||||
* </script>
|
||||
*
|
||||
* {latestSavedPost ? 'Saved' : 'Nothing saved yet'}
|
||||
* ```
|
||||
*/
|
||||
export function useMutationState<
|
||||
TResult = MutationState,
|
||||
TMutation extends Mutation<any, any, any, any> =
|
||||
MutationTypeFromResult<TResult>,
|
||||
>(
|
||||
options: MutationStateOptions<TResult, TMutation> = {},
|
||||
queryClient?: QueryClient,
|
||||
): Array<TResult> {
|
||||
const mutationCache = useQueryClient(queryClient).getMutationCache()
|
||||
const result = $state(getResult(mutationCache, options))
|
||||
|
||||
$effect(() => {
|
||||
const unsubscribe = mutationCache.subscribe(() => {
|
||||
const nextResult = replaceEqualDeep(
|
||||
result,
|
||||
getResult(mutationCache, options),
|
||||
)
|
||||
if (result !== nextResult) {
|
||||
result.splice(0, result.length, ...nextResult)
|
||||
}
|
||||
})
|
||||
|
||||
return unsubscribe
|
||||
})
|
||||
|
||||
/* $effect(() => {
|
||||
mutationCache.subscribe(() => {
|
||||
const nextResult = replaceEqualDeep(
|
||||
result.current,
|
||||
getResult(mutationCache, optionsRef),
|
||||
)
|
||||
if (result.current !== nextResult) {
|
||||
result = nextResult
|
||||
//notifyManager.schedule(onStoreChange)
|
||||
}
|
||||
})
|
||||
}) */
|
||||
return result
|
||||
}
|
||||
7
packages/svelte-query/src/useQueryClient.ts
Normal file
7
packages/svelte-query/src/useQueryClient.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { getQueryClientContext } from './context.js'
|
||||
import type { QueryClient } from '@tanstack/query-core'
|
||||
|
||||
export function useQueryClient(queryClient?: QueryClient): QueryClient {
|
||||
if (queryClient) return queryClient
|
||||
return getQueryClientContext()
|
||||
}
|
||||
44
packages/svelte-query/src/utils.svelte.ts
Normal file
44
packages/svelte-query/src/utils.svelte.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { untrack } from 'svelte'
|
||||
// modified from the great https://github.com/svecosystem/runed
|
||||
function runEffect(
|
||||
flush: 'post' | 'pre',
|
||||
effect: () => void | VoidFunction,
|
||||
): void {
|
||||
switch (flush) {
|
||||
case 'post':
|
||||
$effect(effect)
|
||||
break
|
||||
case 'pre':
|
||||
$effect.pre(effect)
|
||||
break
|
||||
}
|
||||
}
|
||||
type Getter<T> = () => T
|
||||
export const watchChanges = <T>(
|
||||
sources: Getter<T> | Array<Getter<T>>,
|
||||
flush: 'post' | 'pre',
|
||||
effect: (
|
||||
values: T | Array<T>,
|
||||
previousValues: T | undefined | Array<T | undefined>,
|
||||
) => void,
|
||||
) => {
|
||||
let active = false
|
||||
let previousValues: T | undefined | Array<T | undefined> = Array.isArray(
|
||||
sources,
|
||||
)
|
||||
? []
|
||||
: undefined
|
||||
runEffect(flush, () => {
|
||||
const values = Array.isArray(sources)
|
||||
? sources.map((source) => source())
|
||||
: sources()
|
||||
if (!active) {
|
||||
active = true
|
||||
previousValues = values
|
||||
return
|
||||
}
|
||||
const cleanup = untrack(() => effect(values, previousValues))
|
||||
previousValues = values
|
||||
return cleanup
|
||||
})
|
||||
}
|
||||
10
packages/svelte-query/svelte.config.js
Normal file
10
packages/svelte-query/svelte.config.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
|
||||
|
||||
const config = {
|
||||
preprocess: vitePreprocess(),
|
||||
compilerOptions: {
|
||||
runes: true,
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
32
packages/svelte-query/tests/HydrationBoundary/Base.svelte
Normal file
32
packages/svelte-query/tests/HydrationBoundary/Base.svelte
Normal file
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import type { QueryClient } from '@tanstack/query-core'
|
||||
import {
|
||||
HydrationBoundary,
|
||||
createQuery,
|
||||
setQueryClientContext,
|
||||
} from '../../src/index.js'
|
||||
import type { DehydratedState } from '@tanstack/query-core'
|
||||
|
||||
type Props = {
|
||||
queryClient: QueryClient
|
||||
dehydratedState: DehydratedState
|
||||
queryFn: () => Promise<string>
|
||||
}
|
||||
|
||||
let { queryClient, dehydratedState, queryFn }: Props = $props()
|
||||
|
||||
setQueryClientContext(queryClient)
|
||||
|
||||
const query = createQuery(() => ({
|
||||
queryKey: ['string'],
|
||||
queryFn,
|
||||
}))
|
||||
</script>
|
||||
|
||||
<HydrationBoundary
|
||||
state={dehydratedState}
|
||||
options={undefined}
|
||||
queryClient={undefined}
|
||||
>
|
||||
<div>data: {query.data ?? 'undefined'}</div>
|
||||
</HydrationBoundary>
|
||||
@@ -0,0 +1,47 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { render } from '@testing-library/svelte'
|
||||
import { QueryClient, dehydrate, noop } from '@tanstack/query-core'
|
||||
import { sleep } from '@tanstack/query-test-utils'
|
||||
import Base from './Base.svelte'
|
||||
|
||||
describe('HydrationBoundary', () => {
|
||||
let queryClient: QueryClient
|
||||
let stringifiedState: string
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.useFakeTimers()
|
||||
queryClient = new QueryClient()
|
||||
const dehydrateClient = new QueryClient()
|
||||
void dehydrateClient
|
||||
.query({
|
||||
queryKey: ['string'],
|
||||
queryFn: () => sleep(10).then(() => 'stringCached'),
|
||||
})
|
||||
.catch(noop)
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
const dehydrated = dehydrate(dehydrateClient)
|
||||
stringifiedState = JSON.stringify(dehydrated)
|
||||
dehydrateClient.clear()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
queryClient.clear()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('should hydrate queries to the cache on context', async () => {
|
||||
const dehydratedState = JSON.parse(stringifiedState)
|
||||
|
||||
const rendered = render(Base, {
|
||||
props: {
|
||||
queryClient,
|
||||
dehydratedState,
|
||||
queryFn: () => sleep(20).then(() => 'string'),
|
||||
},
|
||||
})
|
||||
|
||||
expect(rendered.getByText('data: stringCached')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(20)
|
||||
expect(rendered.getByText('data: string')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
15
packages/svelte-query/tests/QueryClientProvider/Base.svelte
Normal file
15
packages/svelte-query/tests/QueryClientProvider/Base.svelte
Normal file
@@ -0,0 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { QueryClientProvider } from '../../src/index.js'
|
||||
import Child from './Child.svelte'
|
||||
import type { QueryClient } from '@tanstack/query-core'
|
||||
|
||||
type Props = {
|
||||
queryClient: QueryClient
|
||||
}
|
||||
|
||||
let { queryClient }: Props = $props()
|
||||
</script>
|
||||
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Child />
|
||||
</QueryClientProvider>
|
||||
11
packages/svelte-query/tests/QueryClientProvider/Child.svelte
Normal file
11
packages/svelte-query/tests/QueryClientProvider/Child.svelte
Normal file
@@ -0,0 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { createQuery } from '../../src/index.js'
|
||||
import { sleep } from '@tanstack/query-test-utils'
|
||||
|
||||
const query = createQuery(() => ({
|
||||
queryKey: ['hello'],
|
||||
queryFn: () => sleep(10).then(() => 'test'),
|
||||
}))
|
||||
</script>
|
||||
|
||||
<div>Data: {query.data ?? 'undefined'}</div>
|
||||
@@ -0,0 +1,33 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { render } from '@testing-library/svelte'
|
||||
import { QueryClient } from '@tanstack/query-core'
|
||||
import Base from './Base.svelte'
|
||||
|
||||
describe('QueryClientProvider', () => {
|
||||
let queryClient: QueryClient
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
queryClient = new QueryClient()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
queryClient.clear()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('should set a specific cache for all queries to use', async () => {
|
||||
const queryCache = queryClient.getQueryCache()
|
||||
|
||||
const rendered = render(Base, {
|
||||
props: {
|
||||
queryClient,
|
||||
},
|
||||
})
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('Data: test')).toBeInTheDocument()
|
||||
|
||||
expect(queryCache.find({ queryKey: ['hello'] })?.state.data).toBe('test')
|
||||
})
|
||||
})
|
||||
244
packages/svelte-query/tests/containers.svelte.test.ts
Normal file
244
packages/svelte-query/tests/containers.svelte.test.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
import { flushSync } from 'svelte'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createRawRef } from '../src/containers.svelte.js'
|
||||
import { withEffectRoot } from './utils.svelte.js'
|
||||
|
||||
describe('createRawRef', () => {
|
||||
it('should create a reactive reference', () => {
|
||||
const [ref, update] = createRawRef({ a: 1, b: 2 })
|
||||
|
||||
expect(ref).toEqual({ a: 1, b: 2 })
|
||||
|
||||
update({ a: 3, b: 4 })
|
||||
expect(ref).toEqual({ a: 3, b: 4 })
|
||||
|
||||
ref.a = 5
|
||||
expect(ref).toEqual({ a: 5, b: 4 })
|
||||
})
|
||||
|
||||
it('should handle nested objects', () => {
|
||||
const [ref, update] = createRawRef<{ a: any }>({ a: { b: { c: 1 } } })
|
||||
|
||||
expect(ref).toEqual({ a: { b: { c: 1 } } })
|
||||
|
||||
// update with same structure
|
||||
update({ a: { b: { c: 2 } } })
|
||||
expect(ref).toEqual({ a: { b: { c: 2 } } })
|
||||
|
||||
ref.a.b.c = 3
|
||||
expect(ref).toEqual({ a: { b: { c: 3 } } })
|
||||
|
||||
// update with different structure should wipe out everything below the first level
|
||||
update({ a: { b: 3 } })
|
||||
expect(ref).toEqual({ a: { b: 3 } })
|
||||
})
|
||||
|
||||
it('should remove properties when a new object is assigned', () => {
|
||||
const [ref, update] = createRawRef<Record<string, number>>({
|
||||
a: 1,
|
||||
b: 2,
|
||||
})
|
||||
|
||||
expect(ref).toEqual({ a: 1, b: 2 })
|
||||
|
||||
update({ a: 3 })
|
||||
expect(ref).toEqual({ a: 3 })
|
||||
})
|
||||
|
||||
it(
|
||||
'should not break reactivity when removing keys',
|
||||
withEffectRoot(() => {
|
||||
const [ref, update] = createRawRef<Record<string, number>>({ a: 1, b: 2 })
|
||||
const states: Array<number | undefined> = []
|
||||
$effect(() => {
|
||||
states.push(ref.b)
|
||||
})
|
||||
|
||||
// these flushSync calls force the effect to run and push the value to the states array
|
||||
flushSync()
|
||||
update({ a: 3 }) // should remove b, and should rerun the effect
|
||||
flushSync()
|
||||
update({ a: 3, b: 4 }) // should add b back, and should rerun the effect
|
||||
flushSync()
|
||||
delete ref.b // should remove b, and should rerun the effect
|
||||
flushSync()
|
||||
delete ref.a // should remove a, and should _not_ rerun the effect
|
||||
expect(states).toEqual([2, undefined, 4, undefined])
|
||||
}),
|
||||
)
|
||||
|
||||
it(
|
||||
'should correctly trap calls to `in`',
|
||||
withEffectRoot(() => {
|
||||
const [ref, update] = createRawRef<Record<string, number>>({
|
||||
a: 1,
|
||||
b: 2,
|
||||
})
|
||||
|
||||
expect('b' in ref).toBe(true)
|
||||
delete ref.b
|
||||
expect('b' in ref).toBe(false)
|
||||
update({})
|
||||
expect('a' in ref).toBe(false)
|
||||
update({ a: 1, b: 2 })
|
||||
expect('b' in ref).toBe(true)
|
||||
expect('a' in ref).toBe(true)
|
||||
}),
|
||||
)
|
||||
|
||||
it('should correctly trap calls to `ownKeys`', () => {
|
||||
const [ref, update] = createRawRef<Record<string, number>>({
|
||||
a: 1,
|
||||
b: 2,
|
||||
})
|
||||
|
||||
expect(Object.keys(ref)).toEqual(['a', 'b'])
|
||||
|
||||
delete ref.b
|
||||
expect(Reflect.ownKeys(ref)).toEqual(['a'])
|
||||
|
||||
update({})
|
||||
expect(Object.keys(ref)).toEqual([])
|
||||
|
||||
update({ a: 1, b: 2 })
|
||||
expect(Object.keys(ref)).toEqual(['a', 'b'])
|
||||
})
|
||||
|
||||
it('should correctly trap calls to `getOwnPropertyDescriptor`', () => {
|
||||
const [ref, update] = createRawRef<Record<string, number>>({
|
||||
a: 1,
|
||||
b: 2,
|
||||
})
|
||||
|
||||
expect(Reflect.getOwnPropertyDescriptor(ref, 'b')).toEqual({
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get: expect.any(Function),
|
||||
set: expect.any(Function),
|
||||
})
|
||||
|
||||
delete ref.b
|
||||
expect(Reflect.getOwnPropertyDescriptor(ref, 'b')).toEqual(undefined)
|
||||
|
||||
update({})
|
||||
expect(Reflect.getOwnPropertyDescriptor(ref, 'a')).toEqual(undefined)
|
||||
|
||||
update({ a: 1, b: 2 })
|
||||
expect(Reflect.getOwnPropertyDescriptor(ref, 'a')).toEqual({
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get: expect.any(Function),
|
||||
set: expect.any(Function),
|
||||
})
|
||||
expect(Reflect.getOwnPropertyDescriptor(ref, 'b')).toEqual({
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get: expect.any(Function),
|
||||
set: expect.any(Function),
|
||||
})
|
||||
})
|
||||
|
||||
it('should lazily access values when using `update`', () => {
|
||||
let aAccessed = false
|
||||
let bAccessed = false
|
||||
const [ref, update] = createRawRef({
|
||||
get a() {
|
||||
aAccessed = true
|
||||
return 1
|
||||
},
|
||||
get b() {
|
||||
bAccessed = true
|
||||
return 2
|
||||
},
|
||||
})
|
||||
|
||||
expect(aAccessed).toBe(false)
|
||||
expect(bAccessed).toBe(false)
|
||||
|
||||
expect(ref.a).toBe(1)
|
||||
|
||||
expect(aAccessed).toBe(true)
|
||||
expect(bAccessed).toBe(false)
|
||||
|
||||
aAccessed = false
|
||||
bAccessed = false
|
||||
|
||||
update({
|
||||
get a() {
|
||||
aAccessed = true
|
||||
return 2
|
||||
},
|
||||
get b() {
|
||||
bAccessed = true
|
||||
return 3
|
||||
},
|
||||
})
|
||||
|
||||
expect(aAccessed).toBe(false)
|
||||
expect(bAccessed).toBe(false)
|
||||
|
||||
expect(ref.a).toBe(2)
|
||||
|
||||
expect(aAccessed).toBe(true)
|
||||
expect(bAccessed).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle arrays', () => {
|
||||
const [ref, update] = createRawRef([1, 2, 3])
|
||||
|
||||
expect(ref).toEqual([1, 2, 3])
|
||||
|
||||
ref[0] = 4
|
||||
expect(ref).toEqual([4, 2, 3])
|
||||
|
||||
update([5, 6])
|
||||
expect(ref).toEqual([5, 6])
|
||||
|
||||
update([7, 8, 9])
|
||||
expect(ref).toEqual([7, 8, 9])
|
||||
})
|
||||
|
||||
it('should return `false` when deleting a property that does not exist', () => {
|
||||
const [ref] = createRawRef<Record<string, number>>({ a: 1, b: 2 })
|
||||
|
||||
expect(Reflect.deleteProperty(ref, 'c')).toBe(false)
|
||||
expect(ref).toEqual({ a: 1, b: 2 })
|
||||
})
|
||||
|
||||
it('should handle shrinking an array by more than one entry at once', () => {
|
||||
// Regression for #10341: createQueries crashed with
|
||||
// `TypeError: can't delete property 'N': proxy deleteProperty handler returned false`
|
||||
// when two or more items were removed from the reactive array in a single update.
|
||||
const [ref, update] = createRawRef([1, 2, 3, 4, 5])
|
||||
|
||||
expect(ref).toEqual([1, 2, 3, 4, 5])
|
||||
|
||||
update([1, 2])
|
||||
expect(ref).toEqual([1, 2])
|
||||
|
||||
update([1, 2, 3, 4])
|
||||
expect(ref).toEqual([1, 2, 3, 4])
|
||||
|
||||
update([])
|
||||
expect(ref).toEqual([])
|
||||
})
|
||||
|
||||
it('should behave like a regular object when not using `update`', () => {
|
||||
const [ref] = createRawRef<Record<string, unknown>>({ a: 1, b: 2 })
|
||||
|
||||
expect(ref).toEqual({ a: 1, b: 2 })
|
||||
|
||||
ref.a = 3
|
||||
expect(ref).toEqual({ a: 3, b: 2 })
|
||||
|
||||
ref.b = 4
|
||||
expect(ref).toEqual({ a: 3, b: 4 })
|
||||
|
||||
ref.c = 5
|
||||
expect(ref).toEqual({ a: 3, b: 4, c: 5 })
|
||||
|
||||
ref.fn = () => 6
|
||||
expect(ref).toEqual({ a: 3, b: 4, c: 5, fn: expect.any(Function) })
|
||||
expect((ref.fn as () => number)()).toBe(6)
|
||||
})
|
||||
})
|
||||
5
packages/svelte-query/tests/context/Base.svelte
Normal file
5
packages/svelte-query/tests/context/Base.svelte
Normal file
@@ -0,0 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { getQueryClientContext } from '../../src/index.js'
|
||||
|
||||
getQueryClientContext()
|
||||
</script>
|
||||
18
packages/svelte-query/tests/context/context.svelte.test.ts
Normal file
18
packages/svelte-query/tests/context/context.svelte.test.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { render } from '@testing-library/svelte'
|
||||
import { getIsRestoringContext } from '../../src/index.js'
|
||||
import Base from './Base.svelte'
|
||||
|
||||
describe('getQueryClientContext', () => {
|
||||
it('should throw when called without a client in context', () => {
|
||||
expect(() => render(Base)).toThrow(
|
||||
'No QueryClient was found in Svelte context. Did you forget to wrap your component with QueryClientProvider?',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getIsRestoringContext', () => {
|
||||
it('should not throw when called outside of a component', () => {
|
||||
expect(() => getIsRestoringContext()).not.toThrow()
|
||||
})
|
||||
})
|
||||
30
packages/svelte-query/tests/createInfiniteQuery/Base.svelte
Normal file
30
packages/svelte-query/tests/createInfiniteQuery/Base.svelte
Normal file
@@ -0,0 +1,30 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from 'svelte'
|
||||
import { queryKey, sleep } from '@tanstack/query-test-utils'
|
||||
import { createInfiniteQuery } from '../../src/index.js'
|
||||
import { setQueryClientContext } from '../../src/context.js'
|
||||
import type { QueryClient, QueryObserverResult } from '@tanstack/query-core'
|
||||
|
||||
type Props = {
|
||||
queryClient: QueryClient
|
||||
states: { value: Array<QueryObserverResult> }
|
||||
}
|
||||
|
||||
let { queryClient, states }: Props = $props()
|
||||
|
||||
setQueryClientContext(queryClient)
|
||||
|
||||
const query = createInfiniteQuery(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: ({ pageParam }) => sleep(10).then(() => pageParam),
|
||||
getNextPageParam: (lastPage) => lastPage + 1,
|
||||
initialPageParam: 0,
|
||||
}))
|
||||
|
||||
$effect(() => {
|
||||
// @ts-expect-error
|
||||
states.value = [...untrack(() => states.value), $state.snapshot(query)]
|
||||
})
|
||||
</script>
|
||||
|
||||
<div>Status: {query.status}</div>
|
||||
@@ -0,0 +1,39 @@
|
||||
<script lang="ts">
|
||||
import { QueryClient } from '@tanstack/query-core'
|
||||
import { createInfiniteQuery } from '../../src/index.js'
|
||||
import { setQueryClientContext } from '../../src/context.js'
|
||||
import { sleep } from '@tanstack/query-test-utils'
|
||||
|
||||
type Props = {
|
||||
queryClient: QueryClient
|
||||
}
|
||||
|
||||
let { queryClient }: Props = $props()
|
||||
|
||||
const queryKey = ['test']
|
||||
|
||||
let firstPage = $state(0)
|
||||
|
||||
setQueryClientContext(queryClient)
|
||||
|
||||
const query = createInfiniteQuery(() => ({
|
||||
queryKey: queryKey,
|
||||
queryFn: ({ pageParam }) => sleep(10).then(() => pageParam),
|
||||
getNextPageParam: (lastPage) => lastPage + 1,
|
||||
initialPageParam: firstPage,
|
||||
}))
|
||||
</script>
|
||||
|
||||
<button
|
||||
onclick={() => {
|
||||
queryClient.setQueryData(queryKey, {
|
||||
pages: [7, 8],
|
||||
pageParams: [7, 8],
|
||||
})
|
||||
firstPage = 7
|
||||
}}
|
||||
>
|
||||
setPages
|
||||
</button>
|
||||
|
||||
<div>Data: {JSON.stringify(query.data)}</div>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from 'svelte'
|
||||
import { createInfiniteQuery } from '../../src/index.js'
|
||||
import { setQueryClientContext } from '../../src/context.js'
|
||||
import type { QueryClient, QueryObserverResult } from '@tanstack/query-core'
|
||||
import { queryKey, sleep } from '@tanstack/query-test-utils'
|
||||
|
||||
type Props = {
|
||||
queryClient: QueryClient
|
||||
states: { value: Array<QueryObserverResult> }
|
||||
}
|
||||
|
||||
let { queryClient, states }: Props = $props()
|
||||
|
||||
setQueryClientContext(queryClient)
|
||||
|
||||
const query = createInfiniteQuery(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: ({ pageParam }) => sleep(10).then(() => pageParam),
|
||||
getNextPageParam: (lastPage) => lastPage + 1,
|
||||
initialPageParam: 0,
|
||||
initialData: { pages: [0], pageParams: [0] },
|
||||
}))
|
||||
|
||||
$effect(() => {
|
||||
// @ts-expect-error
|
||||
states.value = [...untrack(() => states.value), $state.snapshot(query)]
|
||||
})
|
||||
</script>
|
||||
|
||||
<div>Status: {query.status}</div>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from 'svelte'
|
||||
import { createInfiniteQuery } from '../../src/index.js'
|
||||
import { setQueryClientContext } from '../../src/context.js'
|
||||
import type { QueryClient, QueryObserverResult } from '@tanstack/query-core'
|
||||
import { queryKey, sleep } from '@tanstack/query-test-utils'
|
||||
|
||||
type Props = {
|
||||
queryClient: QueryClient
|
||||
states: { value: Array<QueryObserverResult> }
|
||||
}
|
||||
|
||||
let { queryClient, states }: Props = $props()
|
||||
|
||||
setQueryClientContext(queryClient)
|
||||
|
||||
const query = createInfiniteQuery(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => sleep(10).then(() => ({ count: 1 })),
|
||||
select: (data) => ({
|
||||
pages: data.pages.map((x) => `count: ${x.count}`),
|
||||
pageParams: data.pageParams,
|
||||
}),
|
||||
getNextPageParam: () => undefined,
|
||||
initialPageParam: 0,
|
||||
}))
|
||||
|
||||
$effect(() => {
|
||||
// @ts-expect-error
|
||||
states.value = [...untrack(() => states.value), $state.snapshot(query)]
|
||||
})
|
||||
</script>
|
||||
|
||||
<div>{query.data?.pages.join(',')}</div>
|
||||
@@ -0,0 +1,174 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { fireEvent, render } from '@testing-library/svelte'
|
||||
import { QueryClient } from '@tanstack/query-core'
|
||||
import { ref } from '../utils.svelte.js'
|
||||
import Base from './Base.svelte'
|
||||
import Select from './Select.svelte'
|
||||
import ChangeClient from './ChangeClient.svelte'
|
||||
import InitialData from './InitialData.svelte'
|
||||
import type { QueryObserverResult } from '@tanstack/query-core'
|
||||
|
||||
describe('createInfiniteQuery', () => {
|
||||
let queryClient: QueryClient
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
queryClient = new QueryClient()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
queryClient.clear()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('should return the correct states for a successful query', async () => {
|
||||
let states = ref<Array<QueryObserverResult>>([])
|
||||
|
||||
const rendered = render(Base, {
|
||||
props: {
|
||||
queryClient,
|
||||
states,
|
||||
},
|
||||
})
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('Status: success')).toBeInTheDocument()
|
||||
|
||||
expect(states.value).toHaveLength(2)
|
||||
|
||||
expect(states.value[0]).toEqual({
|
||||
data: undefined,
|
||||
dataUpdatedAt: 0,
|
||||
error: null,
|
||||
errorUpdatedAt: 0,
|
||||
failureCount: 0,
|
||||
failureReason: null,
|
||||
errorUpdateCount: 0,
|
||||
fetchNextPage: expect.any(Function),
|
||||
fetchPreviousPage: expect.any(Function),
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
isError: false,
|
||||
isFetched: false,
|
||||
isFetchedAfterMount: false,
|
||||
isFetching: true,
|
||||
isPaused: false,
|
||||
isFetchNextPageError: false,
|
||||
isFetchingNextPage: false,
|
||||
isFetchPreviousPageError: false,
|
||||
isFetchingPreviousPage: false,
|
||||
isLoading: true,
|
||||
isPending: true,
|
||||
isInitialLoading: true,
|
||||
isLoadingError: false,
|
||||
isPlaceholderData: false,
|
||||
isRefetchError: false,
|
||||
isRefetching: false,
|
||||
isStale: true,
|
||||
isSuccess: false,
|
||||
isEnabled: true,
|
||||
refetch: expect.any(Function),
|
||||
status: 'pending',
|
||||
fetchStatus: 'fetching',
|
||||
})
|
||||
|
||||
expect(states.value[1]).toEqual({
|
||||
data: { pages: [0], pageParams: [0] },
|
||||
dataUpdatedAt: expect.any(Number),
|
||||
error: null,
|
||||
errorUpdatedAt: 0,
|
||||
failureCount: 0,
|
||||
failureReason: null,
|
||||
errorUpdateCount: 0,
|
||||
fetchNextPage: expect.any(Function),
|
||||
fetchPreviousPage: expect.any(Function),
|
||||
hasNextPage: true,
|
||||
hasPreviousPage: false,
|
||||
isError: false,
|
||||
isFetched: true,
|
||||
isFetchedAfterMount: true,
|
||||
isFetching: false,
|
||||
isPaused: false,
|
||||
isFetchNextPageError: false,
|
||||
isFetchingNextPage: false,
|
||||
isFetchPreviousPageError: false,
|
||||
isFetchingPreviousPage: false,
|
||||
isLoading: false,
|
||||
isPending: false,
|
||||
isInitialLoading: false,
|
||||
isLoadingError: false,
|
||||
isPlaceholderData: false,
|
||||
isRefetchError: false,
|
||||
isRefetching: false,
|
||||
isStale: true,
|
||||
isSuccess: true,
|
||||
isEnabled: true,
|
||||
refetch: expect.any(Function),
|
||||
status: 'success',
|
||||
fetchStatus: 'idle',
|
||||
})
|
||||
})
|
||||
|
||||
it('should render with initialData and no pending state', async () => {
|
||||
let states = ref<Array<QueryObserverResult>>([])
|
||||
|
||||
const rendered = render(InitialData, {
|
||||
props: {
|
||||
queryClient,
|
||||
states,
|
||||
},
|
||||
})
|
||||
|
||||
expect(rendered.getByText('Status: success')).toBeInTheDocument()
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(states.value.every((state) => state.status === 'success')).toBe(true)
|
||||
expect(states.value[0]?.data).toEqual({ pages: [0], pageParams: [0] })
|
||||
})
|
||||
|
||||
it('should be able to select a part of the data', async () => {
|
||||
let states = ref<Array<QueryObserverResult>>([])
|
||||
|
||||
const rendered = render(Select, {
|
||||
props: {
|
||||
queryClient,
|
||||
states,
|
||||
},
|
||||
})
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('count: 1')).toBeInTheDocument()
|
||||
|
||||
expect(states.value).toHaveLength(2)
|
||||
|
||||
expect(states.value[0]).toMatchObject({
|
||||
data: undefined,
|
||||
isSuccess: false,
|
||||
})
|
||||
|
||||
expect(states.value[1]).toMatchObject({
|
||||
data: { pages: ['count: 1'] },
|
||||
isSuccess: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('should be able to set new pages with the query client', async () => {
|
||||
const rendered = render(ChangeClient, {
|
||||
props: {
|
||||
queryClient,
|
||||
},
|
||||
})
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(
|
||||
rendered.getByText('Data: {"pages":[0],"pageParams":[0]}'),
|
||||
).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /setPages/i }))
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(
|
||||
rendered.getByText('Data: {"pages":[7,8],"pageParams":[7,8]}'),
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,227 @@
|
||||
import { describe, expectTypeOf, it } from 'vitest'
|
||||
import { QueryClient } from '@tanstack/query-core'
|
||||
import { queryKey } from '@tanstack/query-test-utils'
|
||||
import { createInfiniteQuery, infiniteQueryOptions } from '../../src/index.js'
|
||||
import type { InfiniteData } from '@tanstack/query-core'
|
||||
|
||||
describe('createInfiniteQuery', () => {
|
||||
describe('pageParam', () => {
|
||||
it('initialPageParam should define type of param passed to queryFunctionContext', () => {
|
||||
createInfiniteQuery(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: ({ pageParam }) => {
|
||||
expectTypeOf(pageParam).toEqualTypeOf<number>()
|
||||
},
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: () => undefined,
|
||||
}))
|
||||
})
|
||||
|
||||
it('direction should be passed to queryFn of createInfiniteQuery', () => {
|
||||
createInfiniteQuery(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: ({ direction }) => {
|
||||
expectTypeOf(direction).toEqualTypeOf<'forward' | 'backward'>()
|
||||
},
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: () => undefined,
|
||||
}))
|
||||
})
|
||||
|
||||
it('initialPageParam should define type of param passed to queryFunctionContext for fetchInfiniteQuery', () => {
|
||||
const queryClient = new QueryClient()
|
||||
// grandfathered direct test
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
queryClient.fetchInfiniteQuery({
|
||||
queryKey: queryKey(),
|
||||
queryFn: ({ pageParam }) => {
|
||||
expectTypeOf(pageParam).toEqualTypeOf<number>()
|
||||
},
|
||||
initialPageParam: 1,
|
||||
})
|
||||
})
|
||||
|
||||
it('initialPageParam should define type of param passed to queryFunctionContext for prefetchInfiniteQuery', () => {
|
||||
const queryClient = new QueryClient()
|
||||
// grandfathered direct test
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
queryClient.prefetchInfiniteQuery({
|
||||
queryKey: queryKey(),
|
||||
queryFn: ({ pageParam }) => {
|
||||
expectTypeOf(pageParam).toEqualTypeOf<number>()
|
||||
},
|
||||
initialPageParam: 1,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('initialData', () => {
|
||||
it('TData should always be defined when initialData is provided', () => {
|
||||
const { data } = createInfiniteQuery(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: ({ pageParam }) => {
|
||||
return pageParam * 5
|
||||
},
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: () => undefined,
|
||||
initialData: { pages: [5], pageParams: [1] },
|
||||
}))
|
||||
|
||||
// TODO: Order of generics prevents pageParams to be typed correctly. Using `unknown` for now
|
||||
expectTypeOf(data).toEqualTypeOf<InfiniteData<number, unknown>>()
|
||||
})
|
||||
|
||||
it('TData should always be defined when initialData is provided through infiniteQueryOptions', () => {
|
||||
const options = infiniteQueryOptions({
|
||||
queryKey: queryKey(),
|
||||
queryFn: ({ pageParam }: { pageParam: number }) => {
|
||||
return pageParam * 5
|
||||
},
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: () => undefined,
|
||||
initialData: { pages: [5], pageParams: [1] },
|
||||
})
|
||||
const { data } = createInfiniteQuery(() => options)
|
||||
|
||||
// known issue: type of pageParams is unknown when returned from createInfiniteQuery
|
||||
expectTypeOf(data).toEqualTypeOf<InfiniteData<number, unknown>>()
|
||||
})
|
||||
|
||||
it('TData should have undefined in the union when initialData is NOT provided', () => {
|
||||
const { data } = createInfiniteQuery(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: ({ pageParam }) => {
|
||||
return pageParam * 5
|
||||
},
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: () => undefined,
|
||||
}))
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<
|
||||
InfiniteData<number, unknown> | undefined
|
||||
>()
|
||||
})
|
||||
})
|
||||
|
||||
describe('select', () => {
|
||||
it('should still return paginated data if no select result', () => {
|
||||
const infiniteQuery = createInfiniteQuery(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: ({ pageParam }) => {
|
||||
return pageParam * 5
|
||||
},
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: () => undefined,
|
||||
}))
|
||||
|
||||
// TODO: Order of generics prevents pageParams to be typed correctly. Using `unknown` for now
|
||||
expectTypeOf(infiniteQuery.data).toEqualTypeOf<
|
||||
InfiniteData<number, unknown> | undefined
|
||||
>()
|
||||
})
|
||||
|
||||
it('should be able to transform data to arbitrary result', () => {
|
||||
const infiniteQuery = createInfiniteQuery(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: ({ pageParam }) => {
|
||||
return pageParam * 5
|
||||
},
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: () => undefined,
|
||||
select: (data) => {
|
||||
expectTypeOf(data).toEqualTypeOf<InfiniteData<number, number>>()
|
||||
return 'selected' as const
|
||||
},
|
||||
}))
|
||||
|
||||
expectTypeOf(infiniteQuery.data).toEqualTypeOf<'selected' | undefined>()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getNextPageParam / getPreviousPageParam', () => {
|
||||
it('should get typed params', () => {
|
||||
const infiniteQuery = createInfiniteQuery(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: ({ pageParam }) => {
|
||||
return String(pageParam)
|
||||
},
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: (
|
||||
lastPage,
|
||||
allPages,
|
||||
lastPageParam,
|
||||
allPageParams,
|
||||
) => {
|
||||
expectTypeOf(lastPage).toEqualTypeOf<string>()
|
||||
expectTypeOf(allPages).toEqualTypeOf<Array<string>>()
|
||||
expectTypeOf(lastPageParam).toEqualTypeOf<number>()
|
||||
expectTypeOf(allPageParams).toEqualTypeOf<Array<number>>()
|
||||
return undefined
|
||||
},
|
||||
getPreviousPageParam: (
|
||||
firstPage,
|
||||
allPages,
|
||||
firstPageParam,
|
||||
allPageParams,
|
||||
) => {
|
||||
expectTypeOf(firstPage).toEqualTypeOf<string>()
|
||||
expectTypeOf(allPages).toEqualTypeOf<Array<string>>()
|
||||
expectTypeOf(firstPageParam).toEqualTypeOf<number>()
|
||||
expectTypeOf(allPageParams).toEqualTypeOf<Array<number>>()
|
||||
return undefined
|
||||
},
|
||||
}))
|
||||
|
||||
// TODO: Order of generics prevents pageParams to be typed correctly. Using `unknown` for now
|
||||
expectTypeOf(infiniteQuery.data).toEqualTypeOf<
|
||||
InfiniteData<string, unknown> | undefined
|
||||
>()
|
||||
})
|
||||
})
|
||||
|
||||
describe('error booleans', () => {
|
||||
it('should not be permanently `false`', () => {
|
||||
const {
|
||||
isFetchNextPageError,
|
||||
isFetchPreviousPageError,
|
||||
isLoadingError,
|
||||
isRefetchError,
|
||||
} = createInfiniteQuery(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: ({ pageParam }) => {
|
||||
return pageParam * 5
|
||||
},
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: () => undefined,
|
||||
}))
|
||||
|
||||
expectTypeOf(isFetchNextPageError).toEqualTypeOf<boolean>()
|
||||
expectTypeOf(isFetchPreviousPageError).toEqualTypeOf<boolean>()
|
||||
expectTypeOf(isLoadingError).toEqualTypeOf<boolean>()
|
||||
expectTypeOf(isRefetchError).toEqualTypeOf<boolean>()
|
||||
})
|
||||
})
|
||||
|
||||
describe('queryClient', () => {
|
||||
it('should accept queryClient as second argument', () => {
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
const infiniteQuery = createInfiniteQuery(
|
||||
() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: ({ pageParam }) => {
|
||||
return pageParam * 5
|
||||
},
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: () => undefined,
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
// TODO: Order of generics prevents pageParams to be typed correctly. Using `unknown` for now
|
||||
expectTypeOf(infiniteQuery.data).toEqualTypeOf<
|
||||
InfiniteData<number, unknown> | undefined
|
||||
>()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,30 @@
|
||||
<script lang="ts">
|
||||
import type { QueryClient } from '@tanstack/query-core'
|
||||
import { createMutation, setQueryClientContext } from '../../src/index.js'
|
||||
import { sleep } from '@tanstack/query-test-utils'
|
||||
|
||||
type Props = {
|
||||
queryClient: QueryClient
|
||||
onSuccessPerCall: (...args: Array<unknown>) => void
|
||||
}
|
||||
|
||||
const { queryClient, onSuccessPerCall }: Props = $props()
|
||||
|
||||
setQueryClientContext(queryClient)
|
||||
|
||||
const mutation = createMutation(() => ({
|
||||
mutationFn: (text: string) => sleep(10).then(() => text),
|
||||
}))
|
||||
</script>
|
||||
|
||||
<button
|
||||
onclick={() => mutation.mutate('Todo 1', { onSuccess: onSuccessPerCall })}
|
||||
>
|
||||
mutate1
|
||||
</button>
|
||||
<button
|
||||
onclick={() => mutation.mutate('Todo 2', { onSuccess: onSuccessPerCall })}
|
||||
>
|
||||
mutate2
|
||||
</button>
|
||||
<div>data: {mutation.data ?? 'null'}, status: {mutation.status}</div>
|
||||
24
packages/svelte-query/tests/createMutation/Failure.svelte
Normal file
24
packages/svelte-query/tests/createMutation/Failure.svelte
Normal file
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import type { QueryClient } from '@tanstack/query-core'
|
||||
import { createMutation, setQueryClientContext } from '../../src/index.js'
|
||||
|
||||
type Props = {
|
||||
queryClient: QueryClient
|
||||
mutationFn: (value: { count: number }) => Promise<{ count: number }>
|
||||
}
|
||||
|
||||
let { queryClient, mutationFn }: Props = $props()
|
||||
|
||||
let count = $state(0)
|
||||
|
||||
setQueryClientContext(queryClient)
|
||||
|
||||
const mutation = createMutation(() => ({ mutationFn }))
|
||||
</script>
|
||||
|
||||
<button onclick={() => mutation.mutate({ count: ++count })}>Mutate</button>
|
||||
|
||||
<div>Data: {mutation.data?.count ?? 'undefined'}</div>
|
||||
<div>Status: {mutation.status}</div>
|
||||
<div>Failure Count: {mutation.failureCount}</div>
|
||||
<div>Failure Reason: {mutation.failureReason ?? 'undefined'}</div>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import type { QueryClient, QueryKey } from '@tanstack/query-core'
|
||||
import { createMutation, setQueryClientContext } from '../../src/index.js'
|
||||
import { sleep } from '@tanstack/query-test-utils'
|
||||
|
||||
type Props = {
|
||||
queryClient: QueryClient
|
||||
queryKey: QueryKey
|
||||
}
|
||||
|
||||
const { queryClient, queryKey }: Props = $props()
|
||||
|
||||
setQueryClientContext(queryClient)
|
||||
|
||||
const mutation = createMutation(() => ({
|
||||
mutationFn: () => sleep(10).then(() => 'mutated'),
|
||||
onSuccess: (_data, _variables, _onMutateResult, context) => {
|
||||
context.client.invalidateQueries({ queryKey })
|
||||
},
|
||||
}))
|
||||
</script>
|
||||
|
||||
<button onclick={() => mutation.mutate()}>Mutate</button>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import type { QueryClient, QueryKey } from '@tanstack/query-core'
|
||||
import { createMutation, setQueryClientContext } from '../../src/index.js'
|
||||
import { sleep } from '@tanstack/query-test-utils'
|
||||
|
||||
type Props = {
|
||||
queryClient: QueryClient
|
||||
queryKey: QueryKey
|
||||
}
|
||||
|
||||
const { queryClient, queryKey }: Props = $props()
|
||||
|
||||
setQueryClientContext(queryClient)
|
||||
|
||||
const mutation = createMutation(() => ({
|
||||
mutationFn: (_text: string, context) =>
|
||||
sleep(10).then(() => context.client.getQueryData(queryKey)),
|
||||
}))
|
||||
</script>
|
||||
|
||||
<button onclick={() => mutation.mutate('todo')}>Mutate</button>
|
||||
|
||||
<div>data: {mutation.data}</div>
|
||||
@@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import type { QueryClient } from '@tanstack/query-core'
|
||||
import { createMutation, setQueryClientContext } from '../../src/index.js'
|
||||
import { sleep } from '@tanstack/query-test-utils'
|
||||
|
||||
type Props = {
|
||||
queryClient: QueryClient
|
||||
queryKey: Array<string>
|
||||
shouldSucceed: boolean
|
||||
}
|
||||
|
||||
const { queryClient, queryKey, shouldSucceed }: Props = $props()
|
||||
|
||||
setQueryClientContext(queryClient)
|
||||
|
||||
const mutation = createMutation(() => ({
|
||||
mutationFn: (newTodo: string) =>
|
||||
shouldSucceed
|
||||
? sleep(10).then(() => newTodo)
|
||||
: sleep(10).then(() => Promise.reject(new Error('Some error'))),
|
||||
onMutate: async (newTodo: string) => {
|
||||
await queryClient.cancelQueries({ queryKey })
|
||||
const previousTodos = queryClient.getQueryData<Array<string>>(queryKey)
|
||||
|
||||
queryClient.setQueryData<Array<string>>(queryKey, (old) => [
|
||||
...(old ?? []),
|
||||
newTodo,
|
||||
])
|
||||
|
||||
return { previousTodos }
|
||||
},
|
||||
onError: (_err, _newTodo, onMutateResult) => {
|
||||
queryClient.setQueryData(queryKey, onMutateResult?.previousTodos)
|
||||
},
|
||||
}))
|
||||
</script>
|
||||
|
||||
<button onclick={() => mutation.mutate('Todo 2')}>add</button>
|
||||
@@ -0,0 +1,58 @@
|
||||
<script lang="ts">
|
||||
import type { QueryClient } from '@tanstack/query-core'
|
||||
import { createMutation, setQueryClientContext } from '../../src/index.js'
|
||||
import { sleep } from '@tanstack/query-test-utils'
|
||||
|
||||
type Props = {
|
||||
queryClient: QueryClient
|
||||
strategy: 'all' | 'allSettled'
|
||||
failOn?: string
|
||||
}
|
||||
|
||||
const { queryClient, strategy, failOn }: Props = $props()
|
||||
|
||||
let result = $state('idle')
|
||||
|
||||
setQueryClientContext(queryClient)
|
||||
|
||||
const mutation = createMutation(() => ({
|
||||
mutationFn: (file: string) =>
|
||||
sleep(10).then(() => {
|
||||
if (file === failOn) {
|
||||
throw new Error('upload failed')
|
||||
}
|
||||
return `uploaded: ${file}`
|
||||
}),
|
||||
retry: false,
|
||||
}))
|
||||
|
||||
async function uploadAll() {
|
||||
if (strategy === 'all') {
|
||||
try {
|
||||
const results = await Promise.all([
|
||||
mutation.mutateAsync('file1'),
|
||||
mutation.mutateAsync('file2'),
|
||||
mutation.mutateAsync('file3'),
|
||||
])
|
||||
result = results.join(', ')
|
||||
} catch (error) {
|
||||
result = `error: ${(error as Error).message}`
|
||||
}
|
||||
} else {
|
||||
const results = await Promise.allSettled([
|
||||
mutation.mutateAsync('file1'),
|
||||
mutation.mutateAsync('file2'),
|
||||
mutation.mutateAsync('file3'),
|
||||
])
|
||||
result = results
|
||||
.map((r) =>
|
||||
r.status === 'fulfilled' ? r.value : `error: ${r.reason.message}`,
|
||||
)
|
||||
.join(', ')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={uploadAll}>upload all</button>
|
||||
|
||||
<div>result: {result}</div>
|
||||
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import type { QueryClient } from '@tanstack/query-core'
|
||||
import { createMutation, setQueryClientContext } from '../../src/index.js'
|
||||
import { sleep } from '@tanstack/query-test-utils'
|
||||
|
||||
type Props = {
|
||||
queryClient: QueryClient
|
||||
perCallOnSuccess: (...args: Array<unknown>) => void
|
||||
}
|
||||
|
||||
const { queryClient, perCallOnSuccess }: Props = $props()
|
||||
|
||||
setQueryClientContext(queryClient)
|
||||
|
||||
const mutation = createMutation(() => ({
|
||||
mutationFn: (text: string) => sleep(10).then(() => text),
|
||||
}))
|
||||
</script>
|
||||
|
||||
<button
|
||||
onclick={() => mutation.mutate('todo', { onSuccess: perCallOnSuccess })}
|
||||
>
|
||||
Mutate
|
||||
</button>
|
||||
23
packages/svelte-query/tests/createMutation/Reset.svelte
Normal file
23
packages/svelte-query/tests/createMutation/Reset.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import type { QueryClient } from '@tanstack/query-core'
|
||||
import { createMutation, setQueryClientContext } from '../../src/index.js'
|
||||
import { sleep } from '@tanstack/query-test-utils'
|
||||
|
||||
type Props = {
|
||||
queryClient: QueryClient
|
||||
}
|
||||
|
||||
let { queryClient }: Props = $props()
|
||||
|
||||
setQueryClientContext(queryClient)
|
||||
|
||||
const mutation = createMutation(() => ({
|
||||
mutationFn: () =>
|
||||
sleep(10).then(() => Promise.reject(new Error('Expected mock error'))),
|
||||
}))
|
||||
</script>
|
||||
|
||||
<button onclick={() => mutation.reset()}>Reset</button>
|
||||
<button onclick={() => mutation.mutate()}>Mutate</button>
|
||||
|
||||
<div>Error: {mutation.error?.message ?? 'undefined'}</div>
|
||||
31
packages/svelte-query/tests/createMutation/Success.svelte
Normal file
31
packages/svelte-query/tests/createMutation/Success.svelte
Normal file
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import type { QueryClient } from '@tanstack/query-core'
|
||||
import { createMutation, setQueryClientContext } from '../../src/index.js'
|
||||
import { sleep } from '@tanstack/query-test-utils'
|
||||
|
||||
type Props = {
|
||||
queryClient: QueryClient
|
||||
onSuccessMock: (data: number) => void
|
||||
onSettledMock: (data: number | undefined) => void
|
||||
}
|
||||
|
||||
const { queryClient, onSettledMock, onSuccessMock }: Props = $props()
|
||||
|
||||
let count = $state(0)
|
||||
|
||||
setQueryClientContext(queryClient)
|
||||
|
||||
const mutation = createMutation(() => ({
|
||||
mutationFn: (vars: { count: number }) => sleep(10).then(() => vars.count),
|
||||
onSuccess: (data) => {
|
||||
onSuccessMock(data)
|
||||
},
|
||||
onSettled: (data) => {
|
||||
onSettledMock(data)
|
||||
},
|
||||
}))
|
||||
</script>
|
||||
|
||||
<button onclick={() => mutation.mutate({ count: ++count })}>Mutate</button>
|
||||
|
||||
<div>Count: {count}</div>
|
||||
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import type { MutationKey, QueryClient } from '@tanstack/query-core'
|
||||
import { createMutation, setQueryClientContext } from '../../src/index.js'
|
||||
import { sleep } from '@tanstack/query-test-utils'
|
||||
|
||||
type Props = {
|
||||
queryClient: QueryClient
|
||||
mutationKey?: MutationKey
|
||||
onSuccessMock: (...args: Array<unknown>) => void
|
||||
}
|
||||
|
||||
const { queryClient, mutationKey, onSuccessMock }: Props = $props()
|
||||
|
||||
setQueryClientContext(queryClient)
|
||||
|
||||
const mutation = createMutation(() => ({
|
||||
mutationKey,
|
||||
mutationFn: (text: string) => sleep(10).then(() => text.toUpperCase()),
|
||||
onMutate: (text: string) => ({ startedWith: text }),
|
||||
onSuccess: onSuccessMock,
|
||||
}))
|
||||
</script>
|
||||
|
||||
<button onclick={() => mutation.mutate('todo')}>Mutate</button>
|
||||
@@ -0,0 +1,706 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { flushSync } from 'svelte'
|
||||
import { fireEvent, render } from '@testing-library/svelte'
|
||||
import { QueryClient, noop } from '@tanstack/query-core'
|
||||
import { queryKey, sleep } from '@tanstack/query-test-utils'
|
||||
import { createMutation } from '../../src/index.js'
|
||||
import { withEffectRoot } from '../utils.svelte.js'
|
||||
import Reset from './Reset.svelte'
|
||||
import Success from './Success.svelte'
|
||||
import Failure from './Failure.svelte'
|
||||
import OptimisticUpdate from './OptimisticUpdate.svelte'
|
||||
import SuccessContext from './SuccessContext.svelte'
|
||||
import InvalidateFromContext from './InvalidateFromContext.svelte'
|
||||
import PerCallSuccess from './PerCallSuccess.svelte'
|
||||
import MutationFnContext from './MutationFnContext.svelte'
|
||||
import ParallelMutateAsync from './ParallelMutateAsync.svelte'
|
||||
import ConcurrentMutate from './ConcurrentMutate.svelte'
|
||||
|
||||
describe('createMutation', () => {
|
||||
let queryClient: QueryClient
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
queryClient = new QueryClient()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
queryClient.clear()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('should be able to reset `error`', async () => {
|
||||
const rendered = render(Reset, {
|
||||
props: { queryClient },
|
||||
})
|
||||
|
||||
expect(rendered.queryByText('Error: undefined')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /Mutate/i }))
|
||||
await vi.advanceTimersByTimeAsync(11)
|
||||
expect(rendered.getByText('Error: Expected mock error')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /Reset/i }))
|
||||
await vi.advanceTimersByTimeAsync(11)
|
||||
expect(rendered.getByText('Error: undefined')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it(
|
||||
'should call mutate callbacks when createMutation has no callbacks',
|
||||
withEffectRoot(async () => {
|
||||
const callbacks: Array<string> = []
|
||||
|
||||
const mutation = createMutation(
|
||||
() => ({
|
||||
mutationFn: (text: string) => sleep(10).then(() => text),
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
mutation.mutate('todo', {
|
||||
onSuccess: () => callbacks.push('mutate.onSuccess'),
|
||||
onSettled: () => callbacks.push('mutate.onSettled'),
|
||||
})
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(callbacks).toEqual(['mutate.onSuccess', 'mutate.onSettled'])
|
||||
}),
|
||||
)
|
||||
|
||||
it(
|
||||
'should call mutate error callbacks when createMutation has no callbacks',
|
||||
withEffectRoot(async () => {
|
||||
const callbacks: Array<string> = []
|
||||
|
||||
const mutation = createMutation(
|
||||
() => ({
|
||||
mutationFn: (_text: string) =>
|
||||
sleep(10).then(() => Promise.reject(new Error('oops'))),
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
mutation.mutate('todo', {
|
||||
onError: () => callbacks.push('mutate.onError'),
|
||||
onSettled: () => callbacks.push('mutate.onSettled'),
|
||||
})
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(callbacks).toEqual(['mutate.onError', 'mutate.onSettled'])
|
||||
}),
|
||||
)
|
||||
|
||||
it(
|
||||
'should call only mutate onSuccess when createMutation has no callbacks',
|
||||
withEffectRoot(async () => {
|
||||
const callbacks: Array<string> = []
|
||||
|
||||
const mutation = createMutation(
|
||||
() => ({
|
||||
mutationFn: (text: string) => sleep(10).then(() => text),
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
mutation.mutate('todo', {
|
||||
onSuccess: () => callbacks.push('mutate.onSuccess'),
|
||||
})
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(callbacks).toEqual(['mutate.onSuccess'])
|
||||
}),
|
||||
)
|
||||
|
||||
it(
|
||||
'should call only mutate onError when createMutation has no callbacks',
|
||||
withEffectRoot(async () => {
|
||||
const callbacks: Array<string> = []
|
||||
|
||||
const mutation = createMutation(
|
||||
() => ({
|
||||
mutationFn: (_text: string) =>
|
||||
sleep(10).then(() => Promise.reject(new Error('oops'))),
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
mutation.mutate('todo', {
|
||||
onError: () => callbacks.push('mutate.onError'),
|
||||
})
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(callbacks).toEqual(['mutate.onError'])
|
||||
}),
|
||||
)
|
||||
|
||||
it(
|
||||
'should call only mutate onSettled when createMutation has no callbacks',
|
||||
withEffectRoot(async () => {
|
||||
const callbacks: Array<string> = []
|
||||
|
||||
const mutation = createMutation(
|
||||
() => ({
|
||||
mutationFn: (text: string) => sleep(10).then(() => text),
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
mutation.mutate('todo', {
|
||||
onSettled: () => callbacks.push('mutate.onSettled'),
|
||||
})
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(callbacks).toEqual(['mutate.onSettled'])
|
||||
}),
|
||||
)
|
||||
|
||||
it(
|
||||
'should call mutateAsync callbacks when createMutation has no callbacks',
|
||||
withEffectRoot(async () => {
|
||||
const callbacks: Array<string> = []
|
||||
|
||||
const mutation = createMutation(
|
||||
() => ({
|
||||
mutationFn: (text: string) => sleep(10).then(() => text),
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
mutation.mutateAsync('todo', {
|
||||
onSuccess: () => callbacks.push('mutateAsync.onSuccess'),
|
||||
onSettled: () => callbacks.push('mutateAsync.onSettled'),
|
||||
})
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(callbacks).toEqual([
|
||||
'mutateAsync.onSuccess',
|
||||
'mutateAsync.onSettled',
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
it(
|
||||
'should call mutateAsync error callbacks when createMutation has no callbacks',
|
||||
withEffectRoot(async () => {
|
||||
const callbacks: Array<string> = []
|
||||
|
||||
const mutation = createMutation(
|
||||
() => ({
|
||||
mutationFn: (_text: string) =>
|
||||
sleep(10).then(() => Promise.reject(new Error('oops'))),
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
mutation
|
||||
.mutateAsync('todo', {
|
||||
onError: () => callbacks.push('mutateAsync.onError'),
|
||||
onSettled: () => callbacks.push('mutateAsync.onSettled'),
|
||||
})
|
||||
.catch(noop)
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(callbacks).toEqual([
|
||||
'mutateAsync.onError',
|
||||
'mutateAsync.onSettled',
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
it(
|
||||
'should call only mutateAsync onSuccess when createMutation has no callbacks',
|
||||
withEffectRoot(async () => {
|
||||
const callbacks: Array<string> = []
|
||||
|
||||
const mutation = createMutation(
|
||||
() => ({
|
||||
mutationFn: (text: string) => sleep(10).then(() => text),
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
mutation.mutateAsync('todo', {
|
||||
onSuccess: () => callbacks.push('mutateAsync.onSuccess'),
|
||||
})
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(callbacks).toEqual(['mutateAsync.onSuccess'])
|
||||
}),
|
||||
)
|
||||
|
||||
it(
|
||||
'should call only mutateAsync onError when createMutation has no callbacks',
|
||||
withEffectRoot(async () => {
|
||||
const callbacks: Array<string> = []
|
||||
|
||||
const mutation = createMutation(
|
||||
() => ({
|
||||
mutationFn: (_text: string) =>
|
||||
sleep(10).then(() => Promise.reject(new Error('oops'))),
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
mutation
|
||||
.mutateAsync('todo', {
|
||||
onError: () => callbacks.push('mutateAsync.onError'),
|
||||
})
|
||||
.catch(noop)
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(callbacks).toEqual(['mutateAsync.onError'])
|
||||
}),
|
||||
)
|
||||
|
||||
it(
|
||||
'should call only mutateAsync onSettled when createMutation has no callbacks',
|
||||
withEffectRoot(async () => {
|
||||
const callbacks: Array<string> = []
|
||||
|
||||
const mutation = createMutation(
|
||||
() => ({
|
||||
mutationFn: (text: string) => sleep(10).then(() => text),
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
mutation.mutateAsync('todo', {
|
||||
onSettled: () => callbacks.push('mutateAsync.onSettled'),
|
||||
})
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(callbacks).toEqual(['mutateAsync.onSettled'])
|
||||
}),
|
||||
)
|
||||
|
||||
it('should be able to call `onSuccess` and `onSettled` after each successful mutate', async () => {
|
||||
const onSuccessMock = vi.fn()
|
||||
const onSettledMock = vi.fn()
|
||||
|
||||
const rendered = render(Success, {
|
||||
props: {
|
||||
queryClient,
|
||||
onSuccessMock,
|
||||
onSettledMock,
|
||||
},
|
||||
})
|
||||
|
||||
expect(rendered.queryByText('Count: 0')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /Mutate/i }))
|
||||
fireEvent.click(rendered.getByRole('button', { name: /Mutate/i }))
|
||||
fireEvent.click(rendered.getByRole('button', { name: /Mutate/i }))
|
||||
await vi.advanceTimersByTimeAsync(11)
|
||||
expect(rendered.queryByText('Count: 3')).toBeInTheDocument()
|
||||
|
||||
expect(onSuccessMock).toHaveBeenCalledTimes(3)
|
||||
expect(onSuccessMock).toHaveBeenNthCalledWith(1, 1)
|
||||
expect(onSuccessMock).toHaveBeenNthCalledWith(2, 2)
|
||||
expect(onSuccessMock).toHaveBeenNthCalledWith(3, 3)
|
||||
|
||||
expect(onSettledMock).toHaveBeenCalledTimes(3)
|
||||
expect(onSettledMock).toHaveBeenNthCalledWith(1, 1)
|
||||
expect(onSettledMock).toHaveBeenNthCalledWith(2, 2)
|
||||
expect(onSettledMock).toHaveBeenNthCalledWith(3, 3)
|
||||
})
|
||||
|
||||
it('should set correct values for `failureReason` and `failureCount` on multiple mutate calls', async () => {
|
||||
type Value = { count: number }
|
||||
|
||||
const mutationFn = vi.fn<(value: Value) => Promise<Value>>()
|
||||
|
||||
mutationFn.mockImplementationOnce(() =>
|
||||
sleep(20).then(() => Promise.reject(`Expected mock error`)),
|
||||
)
|
||||
|
||||
mutationFn.mockImplementation((value) => sleep(10).then(() => value))
|
||||
|
||||
const rendered = render(Failure, {
|
||||
props: {
|
||||
queryClient,
|
||||
mutationFn,
|
||||
},
|
||||
})
|
||||
|
||||
expect(rendered.queryByText('Data: undefined')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /Mutate/i }))
|
||||
expect(rendered.getByText('Data: undefined')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(21)
|
||||
expect(rendered.getByText('Status: error')).toBeInTheDocument()
|
||||
expect(rendered.getByText('Failure Count: 1')).toBeInTheDocument()
|
||||
expect(
|
||||
rendered.getByText('Failure Reason: Expected mock error'),
|
||||
).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /Mutate/i }))
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(rendered.getByText('Status: pending')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(11)
|
||||
expect(rendered.getByText('Status: success')).toBeInTheDocument()
|
||||
expect(rendered.getByText('Data: 2')).toBeInTheDocument()
|
||||
expect(rendered.getByText('Failure Count: 0')).toBeInTheDocument()
|
||||
expect(rendered.getByText('Failure Reason: undefined')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it(
|
||||
'should be able to call `onSuccess` callback after successful mutateAsync',
|
||||
withEffectRoot(async () => {
|
||||
const callbacks: Array<string> = []
|
||||
|
||||
const mutation = createMutation(
|
||||
() => ({
|
||||
mutationFn: (text: string) => sleep(10).then(() => text),
|
||||
onSuccess: () => callbacks.push('useMutation.onSuccess'),
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
mutation.mutateAsync('todo', {
|
||||
onSuccess: () => callbacks.push('mutateAsync.onSuccess'),
|
||||
})
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(callbacks).toEqual([
|
||||
'useMutation.onSuccess',
|
||||
'mutateAsync.onSuccess',
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
it(
|
||||
'should be able to call `onError` callback after failed mutateAsync',
|
||||
withEffectRoot(async () => {
|
||||
const callbacks: Array<string> = []
|
||||
|
||||
const mutation = createMutation(
|
||||
() => ({
|
||||
mutationFn: (_text: string) =>
|
||||
sleep(10).then(() => Promise.reject(new Error('oops'))),
|
||||
onError: () => callbacks.push('useMutation.onError'),
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
mutation
|
||||
.mutateAsync('todo', {
|
||||
onError: () => callbacks.push('mutateAsync.onError'),
|
||||
})
|
||||
.catch(noop)
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(callbacks).toEqual(['useMutation.onError', 'mutateAsync.onError'])
|
||||
}),
|
||||
)
|
||||
|
||||
it(
|
||||
'should be able to call `onSettled` callback after mutateAsync',
|
||||
withEffectRoot(async () => {
|
||||
const callbacks: Array<string> = []
|
||||
|
||||
const mutation = createMutation(
|
||||
() => ({
|
||||
mutationFn: (text: string) => sleep(10).then(() => text),
|
||||
onSettled: () => callbacks.push('useMutation.onSettled'),
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
mutation.mutateAsync('todo', {
|
||||
onSettled: () => callbacks.push('mutateAsync.onSettled'),
|
||||
})
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(callbacks).toEqual([
|
||||
'useMutation.onSettled',
|
||||
'mutateAsync.onSettled',
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
it(
|
||||
'should be able to override the useMutation success callbacks',
|
||||
withEffectRoot(async () => {
|
||||
const callbacks: Array<string> = []
|
||||
|
||||
const mutation = createMutation(
|
||||
() => ({
|
||||
mutationFn: (text: string) => sleep(10).then(() => text),
|
||||
onSuccess: () =>
|
||||
sleep(10).then(() => callbacks.push('useMutation.onSuccess')),
|
||||
onSettled: () =>
|
||||
sleep(10).then(() => callbacks.push('useMutation.onSettled')),
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
mutation
|
||||
.mutateAsync('todo', {
|
||||
onSuccess: () => callbacks.push('mutateAsync.onSuccess'),
|
||||
onSettled: () => callbacks.push('mutateAsync.onSettled'),
|
||||
})
|
||||
.then((result) => callbacks.push(`mutateAsync.result:${result}`))
|
||||
await vi.advanceTimersByTimeAsync(30)
|
||||
|
||||
expect(callbacks).toEqual([
|
||||
'useMutation.onSuccess',
|
||||
'useMutation.onSettled',
|
||||
'mutateAsync.onSuccess',
|
||||
'mutateAsync.onSettled',
|
||||
'mutateAsync.result:todo',
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
it(
|
||||
'should be able to override the error callbacks when using mutateAsync',
|
||||
withEffectRoot(async () => {
|
||||
const callbacks: Array<string> = []
|
||||
|
||||
const mutation = createMutation(
|
||||
() => ({
|
||||
mutationFn: (_text: string) =>
|
||||
sleep(10).then(() => Promise.reject(new Error('oops'))),
|
||||
onError: () =>
|
||||
sleep(10).then(() => callbacks.push('useMutation.onError')),
|
||||
onSettled: () =>
|
||||
sleep(10).then(() => callbacks.push('useMutation.onSettled')),
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
mutation
|
||||
.mutateAsync('todo', {
|
||||
onError: () => callbacks.push('mutateAsync.onError'),
|
||||
onSettled: () => callbacks.push('mutateAsync.onSettled'),
|
||||
})
|
||||
.catch((error) =>
|
||||
callbacks.push(`mutateAsync.error:${(error as Error).message}`),
|
||||
)
|
||||
await vi.advanceTimersByTimeAsync(30)
|
||||
|
||||
expect(callbacks).toEqual([
|
||||
'useMutation.onError',
|
||||
'useMutation.onSettled',
|
||||
'mutateAsync.onError',
|
||||
'mutateAsync.onSettled',
|
||||
'mutateAsync.error:oops',
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
it(
|
||||
'should recreate observer when queryClient changes',
|
||||
withEffectRoot(async () => {
|
||||
const queryClient1 = new QueryClient()
|
||||
const queryClient2 = new QueryClient()
|
||||
|
||||
let activeClient = $state(queryClient1)
|
||||
|
||||
const mutation = createMutation(
|
||||
() => ({
|
||||
mutationFn: (params: string) => sleep(10).then(() => params),
|
||||
}),
|
||||
() => activeClient,
|
||||
)
|
||||
|
||||
mutation.mutate('first')
|
||||
await vi.advanceTimersByTimeAsync(11)
|
||||
|
||||
expect(mutation.status).toBe('success')
|
||||
expect(mutation.data).toBe('first')
|
||||
|
||||
activeClient = queryClient2
|
||||
flushSync()
|
||||
|
||||
expect(mutation.status).toBe('idle')
|
||||
expect(mutation.data).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
|
||||
it('should update the cache in onMutate and roll back via onMutateResult in onError', async () => {
|
||||
const key = queryKey()
|
||||
queryClient.setQueryData<Array<string>>(key, ['Todo 1'])
|
||||
|
||||
const rendered = render(OptimisticUpdate, {
|
||||
props: { queryClient, queryKey: key, shouldSucceed: false },
|
||||
})
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /add/i }))
|
||||
// The optimistic value lands after onMutate's first await, so flush
|
||||
// microtasks before asserting.
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(queryClient.getQueryData(key)).toEqual(['Todo 1', 'Todo 2'])
|
||||
|
||||
await vi.advanceTimersByTimeAsync(11)
|
||||
|
||||
expect(queryClient.getQueryData(key)).toEqual(['Todo 1'])
|
||||
})
|
||||
|
||||
it('should keep the optimistic update in place when the mutation succeeds', async () => {
|
||||
const key = queryKey()
|
||||
queryClient.setQueryData<Array<string>>(key, ['Todo 1'])
|
||||
|
||||
const rendered = render(OptimisticUpdate, {
|
||||
props: { queryClient, queryKey: key, shouldSucceed: true },
|
||||
})
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /add/i }))
|
||||
await vi.advanceTimersByTimeAsync(11)
|
||||
|
||||
expect(queryClient.getQueryData(key)).toEqual(['Todo 1', 'Todo 2'])
|
||||
})
|
||||
|
||||
it('should pass a non-undefined onMutateResult alongside context to onSuccess', async () => {
|
||||
const onSuccessMock = vi.fn()
|
||||
|
||||
const rendered = render(SuccessContext, {
|
||||
props: { queryClient, onSuccessMock },
|
||||
})
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /Mutate/i }))
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(onSuccessMock).toHaveBeenCalledTimes(1)
|
||||
const [data, variables, onMutateResult, context] =
|
||||
onSuccessMock.mock.calls[0]!
|
||||
expect(data).toBe('TODO')
|
||||
expect(variables).toBe('todo')
|
||||
expect(onMutateResult).toEqual({ startedWith: 'todo' })
|
||||
expect(context.client).toBe(queryClient)
|
||||
expect(context.meta).toBeUndefined()
|
||||
expect(context.mutationKey).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should include mutationKey in the context passed to hook-level callbacks', async () => {
|
||||
const onSuccessMock = vi.fn()
|
||||
|
||||
const rendered = render(SuccessContext, {
|
||||
props: { queryClient, mutationKey: ['todos', 'add'], onSuccessMock },
|
||||
})
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /Mutate/i }))
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(onSuccessMock).toHaveBeenCalledTimes(1)
|
||||
expect(onSuccessMock.mock.calls[0]?.[3].mutationKey).toEqual([
|
||||
'todos',
|
||||
'add',
|
||||
])
|
||||
})
|
||||
|
||||
it('should give mutationFn the same QueryClient instance via context', async () => {
|
||||
const key = queryKey()
|
||||
queryClient.setQueryData(key, 'tag-from-this-client')
|
||||
|
||||
const rendered = render(MutationFnContext, {
|
||||
props: { queryClient, queryKey: key },
|
||||
})
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /Mutate/i }))
|
||||
await vi.advanceTimersByTimeAsync(11)
|
||||
|
||||
expect(rendered.getByText('data: tag-from-this-client')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should let onSuccess invalidate queries via context.client without a useQueryClient() closure', async () => {
|
||||
const key = queryKey()
|
||||
queryClient.setQueryData(key, 'data')
|
||||
|
||||
const rendered = render(InvalidateFromContext, {
|
||||
props: { queryClient, queryKey: key },
|
||||
})
|
||||
|
||||
expect(queryClient.getQueryState(key)?.isInvalidated).toBe(false)
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /Mutate/i }))
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(queryClient.getQueryState(key)?.isInvalidated).toBe(true)
|
||||
})
|
||||
|
||||
it('should give a per-call onSuccess the same QueryClient instance via context', async () => {
|
||||
const perCallOnSuccess = vi.fn()
|
||||
|
||||
const rendered = render(PerCallSuccess, {
|
||||
props: { queryClient, perCallOnSuccess },
|
||||
})
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /Mutate/i }))
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(perCallOnSuccess).toHaveBeenCalledTimes(1)
|
||||
expect(perCallOnSuccess.mock.calls[0]?.[3].client).toBe(queryClient)
|
||||
})
|
||||
|
||||
it('should be able to run multiple mutateAsync calls in parallel with Promise.all', async () => {
|
||||
const rendered = render(ParallelMutateAsync, {
|
||||
props: { queryClient, strategy: 'all' },
|
||||
})
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /upload all/i }))
|
||||
await vi.advanceTimersByTimeAsync(11)
|
||||
|
||||
expect(
|
||||
rendered.getByText(
|
||||
'result: uploaded: file1, uploaded: file2, uploaded: file3',
|
||||
),
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle Promise.all rejection when one parallel mutateAsync call fails', async () => {
|
||||
const rendered = render(ParallelMutateAsync, {
|
||||
props: { queryClient, strategy: 'all', failOn: 'file2' },
|
||||
})
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /upload all/i }))
|
||||
await vi.advanceTimersByTimeAsync(11)
|
||||
|
||||
expect(
|
||||
rendered.getByText('result: error: upload failed'),
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle partial failure in parallel mutateAsync calls with Promise.allSettled', async () => {
|
||||
const rendered = render(ParallelMutateAsync, {
|
||||
props: { queryClient, strategy: 'allSettled', failOn: 'file2' },
|
||||
})
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /upload all/i }))
|
||||
await vi.advanceTimersByTimeAsync(11)
|
||||
|
||||
expect(
|
||||
rendered.getByText(
|
||||
'result: uploaded: file1, error: upload failed, uploaded: file3',
|
||||
),
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should only fire the per-call onSuccess for the last mutate() call', async () => {
|
||||
const onSuccessPerCall = vi.fn()
|
||||
|
||||
const rendered = render(ConcurrentMutate, {
|
||||
props: { queryClient, onSuccessPerCall },
|
||||
})
|
||||
|
||||
expect(rendered.getByText('data: null, status: idle')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate1/i }))
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate2/i }))
|
||||
|
||||
await vi.advanceTimersByTimeAsync(11)
|
||||
|
||||
expect(
|
||||
rendered.getByText('data: Todo 2, status: success'),
|
||||
).toBeInTheDocument()
|
||||
|
||||
expect(onSuccessPerCall).toHaveBeenCalledTimes(1)
|
||||
expect(onSuccessPerCall).toHaveBeenCalledWith(
|
||||
'Todo 2',
|
||||
'Todo 2',
|
||||
undefined,
|
||||
expect.anything(),
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,203 @@
|
||||
import { describe, expectTypeOf, it } from 'vitest'
|
||||
import { QueryClient } from '@tanstack/query-core'
|
||||
import { createMutation } from '../../src/index.js'
|
||||
import type {
|
||||
DefaultError,
|
||||
MutationFunctionContext,
|
||||
MutationKey,
|
||||
} from '@tanstack/query-core'
|
||||
import type { CreateMutationResult } from '../../src/types.js'
|
||||
|
||||
describe('createMutation', () => {
|
||||
it('should infer TData from mutationFn return type', () => {
|
||||
const mutation = createMutation(() => ({
|
||||
mutationFn: () => Promise.resolve('data'),
|
||||
}))
|
||||
|
||||
expectTypeOf(mutation.data).toEqualTypeOf<string | undefined>()
|
||||
expectTypeOf(mutation.error).toEqualTypeOf<DefaultError | null>()
|
||||
})
|
||||
|
||||
it('should infer TVariables from mutationFn parameter', () => {
|
||||
const mutation = createMutation(() => ({
|
||||
mutationFn: (vars: { id: string }) => Promise.resolve(vars.id),
|
||||
}))
|
||||
|
||||
expectTypeOf(mutation.mutate).toBeCallableWith({ id: '1' })
|
||||
expectTypeOf(mutation.data).toEqualTypeOf<string | undefined>()
|
||||
})
|
||||
|
||||
it('should infer TOnMutateResult from onMutate return type', () => {
|
||||
createMutation(() => ({
|
||||
mutationFn: () => Promise.resolve('data'),
|
||||
onMutate: () => {
|
||||
return { token: 'abc' }
|
||||
},
|
||||
onSuccess: (_data, _variables, onMutateResult) => {
|
||||
expectTypeOf(onMutateResult).toEqualTypeOf<{ token: string }>()
|
||||
},
|
||||
onError: (_error, _variables, onMutateResult) => {
|
||||
expectTypeOf(onMutateResult).toEqualTypeOf<
|
||||
{ token: string } | undefined
|
||||
>()
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
it('should allow explicit generic types', () => {
|
||||
const mutation = createMutation<string, Error, { id: number }>(() => ({
|
||||
mutationFn: (vars) => {
|
||||
expectTypeOf(vars).toEqualTypeOf<{ id: number }>()
|
||||
return Promise.resolve('result')
|
||||
},
|
||||
}))
|
||||
|
||||
expectTypeOf(mutation.data).toEqualTypeOf<string | undefined>()
|
||||
expectTypeOf(mutation.error).toEqualTypeOf<Error | null>()
|
||||
})
|
||||
|
||||
it('should return correct CreateMutationResult type', () => {
|
||||
const mutation = createMutation(() => ({
|
||||
mutationFn: () => Promise.resolve(42),
|
||||
}))
|
||||
|
||||
expectTypeOf(mutation).toEqualTypeOf<
|
||||
CreateMutationResult<number, DefaultError, void, unknown>
|
||||
>()
|
||||
})
|
||||
|
||||
it('should type mutateAsync with correct return type', () => {
|
||||
const mutation = createMutation(() => ({
|
||||
mutationFn: (id: string) => Promise.resolve(id.length),
|
||||
}))
|
||||
|
||||
expectTypeOf(mutation.mutateAsync).toBeCallableWith('test')
|
||||
expectTypeOf(mutation.mutateAsync('test')).toEqualTypeOf<Promise<number>>()
|
||||
})
|
||||
|
||||
it('should default TVariables to void when mutationFn has no parameters', () => {
|
||||
const mutation = createMutation(() => ({
|
||||
mutationFn: () => Promise.resolve('data'),
|
||||
}))
|
||||
|
||||
expectTypeOf(mutation.mutate).toBeCallableWith()
|
||||
})
|
||||
|
||||
it('should allow calling mutate with no arguments when TVariables is optional undefinable', () => {
|
||||
const mutation = createMutation(() => ({
|
||||
mutationFn: (_variables: number | undefined) => Promise.resolve(1),
|
||||
}))
|
||||
|
||||
expectTypeOf(mutation.mutate).toBeCallableWith()
|
||||
expectTypeOf(mutation.mutateAsync).toBeCallableWith()
|
||||
})
|
||||
|
||||
it('should infer custom TError type', () => {
|
||||
class CustomError extends Error {
|
||||
code: number
|
||||
constructor(code: number) {
|
||||
super()
|
||||
this.code = code
|
||||
}
|
||||
}
|
||||
|
||||
const mutation = createMutation<string, CustomError>(() => ({
|
||||
mutationFn: () => Promise.resolve('data'),
|
||||
}))
|
||||
|
||||
expectTypeOf(mutation.error).toEqualTypeOf<CustomError | null>()
|
||||
expectTypeOf(mutation.data).toEqualTypeOf<string | undefined>()
|
||||
})
|
||||
|
||||
it('should infer types for onSettled callback', () => {
|
||||
createMutation(() => ({
|
||||
mutationFn: () => Promise.resolve(42),
|
||||
onSettled: (data, error, _variables, _onMutateResult) => {
|
||||
expectTypeOf(data).toEqualTypeOf<number | undefined>()
|
||||
expectTypeOf(error).toEqualTypeOf<DefaultError | null>()
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
it('should infer custom TError in onError callback', () => {
|
||||
class CustomError extends Error {
|
||||
code: number
|
||||
constructor(code: number) {
|
||||
super()
|
||||
this.code = code
|
||||
}
|
||||
}
|
||||
|
||||
createMutation<string, CustomError>(() => ({
|
||||
mutationFn: () => Promise.resolve('data'),
|
||||
onError: (error) => {
|
||||
expectTypeOf(error).toEqualTypeOf<CustomError>()
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
it('should accept queryClient as second argument', () => {
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
const mutation = createMutation(
|
||||
() => ({
|
||||
mutationFn: () => Promise.resolve('data'),
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
expectTypeOf(mutation.data).toEqualTypeOf<string | undefined>()
|
||||
})
|
||||
|
||||
it('should type context as the last argument for mutationFn and every hook-level callback', () => {
|
||||
createMutation(() => ({
|
||||
mutationFn: (_variables, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
expectTypeOf(context.client).toEqualTypeOf<QueryClient>()
|
||||
return Promise.resolve('data')
|
||||
},
|
||||
onMutate: (_variables, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
},
|
||||
onSuccess: (_data, _variables, _onMutateResult, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
},
|
||||
onError: (_error, _variables, _onMutateResult, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
},
|
||||
onSettled: (_data, _error, _variables, _onMutateResult, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
it('should type context as the last argument for every per-call mutate option', () => {
|
||||
const mutation = createMutation(() => ({
|
||||
mutationFn: () => Promise.resolve('data'),
|
||||
}))
|
||||
|
||||
mutation.mutate(undefined, {
|
||||
onSuccess: (_data, _variables, _onMutateResult, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
},
|
||||
onError: (_error, _variables, _onMutateResult, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
},
|
||||
onSettled: (_data, _error, _variables, _onMutateResult, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should type context.mutationKey as MutationKey', () => {
|
||||
createMutation(() => ({
|
||||
mutationKey: ['todos', 'add'] as const,
|
||||
mutationFn: () => Promise.resolve('data'),
|
||||
onSuccess: (_data, _variables, _onMutateResult, context) => {
|
||||
expectTypeOf(context.mutationKey).toEqualTypeOf<
|
||||
MutationKey | undefined
|
||||
>()
|
||||
},
|
||||
}))
|
||||
})
|
||||
})
|
||||
36
packages/svelte-query/tests/createQueries/IsRestoring.svelte
Normal file
36
packages/svelte-query/tests/createQueries/IsRestoring.svelte
Normal file
@@ -0,0 +1,36 @@
|
||||
<script lang="ts">
|
||||
import type { QueryClient } from '@tanstack/query-core'
|
||||
import { queryKey } from '@tanstack/query-test-utils'
|
||||
import {
|
||||
setIsRestoringContext,
|
||||
setQueryClientContext,
|
||||
} from '../../src/context.js'
|
||||
import { createQueries } from '../../src/index.js'
|
||||
|
||||
type Props = {
|
||||
queryClient: QueryClient
|
||||
queryFn1: () => Promise<string>
|
||||
queryFn2: () => Promise<string>
|
||||
}
|
||||
|
||||
let { queryClient, queryFn1, queryFn2 }: Props = $props()
|
||||
|
||||
setQueryClientContext(queryClient)
|
||||
setIsRestoringContext({ current: true })
|
||||
|
||||
const result = createQueries(() => ({
|
||||
queries: [
|
||||
{ queryKey: queryKey(), queryFn: queryFn1 },
|
||||
{ queryKey: queryKey(), queryFn: queryFn2 },
|
||||
],
|
||||
}))
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div data-testid="status1">{result[0].status}</div>
|
||||
<div data-testid="status2">{result[1].status}</div>
|
||||
<div data-testid="fetchStatus1">{result[0].fetchStatus}</div>
|
||||
<div data-testid="fetchStatus2">{result[1].fetchStatus}</div>
|
||||
<div data-testid="data1">{result[0].data ?? 'undefined'}</div>
|
||||
<div data-testid="data2">{result[1].data ?? 'undefined'}</div>
|
||||
</div>
|
||||
@@ -0,0 +1,377 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { render } from '@testing-library/svelte'
|
||||
import { queryKey, sleep } from '@tanstack/query-test-utils'
|
||||
import { QueryClient, createQueries } from '../../src/index.js'
|
||||
import { promiseWithResolvers, ref, withEffectRoot } from '../utils.svelte.js'
|
||||
import IsRestoring from './IsRestoring.svelte'
|
||||
import type { CreateQueryResult } from '../../src/index.js'
|
||||
|
||||
describe('createQueries', () => {
|
||||
let queryClient: QueryClient
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
queryClient = new QueryClient()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
queryClient.clear()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it(
|
||||
'should return the correct states',
|
||||
withEffectRoot(async () => {
|
||||
const key1 = queryKey()
|
||||
const key2 = queryKey()
|
||||
const results: Array<Array<CreateQueryResult>> = []
|
||||
const { promise: promise1, resolve: resolve1 } = promiseWithResolvers()
|
||||
const { promise: promise2, resolve: resolve2 } = promiseWithResolvers()
|
||||
|
||||
const result = createQueries(
|
||||
() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => promise1,
|
||||
},
|
||||
{
|
||||
queryKey: key2,
|
||||
queryFn: () => promise2,
|
||||
},
|
||||
],
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
$effect(() => {
|
||||
results.push([{ ...result[0] }, { ...result[1] }])
|
||||
})
|
||||
|
||||
resolve1(1)
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(result[0].data).toBe(1)
|
||||
|
||||
resolve2(2)
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(result[1].data).toBe(2)
|
||||
|
||||
expect(results.length).toBe(3)
|
||||
expect(results[0]).toMatchObject([
|
||||
{ data: undefined },
|
||||
{ data: undefined },
|
||||
])
|
||||
expect(results[1]).toMatchObject([{ data: 1 }, { data: undefined }])
|
||||
expect(results[2]).toMatchObject([{ data: 1 }, { data: 2 }])
|
||||
}),
|
||||
)
|
||||
|
||||
it(
|
||||
'should track results',
|
||||
withEffectRoot(async () => {
|
||||
const key1 = queryKey()
|
||||
const results: Array<Array<CreateQueryResult>> = []
|
||||
let count = 0
|
||||
|
||||
const result = createQueries(
|
||||
() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => Promise.resolve(++count),
|
||||
},
|
||||
],
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
$effect(() => {
|
||||
results.push([result[0]])
|
||||
})
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(result[0].data).toBe(1)
|
||||
|
||||
expect(results.length).toBe(2)
|
||||
expect(results[0]).toMatchObject([{ data: undefined }])
|
||||
expect(results[1]).toMatchObject([{ data: 1 }])
|
||||
|
||||
// Trigger refetch
|
||||
result[0].refetch()
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(result[0].data).toBe(2)
|
||||
|
||||
// Only one render for data update, no render for isFetching transition
|
||||
expect(results.length).toBe(3)
|
||||
expect(results[2]).toMatchObject([{ data: 2 }])
|
||||
}),
|
||||
)
|
||||
|
||||
it(
|
||||
'should track queries added to an initially empty array',
|
||||
withEffectRoot(async () => {
|
||||
const key1 = queryKey()
|
||||
const queries = ref<
|
||||
Array<{ queryKey: Array<string>; queryFn: () => Promise<string> }>
|
||||
>([])
|
||||
const results: Array<Array<CreateQueryResult>> = []
|
||||
|
||||
const result = createQueries(
|
||||
() => ({ queries: queries.value }),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
$effect(() => {
|
||||
results.push(result.map((res) => ({ ...res })))
|
||||
})
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(results).toMatchObject([[]])
|
||||
|
||||
queries.value = [
|
||||
{ queryKey: key1, queryFn: () => sleep(10).then(() => 'data1') },
|
||||
]
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(result[0]?.data).toBe('data1')
|
||||
expect(results.at(-1)).toMatchObject([
|
||||
{ status: 'success', data: 'data1' },
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
it(
|
||||
'should combine queries',
|
||||
withEffectRoot(async () => {
|
||||
const key1 = queryKey()
|
||||
const key2 = queryKey()
|
||||
|
||||
const { promise: promise1, resolve: resolve1 } =
|
||||
promiseWithResolvers<string>()
|
||||
const { promise: promise2, resolve: resolve2 } =
|
||||
promiseWithResolvers<string>()
|
||||
|
||||
const queries = createQueries(
|
||||
() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => promise1,
|
||||
},
|
||||
{
|
||||
queryKey: key2,
|
||||
queryFn: () => promise2,
|
||||
},
|
||||
],
|
||||
combine: (results) => {
|
||||
return {
|
||||
combined: true,
|
||||
res: results
|
||||
.flatMap((res) => (res.data ? [res.data] : []))
|
||||
.join(','),
|
||||
}
|
||||
},
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
// Initially both queries are loading
|
||||
expect(queries).toEqual({
|
||||
combined: true,
|
||||
res: '',
|
||||
})
|
||||
|
||||
// Resolve the first query
|
||||
resolve1('first result')
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(queries.res).toBe('first result')
|
||||
|
||||
// Resolve the second query
|
||||
resolve2('second result')
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(queries.res).toBe('first result,second result')
|
||||
|
||||
expect(queries).toEqual({
|
||||
combined: true,
|
||||
res: 'first result,second result',
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it(
|
||||
'should track property access through combine function',
|
||||
withEffectRoot(async () => {
|
||||
const key1 = queryKey()
|
||||
const key2 = queryKey()
|
||||
let count = 0
|
||||
const results: Array<unknown> = []
|
||||
|
||||
const { promise: promise1, resolve: resolve1 } =
|
||||
promiseWithResolvers<string>()
|
||||
const { promise: promise2, resolve: resolve2 } =
|
||||
promiseWithResolvers<string>()
|
||||
const { promise: promise3, resolve: resolve3 } =
|
||||
promiseWithResolvers<string>()
|
||||
const { promise: promise4, resolve: resolve4 } =
|
||||
promiseWithResolvers<string>()
|
||||
|
||||
const queries = createQueries(
|
||||
() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => (count === 0 ? promise1 : promise3),
|
||||
},
|
||||
{
|
||||
queryKey: key2,
|
||||
queryFn: () => (count === 0 ? promise2 : promise4),
|
||||
},
|
||||
],
|
||||
combine: (queryResults) => {
|
||||
return {
|
||||
combined: true,
|
||||
refetch: () =>
|
||||
Promise.all(queryResults.map((res) => res.refetch())),
|
||||
res: queryResults
|
||||
.flatMap((res) => (res.data ? [res.data] : []))
|
||||
.join(','),
|
||||
}
|
||||
},
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
$effect(() => {
|
||||
results.push({ ...queries })
|
||||
})
|
||||
|
||||
// Initially both queries are loading
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(results[0]).toStrictEqual({
|
||||
combined: true,
|
||||
refetch: expect.any(Function),
|
||||
res: '',
|
||||
})
|
||||
|
||||
// Resolve the first query
|
||||
resolve1('first result ' + count)
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(queries.res).toBe('first result 0')
|
||||
|
||||
expect(results[1]).toStrictEqual({
|
||||
combined: true,
|
||||
refetch: expect.any(Function),
|
||||
res: 'first result 0',
|
||||
})
|
||||
|
||||
// Resolve the second query
|
||||
resolve2('second result ' + count)
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(queries.res).toBe('first result 0,second result 0')
|
||||
|
||||
expect(results[2]).toStrictEqual({
|
||||
combined: true,
|
||||
refetch: expect.any(Function),
|
||||
res: 'first result 0,second result 0',
|
||||
})
|
||||
|
||||
// Increment count and refetch
|
||||
count++
|
||||
queries.refetch()
|
||||
|
||||
// Resolve the refetched queries
|
||||
resolve3('first result ' + count)
|
||||
resolve4('second result ' + count)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(queries.res).toBe('first result 1,second result 1')
|
||||
|
||||
const length = results.length
|
||||
expect(results.at(-1)).toStrictEqual({
|
||||
combined: true,
|
||||
refetch: expect.any(Function),
|
||||
res: 'first result 1,second result 1',
|
||||
})
|
||||
|
||||
// Refetch again but with the same data
|
||||
await queries.refetch()
|
||||
|
||||
// No further re-render because data didn't change
|
||||
expect(results.length).toBe(length)
|
||||
}),
|
||||
)
|
||||
|
||||
it('should not fetch for the duration of the restoring period when isRestoring is true', async () => {
|
||||
const queryFn1 = vi.fn(() => sleep(10).then(() => 'data1'))
|
||||
const queryFn2 = vi.fn(() => sleep(10).then(() => 'data2'))
|
||||
|
||||
const rendered = render(IsRestoring, {
|
||||
props: { queryClient, queryFn1, queryFn2 },
|
||||
})
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(rendered.getByTestId('status1')).toHaveTextContent('pending')
|
||||
expect(rendered.getByTestId('status2')).toHaveTextContent('pending')
|
||||
expect(rendered.getByTestId('fetchStatus1')).toHaveTextContent('idle')
|
||||
expect(rendered.getByTestId('fetchStatus2')).toHaveTextContent('idle')
|
||||
expect(rendered.getByTestId('data1')).toHaveTextContent('undefined')
|
||||
expect(rendered.getByTestId('data2')).toHaveTextContent('undefined')
|
||||
expect(queryFn1).toHaveBeenCalledTimes(0)
|
||||
expect(queryFn2).toHaveBeenCalledTimes(0)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(rendered.getByTestId('status1')).toHaveTextContent('pending')
|
||||
expect(rendered.getByTestId('status2')).toHaveTextContent('pending')
|
||||
expect(rendered.getByTestId('fetchStatus1')).toHaveTextContent('idle')
|
||||
expect(rendered.getByTestId('fetchStatus2')).toHaveTextContent('idle')
|
||||
expect(rendered.getByTestId('data1')).toHaveTextContent('undefined')
|
||||
expect(rendered.getByTestId('data2')).toHaveTextContent('undefined')
|
||||
expect(queryFn1).toHaveBeenCalledTimes(0)
|
||||
expect(queryFn2).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
|
||||
it('should not fetch queries with different durations for the duration of the restoring period when isRestoring is true', async () => {
|
||||
const queryFn1 = vi.fn(() => sleep(10).then(() => 'data1'))
|
||||
const queryFn2 = vi.fn(() => sleep(20).then(() => 'data2'))
|
||||
|
||||
const rendered = render(IsRestoring, {
|
||||
props: { queryClient, queryFn1, queryFn2 },
|
||||
})
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(rendered.getByTestId('status1')).toHaveTextContent('pending')
|
||||
expect(rendered.getByTestId('status2')).toHaveTextContent('pending')
|
||||
expect(rendered.getByTestId('fetchStatus1')).toHaveTextContent('idle')
|
||||
expect(rendered.getByTestId('fetchStatus2')).toHaveTextContent('idle')
|
||||
expect(rendered.getByTestId('data1')).toHaveTextContent('undefined')
|
||||
expect(rendered.getByTestId('data2')).toHaveTextContent('undefined')
|
||||
expect(queryFn1).toHaveBeenCalledTimes(0)
|
||||
expect(queryFn2).toHaveBeenCalledTimes(0)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(rendered.getByTestId('status1')).toHaveTextContent('pending')
|
||||
expect(rendered.getByTestId('status2')).toHaveTextContent('pending')
|
||||
expect(rendered.getByTestId('fetchStatus1')).toHaveTextContent('idle')
|
||||
expect(rendered.getByTestId('fetchStatus2')).toHaveTextContent('idle')
|
||||
expect(rendered.getByTestId('data1')).toHaveTextContent('undefined')
|
||||
expect(rendered.getByTestId('data2')).toHaveTextContent('undefined')
|
||||
expect(queryFn1).toHaveBeenCalledTimes(0)
|
||||
expect(queryFn2).toHaveBeenCalledTimes(0)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(rendered.getByTestId('status1')).toHaveTextContent('pending')
|
||||
expect(rendered.getByTestId('status2')).toHaveTextContent('pending')
|
||||
expect(rendered.getByTestId('fetchStatus1')).toHaveTextContent('idle')
|
||||
expect(rendered.getByTestId('fetchStatus2')).toHaveTextContent('idle')
|
||||
expect(rendered.getByTestId('data1')).toHaveTextContent('undefined')
|
||||
expect(rendered.getByTestId('data2')).toHaveTextContent('undefined')
|
||||
expect(queryFn1).toHaveBeenCalledTimes(0)
|
||||
expect(queryFn2).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,684 @@
|
||||
import { describe, expectTypeOf, it } from 'vitest'
|
||||
import { QueryClient } from '@tanstack/query-core'
|
||||
import { queryKey } from '@tanstack/query-test-utils'
|
||||
import { createQueries, queryOptions } from '../../src/index.js'
|
||||
import type {
|
||||
CreateQueryOptions,
|
||||
CreateQueryResult,
|
||||
QueryFunction,
|
||||
QueryFunctionContext,
|
||||
QueryKey,
|
||||
skipToken,
|
||||
} from '../../src/index.js'
|
||||
|
||||
describe('createQueries', () => {
|
||||
it('should return correct data for dynamic queries with mixed result types', () => {
|
||||
const key1 = queryKey()
|
||||
const key2 = queryKey()
|
||||
const Queries1 = {
|
||||
get: () =>
|
||||
queryOptions({
|
||||
queryKey: key1,
|
||||
queryFn: () => Promise.resolve(1),
|
||||
}),
|
||||
}
|
||||
const Queries2 = {
|
||||
get: () =>
|
||||
queryOptions({
|
||||
queryKey: key2,
|
||||
queryFn: () => Promise.resolve(true),
|
||||
}),
|
||||
}
|
||||
|
||||
const queries1List = [1, 2, 3].map(() => ({ ...Queries1.get() }))
|
||||
const result = createQueries(() => ({
|
||||
queries: [...queries1List, { ...Queries2.get() }],
|
||||
}))
|
||||
|
||||
expectTypeOf(result).toEqualTypeOf<
|
||||
[
|
||||
...Array<CreateQueryResult<number, Error>>,
|
||||
CreateQueryResult<boolean, Error>,
|
||||
]
|
||||
>()
|
||||
})
|
||||
|
||||
it('should handle type parameter - tuple of tuples', () => {
|
||||
const queryClient = new QueryClient()
|
||||
const key1 = queryKey()
|
||||
const key2 = queryKey()
|
||||
const key3 = queryKey()
|
||||
|
||||
const result1 = createQueries<
|
||||
[[number], [string], [Array<string>, boolean]]
|
||||
>(
|
||||
() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => 1,
|
||||
},
|
||||
{
|
||||
queryKey: key2,
|
||||
queryFn: () => 'string',
|
||||
},
|
||||
{
|
||||
queryKey: key3,
|
||||
queryFn: () => ['string[]'],
|
||||
},
|
||||
],
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
expectTypeOf(result1[0]).toEqualTypeOf<CreateQueryResult<number, unknown>>()
|
||||
expectTypeOf(result1[1]).toEqualTypeOf<CreateQueryResult<string, unknown>>()
|
||||
expectTypeOf(result1[2]).toEqualTypeOf<
|
||||
CreateQueryResult<Array<string>, boolean>
|
||||
>()
|
||||
expectTypeOf(result1[0].data).toEqualTypeOf<number | undefined>()
|
||||
expectTypeOf(result1[1].data).toEqualTypeOf<string | undefined>()
|
||||
expectTypeOf(result1[2].data).toEqualTypeOf<Array<string> | undefined>()
|
||||
expectTypeOf(result1[2].error).toEqualTypeOf<boolean | null>()
|
||||
|
||||
// TData (3rd element) takes precedence over TQueryFnData (1st element)
|
||||
const result2 = createQueries<
|
||||
[[string, unknown, string], [string, unknown, number]]
|
||||
>(
|
||||
() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => 'string',
|
||||
select: (a) => {
|
||||
expectTypeOf(a).toEqualTypeOf<string>()
|
||||
return a.toLowerCase()
|
||||
},
|
||||
},
|
||||
{
|
||||
queryKey: key2,
|
||||
queryFn: () => 'string',
|
||||
select: (a) => {
|
||||
expectTypeOf(a).toEqualTypeOf<string>()
|
||||
return parseInt(a)
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
expectTypeOf(result2[0]).toEqualTypeOf<CreateQueryResult<string, unknown>>()
|
||||
expectTypeOf(result2[1]).toEqualTypeOf<CreateQueryResult<number, unknown>>()
|
||||
expectTypeOf(result2[0].data).toEqualTypeOf<string | undefined>()
|
||||
expectTypeOf(result2[1].data).toEqualTypeOf<number | undefined>()
|
||||
|
||||
// types should be enforced
|
||||
createQueries<[[string, unknown, string], [string, boolean, number]]>(
|
||||
() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => 'string',
|
||||
select: (a) => {
|
||||
expectTypeOf(a).toEqualTypeOf<string>()
|
||||
return a.toLowerCase()
|
||||
},
|
||||
placeholderData: 'string',
|
||||
// @ts-expect-error (initialData: string)
|
||||
initialData: 123,
|
||||
},
|
||||
{
|
||||
queryKey: key2,
|
||||
queryFn: () => 'string',
|
||||
select: (a) => {
|
||||
expectTypeOf(a).toEqualTypeOf<string>()
|
||||
return parseInt(a)
|
||||
},
|
||||
placeholderData: 'string',
|
||||
// @ts-expect-error (initialData: string)
|
||||
initialData: 123,
|
||||
},
|
||||
],
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
// field names should be enforced
|
||||
createQueries<[[string]]>(
|
||||
() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => 'string',
|
||||
},
|
||||
],
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle type parameter - tuple of objects', () => {
|
||||
const queryClient = new QueryClient()
|
||||
const key1 = queryKey()
|
||||
const key2 = queryKey()
|
||||
const key3 = queryKey()
|
||||
|
||||
const result1 = createQueries<
|
||||
[
|
||||
{ queryFnData: number },
|
||||
{ queryFnData: string },
|
||||
{ queryFnData: Array<string>; error: boolean },
|
||||
]
|
||||
>(
|
||||
() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => 1,
|
||||
},
|
||||
{
|
||||
queryKey: key2,
|
||||
queryFn: () => 'string',
|
||||
},
|
||||
{
|
||||
queryKey: key3,
|
||||
queryFn: () => ['string[]'],
|
||||
},
|
||||
],
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
expectTypeOf(result1[0]).toEqualTypeOf<CreateQueryResult<number, unknown>>()
|
||||
expectTypeOf(result1[1]).toEqualTypeOf<CreateQueryResult<string, unknown>>()
|
||||
expectTypeOf(result1[2]).toEqualTypeOf<
|
||||
CreateQueryResult<Array<string>, boolean>
|
||||
>()
|
||||
expectTypeOf(result1[0].data).toEqualTypeOf<number | undefined>()
|
||||
expectTypeOf(result1[1].data).toEqualTypeOf<string | undefined>()
|
||||
expectTypeOf(result1[2].data).toEqualTypeOf<Array<string> | undefined>()
|
||||
expectTypeOf(result1[2].error).toEqualTypeOf<boolean | null>()
|
||||
|
||||
// TData (data prop) takes precedence over TQueryFnData (queryFnData prop)
|
||||
const result2 = createQueries<
|
||||
[
|
||||
{ queryFnData: string; data: string },
|
||||
{ queryFnData: string; data: number },
|
||||
]
|
||||
>(
|
||||
() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => 'string',
|
||||
select: (a) => {
|
||||
expectTypeOf(a).toEqualTypeOf<string>()
|
||||
return a.toLowerCase()
|
||||
},
|
||||
},
|
||||
{
|
||||
queryKey: key2,
|
||||
queryFn: () => 'string',
|
||||
select: (a) => {
|
||||
expectTypeOf(a).toEqualTypeOf<string>()
|
||||
return parseInt(a)
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
expectTypeOf(result2[0]).toEqualTypeOf<CreateQueryResult<string, unknown>>()
|
||||
expectTypeOf(result2[1]).toEqualTypeOf<CreateQueryResult<number, unknown>>()
|
||||
expectTypeOf(result2[0].data).toEqualTypeOf<string | undefined>()
|
||||
expectTypeOf(result2[1].data).toEqualTypeOf<number | undefined>()
|
||||
|
||||
// can pass only TData (data prop) although TQueryFnData will be left unknown
|
||||
const result3 = createQueries<[{ data: string }, { data: number }]>(
|
||||
() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => 'string',
|
||||
select: (a) => {
|
||||
expectTypeOf(a).toEqualTypeOf<unknown>()
|
||||
return a as string
|
||||
},
|
||||
},
|
||||
{
|
||||
queryKey: key2,
|
||||
queryFn: () => 'string',
|
||||
select: (a) => {
|
||||
expectTypeOf(a).toEqualTypeOf<unknown>()
|
||||
return a as number
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
expectTypeOf(result3[0]).toEqualTypeOf<CreateQueryResult<string, unknown>>()
|
||||
expectTypeOf(result3[1]).toEqualTypeOf<CreateQueryResult<number, unknown>>()
|
||||
expectTypeOf(result3[0].data).toEqualTypeOf<string | undefined>()
|
||||
expectTypeOf(result3[1].data).toEqualTypeOf<number | undefined>()
|
||||
|
||||
// types should be enforced
|
||||
createQueries<
|
||||
[
|
||||
{ queryFnData: string; data: string },
|
||||
{ queryFnData: string; data: number; error: boolean },
|
||||
]
|
||||
>(
|
||||
() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => 'string',
|
||||
select: (a) => {
|
||||
expectTypeOf(a).toEqualTypeOf<string>()
|
||||
return a.toLowerCase()
|
||||
},
|
||||
placeholderData: 'string',
|
||||
// @ts-expect-error (initialData: string)
|
||||
initialData: 123,
|
||||
},
|
||||
{
|
||||
queryKey: key2,
|
||||
queryFn: () => 'string',
|
||||
select: (a) => {
|
||||
expectTypeOf(a).toEqualTypeOf<string>()
|
||||
return parseInt(a)
|
||||
},
|
||||
placeholderData: 'string',
|
||||
// @ts-expect-error (initialData: string)
|
||||
initialData: 123,
|
||||
},
|
||||
],
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
// field names should be enforced
|
||||
createQueries<[{ queryFnData: string }]>(
|
||||
() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => 'string',
|
||||
},
|
||||
],
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle array literal without type parameter to infer result type', () => {
|
||||
const queryClient = new QueryClient()
|
||||
const key1 = queryKey()
|
||||
const key2 = queryKey()
|
||||
const key3 = queryKey()
|
||||
const key4 = queryKey()
|
||||
|
||||
// Array.map preserves TQueryFnData
|
||||
const result1 = createQueries(
|
||||
() => ({
|
||||
queries: Array(50).map((_, i) => ({
|
||||
queryKey: ['key', i] as const,
|
||||
queryFn: () => i + 10,
|
||||
})),
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
expectTypeOf(result1).toEqualTypeOf<
|
||||
Array<CreateQueryResult<number, Error>>
|
||||
>()
|
||||
if (result1[0]) {
|
||||
expectTypeOf(result1[0].data).toEqualTypeOf<number | undefined>()
|
||||
}
|
||||
|
||||
// Array.map preserves TData
|
||||
const result2 = createQueries(
|
||||
() => ({
|
||||
queries: Array(50).map((_, i) => ({
|
||||
queryKey: ['key', i] as const,
|
||||
queryFn: () => i + 10,
|
||||
select: (data: number) => data.toString(),
|
||||
})),
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
expectTypeOf(result2).toEqualTypeOf<
|
||||
Array<CreateQueryResult<string, Error>>
|
||||
>()
|
||||
|
||||
const result3 = createQueries(
|
||||
() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => 1,
|
||||
},
|
||||
{
|
||||
queryKey: key2,
|
||||
queryFn: () => 'string',
|
||||
},
|
||||
{
|
||||
queryKey: key3,
|
||||
queryFn: () => ['string[]'],
|
||||
select: () => 123,
|
||||
},
|
||||
],
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
expectTypeOf(result3[0]).toEqualTypeOf<CreateQueryResult<number, Error>>()
|
||||
expectTypeOf(result3[1]).toEqualTypeOf<CreateQueryResult<string, Error>>()
|
||||
expectTypeOf(result3[2]).toEqualTypeOf<CreateQueryResult<number, Error>>()
|
||||
expectTypeOf(result3[0].data).toEqualTypeOf<number | undefined>()
|
||||
expectTypeOf(result3[1].data).toEqualTypeOf<string | undefined>()
|
||||
// select takes precedence over queryFn
|
||||
expectTypeOf(result3[2].data).toEqualTypeOf<number | undefined>()
|
||||
|
||||
// initialData/placeholderData are enforced
|
||||
createQueries(
|
||||
() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => 'string',
|
||||
placeholderData: 'string',
|
||||
// @ts-expect-error (initialData: string)
|
||||
initialData: 123,
|
||||
},
|
||||
{
|
||||
queryKey: key2,
|
||||
queryFn: () => 123,
|
||||
// @ts-expect-error (placeholderData: number)
|
||||
placeholderData: 'string',
|
||||
initialData: 123,
|
||||
},
|
||||
],
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
// select params are "indirectly" enforced
|
||||
createQueries(
|
||||
() => ({
|
||||
queries: [
|
||||
// unfortunately TS will not suggest the type for you
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => 'string',
|
||||
},
|
||||
// however you can add a type to the callback
|
||||
{
|
||||
queryKey: key2,
|
||||
queryFn: () => 'string',
|
||||
},
|
||||
// the type you do pass is enforced
|
||||
{
|
||||
queryKey: key3,
|
||||
queryFn: () => 'string',
|
||||
},
|
||||
{
|
||||
queryKey: key4,
|
||||
queryFn: () => 'string',
|
||||
select: (a: string) => parseInt(a),
|
||||
},
|
||||
],
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
// callbacks are also indirectly enforced with Array.map
|
||||
createQueries(
|
||||
() => ({
|
||||
queries: Array(50).map((_, i) => ({
|
||||
queryKey: ['key', i] as const,
|
||||
queryFn: () => i + 10,
|
||||
select: (data: number) => data.toString(),
|
||||
})),
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
// results inference works when all the handlers are defined
|
||||
const result4 = createQueries(
|
||||
() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => 'string',
|
||||
},
|
||||
{
|
||||
queryKey: key2,
|
||||
queryFn: () => 'string',
|
||||
},
|
||||
{
|
||||
queryKey: key4,
|
||||
queryFn: () => 'string',
|
||||
select: (a: string) => parseInt(a),
|
||||
},
|
||||
],
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
expectTypeOf(result4[0]).toEqualTypeOf<CreateQueryResult<string, Error>>()
|
||||
expectTypeOf(result4[1]).toEqualTypeOf<CreateQueryResult<string, Error>>()
|
||||
expectTypeOf(result4[2]).toEqualTypeOf<CreateQueryResult<number, Error>>()
|
||||
|
||||
// handles when queryFn returns a Promise
|
||||
const result5 = createQueries(
|
||||
() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => Promise.resolve('string'),
|
||||
},
|
||||
],
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
expectTypeOf(result5[0]).toEqualTypeOf<CreateQueryResult<string, Error>>()
|
||||
|
||||
// Array as const does not throw error
|
||||
const result6 = createQueries(
|
||||
() =>
|
||||
({
|
||||
queries: [
|
||||
{
|
||||
queryKey: ['key1'],
|
||||
queryFn: () => 'string',
|
||||
},
|
||||
{
|
||||
queryKey: ['key1'],
|
||||
queryFn: () => 123,
|
||||
},
|
||||
],
|
||||
}) as const,
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
expectTypeOf(result6[0]).toEqualTypeOf<CreateQueryResult<string, Error>>()
|
||||
expectTypeOf(result6[1]).toEqualTypeOf<CreateQueryResult<number, Error>>()
|
||||
|
||||
// field names should be enforced - array literal
|
||||
createQueries(
|
||||
() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () => 'string',
|
||||
},
|
||||
],
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
// field names should be enforced - Array.map() result
|
||||
createQueries(
|
||||
() => ({
|
||||
// @ts-expect-error (invalidField)
|
||||
queries: Array(10).map(() => ({
|
||||
someInvalidField: '',
|
||||
})),
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
// supports queryFn using fetch() to return Promise<any> - Array.map() result
|
||||
createQueries(
|
||||
() => ({
|
||||
queries: Array(50).map((_, i) => ({
|
||||
queryKey: ['key', i] as const,
|
||||
queryFn: () =>
|
||||
fetch('return Promise<any>').then((resp) => resp.json()),
|
||||
})),
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
// supports queryFn using fetch() to return Promise<any> - array literal
|
||||
createQueries(
|
||||
() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: key1,
|
||||
queryFn: () =>
|
||||
fetch('return Promise<any>').then((resp) => resp.json()),
|
||||
},
|
||||
],
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle strongly typed queryFn factories and createQueries wrappers', () => {
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
// QueryKey + queryFn factory
|
||||
type QueryKeyA = ['queryA']
|
||||
const getQueryKeyA = (): QueryKeyA => ['queryA']
|
||||
type GetQueryFunctionA = () => QueryFunction<number, QueryKeyA>
|
||||
const getQueryFunctionA: GetQueryFunctionA = () => () => {
|
||||
return 1
|
||||
}
|
||||
type SelectorA = (data: number) => [number, string]
|
||||
const getSelectorA = (): SelectorA => (data) => [data, data.toString()]
|
||||
|
||||
type QueryKeyB = ['queryB', string]
|
||||
const getQueryKeyB = (id: string): QueryKeyB => ['queryB', id]
|
||||
type GetQueryFunctionB = () => QueryFunction<string, QueryKeyB>
|
||||
const getQueryFunctionB: GetQueryFunctionB = () => () => {
|
||||
return '1'
|
||||
}
|
||||
type SelectorB = (data: string) => [string, number]
|
||||
const getSelectorB = (): SelectorB => (data) => [data, +data]
|
||||
|
||||
// Wrapper with strongly typed array-parameter
|
||||
function useWrappedQueries<
|
||||
TQueryFnData,
|
||||
TError,
|
||||
TData,
|
||||
TQueryKey extends QueryKey,
|
||||
>(
|
||||
queries: Array<
|
||||
CreateQueryOptions<TQueryFnData, TError, TData, TQueryKey>
|
||||
>,
|
||||
) {
|
||||
return createQueries(
|
||||
() => ({
|
||||
queries: queries.map(
|
||||
// no need to type the mapped query
|
||||
(query) => {
|
||||
const { queryFn: fn, queryKey: key } = query
|
||||
expectTypeOf(fn).toEqualTypeOf<
|
||||
| typeof skipToken
|
||||
| QueryFunction<TQueryFnData, TQueryKey, never>
|
||||
| undefined
|
||||
>()
|
||||
return {
|
||||
queryKey: key,
|
||||
queryFn: fn
|
||||
? (ctx: QueryFunctionContext<TQueryKey>) => {
|
||||
// eslint-disable-next-line vitest/valid-expect
|
||||
expectTypeOf<TQueryKey>(ctx.queryKey)
|
||||
return (
|
||||
fn as QueryFunction<TQueryFnData, TQueryKey>
|
||||
).call({}, ctx)
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
},
|
||||
),
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
}
|
||||
|
||||
const result = createQueries(
|
||||
() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: getQueryKeyA(),
|
||||
queryFn: getQueryFunctionA(),
|
||||
},
|
||||
{
|
||||
queryKey: getQueryKeyB('id'),
|
||||
queryFn: getQueryFunctionB(),
|
||||
},
|
||||
],
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
expectTypeOf(result[0]).toEqualTypeOf<CreateQueryResult<number, Error>>()
|
||||
expectTypeOf(result[1]).toEqualTypeOf<CreateQueryResult<string, Error>>()
|
||||
|
||||
const withSelector = createQueries(
|
||||
() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: getQueryKeyA(),
|
||||
queryFn: getQueryFunctionA(),
|
||||
select: getSelectorA(),
|
||||
},
|
||||
{
|
||||
queryKey: getQueryKeyB('id'),
|
||||
queryFn: getQueryFunctionB(),
|
||||
select: getSelectorB(),
|
||||
},
|
||||
],
|
||||
}),
|
||||
() => queryClient,
|
||||
)
|
||||
|
||||
expectTypeOf(withSelector[0]).toEqualTypeOf<
|
||||
CreateQueryResult<[number, string], Error>
|
||||
>()
|
||||
expectTypeOf(withSelector[1]).toEqualTypeOf<
|
||||
CreateQueryResult<[string, number], Error>
|
||||
>()
|
||||
|
||||
const withWrappedQueries = useWrappedQueries(
|
||||
Array(10).map(() => ({
|
||||
queryKey: getQueryKeyA(),
|
||||
queryFn: getQueryFunctionA(),
|
||||
select: getSelectorA(),
|
||||
})),
|
||||
)
|
||||
|
||||
expectTypeOf(withWrappedQueries).toEqualTypeOf<
|
||||
Array<CreateQueryResult<number, Error>>
|
||||
>()
|
||||
})
|
||||
})
|
||||
41
packages/svelte-query/tests/createQuery/Base.svelte
Normal file
41
packages/svelte-query/tests/createQuery/Base.svelte
Normal file
@@ -0,0 +1,41 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import type { QueryClient } from '@tanstack/query-core'
|
||||
import { createQuery, setQueryClientContext } from '../../src/index.js'
|
||||
import type { Accessor, CreateQueryOptions } from '../../src/index.js'
|
||||
|
||||
type Props = {
|
||||
queryClient: QueryClient
|
||||
options: Accessor<CreateQueryOptions>
|
||||
}
|
||||
|
||||
let { queryClient, options }: Props = $props()
|
||||
|
||||
setQueryClientContext(queryClient)
|
||||
|
||||
// QueryClientProvider normally mounts/unmounts the client; since this fixture
|
||||
// sets the context directly, do it manually so focus/online refetching works
|
||||
onMount(() => queryClient.mount())
|
||||
onDestroy(() => queryClient.unmount())
|
||||
|
||||
const query = createQuery(options)
|
||||
</script>
|
||||
|
||||
<button onclick={() => query.refetch()}>refetch</button>
|
||||
<button onclick={() => query.refetch({ cancelRefetch: false })}>
|
||||
refetch no cancel
|
||||
</button>
|
||||
|
||||
<div data-testid="status">{query.status}</div>
|
||||
<div data-testid="fetchStatus">{query.fetchStatus}</div>
|
||||
<div data-testid="data">{query.data ?? 'undefined'}</div>
|
||||
<div data-testid="dataUpdatedAt">{query.dataUpdatedAt}</div>
|
||||
<div data-testid="error">
|
||||
{(query.error as Error | null)?.message ?? 'null'}
|
||||
</div>
|
||||
<div data-testid="isFetched">{query.isFetched}</div>
|
||||
<div data-testid="isFetchedAfterMount">{query.isFetchedAfterMount}</div>
|
||||
<div data-testid="isStale">{query.isStale}</div>
|
||||
<div data-testid="isFetching">{query.isFetching}</div>
|
||||
<div data-testid="isSuccess">{query.isSuccess}</div>
|
||||
<div data-testid="isPlaceholderData">{query.isPlaceholderData}</div>
|
||||
29
packages/svelte-query/tests/createQuery/Counter.svelte
Normal file
29
packages/svelte-query/tests/createQuery/Counter.svelte
Normal file
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import type { QueryClient } from '@tanstack/query-core'
|
||||
import { createQuery, setQueryClientContext } from '../../src/index.js'
|
||||
import type { CreateQueryOptions } from '../../src/index.js'
|
||||
|
||||
type Props = {
|
||||
queryClient: QueryClient
|
||||
startCount?: number
|
||||
options: (count: number) => CreateQueryOptions<number, Error>
|
||||
}
|
||||
|
||||
let { queryClient, startCount = 0, options }: Props = $props()
|
||||
|
||||
let count = $state(startCount)
|
||||
|
||||
setQueryClientContext(queryClient)
|
||||
|
||||
const query = createQuery<number, Error>(() => options(count))
|
||||
</script>
|
||||
|
||||
<button onclick={() => count++}>increment</button>
|
||||
<button onclick={() => query.refetch()}>refetch</button>
|
||||
|
||||
<div data-testid="data">{query.data ?? 'undefined'}</div>
|
||||
<div data-testid="status">{query.status}</div>
|
||||
<div data-testid="fetchStatus">{query.fetchStatus}</div>
|
||||
<div data-testid="isFetching">{query.isFetching}</div>
|
||||
<div data-testid="isSuccess">{query.isSuccess}</div>
|
||||
<div data-testid="isPlaceholderData">{query.isPlaceholderData}</div>
|
||||
30
packages/svelte-query/tests/createQuery/IsRestoring.svelte
Normal file
30
packages/svelte-query/tests/createQuery/IsRestoring.svelte
Normal file
@@ -0,0 +1,30 @@
|
||||
<script lang="ts">
|
||||
import type { QueryClient } from '@tanstack/query-core'
|
||||
import { queryKey } from '@tanstack/query-test-utils'
|
||||
import {
|
||||
setIsRestoringContext,
|
||||
setQueryClientContext,
|
||||
} from '../../src/context.js'
|
||||
import { createQuery } from '../../src/index.js'
|
||||
|
||||
type Props = {
|
||||
queryClient: QueryClient
|
||||
queryFn: () => Promise<string>
|
||||
}
|
||||
|
||||
let { queryClient, queryFn }: Props = $props()
|
||||
|
||||
setQueryClientContext(queryClient)
|
||||
setIsRestoringContext({ current: true })
|
||||
|
||||
const query = createQuery(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn,
|
||||
}))
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div data-testid="status">{query.status}</div>
|
||||
<div data-testid="fetchStatus">{query.fetchStatus}</div>
|
||||
<div data-testid="data">{query.data ?? 'undefined'}</div>
|
||||
</div>
|
||||
24
packages/svelte-query/tests/createQuery/Select.svelte
Normal file
24
packages/svelte-query/tests/createQuery/Select.svelte
Normal file
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import type { QueryClient } from '@tanstack/query-core'
|
||||
import { createQuery, setQueryClientContext } from '../../src/index.js'
|
||||
import type { Accessor, CreateQueryOptions } from '../../src/index.js'
|
||||
|
||||
type Data = { name: string }
|
||||
|
||||
type Props = {
|
||||
queryClient: QueryClient
|
||||
options: Accessor<CreateQueryOptions<Data, Error, string>>
|
||||
}
|
||||
|
||||
let { queryClient, options }: Props = $props()
|
||||
|
||||
setQueryClientContext(queryClient)
|
||||
|
||||
const query = createQuery<Data, Error, string>(options)
|
||||
</script>
|
||||
|
||||
<div data-testid="status">{query.status}</div>
|
||||
<div data-testid="data">{query.data ?? 'undefined'}</div>
|
||||
<div data-testid="error">
|
||||
{(query.error as Error | null)?.message ?? 'null'}
|
||||
</div>
|
||||
34
packages/svelte-query/tests/createQuery/TwoQueries.svelte
Normal file
34
packages/svelte-query/tests/createQuery/TwoQueries.svelte
Normal file
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import type { QueryClient } from '@tanstack/query-core'
|
||||
import { createQuery, setQueryClientContext } from '../../src/index.js'
|
||||
import type { Accessor, CreateQueryOptions } from '../../src/index.js'
|
||||
|
||||
type Props = {
|
||||
queryClient: QueryClient
|
||||
options1: Accessor<CreateQueryOptions>
|
||||
options2: Accessor<CreateQueryOptions>
|
||||
}
|
||||
|
||||
let { queryClient, options1, options2 }: Props = $props()
|
||||
|
||||
setQueryClientContext(queryClient)
|
||||
|
||||
onMount(() => queryClient.mount())
|
||||
onDestroy(() => queryClient.unmount())
|
||||
|
||||
const query1 = createQuery(options1)
|
||||
const query2 = createQuery(options2)
|
||||
</script>
|
||||
|
||||
<button onclick={() => query1.refetch()}>refetch1</button>
|
||||
|
||||
<div data-testid="status1">{query1.status}</div>
|
||||
<div data-testid="fetchStatus1">{query1.fetchStatus}</div>
|
||||
<div data-testid="data1">{query1.data ?? 'undefined'}</div>
|
||||
<div data-testid="isStale1">{query1.isStale}</div>
|
||||
|
||||
<div data-testid="status2">{query2.status}</div>
|
||||
<div data-testid="fetchStatus2">{query2.fetchStatus}</div>
|
||||
<div data-testid="data2">{query2.data ?? 'undefined'}</div>
|
||||
<div data-testid="isStale2">{query2.isStale}</div>
|
||||
1653
packages/svelte-query/tests/createQuery/createQuery.svelte.test.ts
Normal file
1653
packages/svelte-query/tests/createQuery/createQuery.svelte.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
136
packages/svelte-query/tests/createQuery/createQuery.test-d.ts
Normal file
136
packages/svelte-query/tests/createQuery/createQuery.test-d.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import { describe, expectTypeOf, it } from 'vitest'
|
||||
import { queryKey } from '@tanstack/query-test-utils'
|
||||
import { createQuery, queryOptions } from '../../src/index.js'
|
||||
|
||||
describe('createQuery', () => {
|
||||
it('should return the correct states for a successful query', () => {
|
||||
const key = queryKey()
|
||||
const query = createQuery<string, Error>(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () => Promise.resolve('test'),
|
||||
}))
|
||||
|
||||
if (query.isPending) {
|
||||
expectTypeOf(query.data).toEqualTypeOf<undefined>()
|
||||
expectTypeOf(query.error).toEqualTypeOf<null>()
|
||||
} else if (query.isLoadingError) {
|
||||
expectTypeOf(query.data).toEqualTypeOf<undefined>()
|
||||
expectTypeOf(query.error).toEqualTypeOf<Error>()
|
||||
} else {
|
||||
expectTypeOf(query.data).toEqualTypeOf<string>()
|
||||
expectTypeOf(query.error).toEqualTypeOf<Error | null>()
|
||||
}
|
||||
})
|
||||
|
||||
describe('initialData', () => {
|
||||
describe('Config object overload', () => {
|
||||
it('TData should always be defined when initialData is provided as an object', () => {
|
||||
const key = queryKey()
|
||||
const { data } = createQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () => ({ wow: true }),
|
||||
initialData: { wow: true },
|
||||
}))
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>()
|
||||
})
|
||||
|
||||
it('TData should be defined when passed through queryOptions', () => {
|
||||
const key = queryKey()
|
||||
const options = queryOptions({
|
||||
queryKey: key,
|
||||
queryFn: () => ({ wow: true }),
|
||||
initialData: { wow: true },
|
||||
})
|
||||
const { data } = createQuery(() => options)
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>()
|
||||
})
|
||||
|
||||
it('TData should have undefined in the union when initialData is NOT provided', () => {
|
||||
const key = queryKey()
|
||||
const { data } = createQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () => ({ wow: true }),
|
||||
}))
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>()
|
||||
})
|
||||
|
||||
it('TData should have undefined in the union when initialData is provided as a function which can return undefined', () => {
|
||||
const key = queryKey()
|
||||
const { data } = createQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () => ({ wow: true }),
|
||||
initialData: () => undefined as { wow: boolean } | undefined,
|
||||
}))
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Query key overload', () => {
|
||||
it('TData should always be defined when initialData is provided', () => {
|
||||
const key = queryKey()
|
||||
const { data } = createQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () => ({ wow: true }),
|
||||
initialData: { wow: true },
|
||||
}))
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>()
|
||||
})
|
||||
|
||||
it('TData should have undefined in the union when initialData is NOT provided', () => {
|
||||
const key = queryKey()
|
||||
const { data } = createQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () => ({ wow: true }),
|
||||
}))
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Query key and func', () => {
|
||||
it('TData should always be defined when initialData is provided', () => {
|
||||
const key = queryKey()
|
||||
const { data } = createQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () => ({ wow: true }),
|
||||
initialData: { wow: true },
|
||||
}))
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>()
|
||||
})
|
||||
|
||||
it('TData should have undefined in the union when initialData is NOT provided', () => {
|
||||
const key = queryKey()
|
||||
const { data } = createQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () => ({ wow: true }),
|
||||
}))
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('generic queryFn', () => {
|
||||
it('should infer the result type from a generic query function', () => {
|
||||
const key = queryKey()
|
||||
|
||||
function queryFn<T = string>(): Promise<T> {
|
||||
return Promise.resolve({} as T)
|
||||
}
|
||||
|
||||
const query = createQuery(() => ({
|
||||
queryKey: key,
|
||||
queryFn: () => queryFn(),
|
||||
}))
|
||||
|
||||
expectTypeOf(query.data).toEqualTypeOf<string | undefined>()
|
||||
expectTypeOf(query.error).toEqualTypeOf<Error | null>()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,16 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { infiniteQueryOptions } from '../src/index.js'
|
||||
import type { CreateInfiniteQueryOptions } from '../src/types.js'
|
||||
|
||||
describe('infiniteQueryOptions', () => {
|
||||
it('should return the object received as a parameter without any modification.', () => {
|
||||
const object: CreateInfiniteQueryOptions = {
|
||||
queryKey: ['key'],
|
||||
queryFn: () => Promise.resolve(5),
|
||||
getNextPageParam: () => null,
|
||||
initialPageParam: null,
|
||||
} as const
|
||||
|
||||
expect(infiniteQueryOptions(object)).toBe(object)
|
||||
})
|
||||
})
|
||||
138
packages/svelte-query/tests/infiniteQueryOptions.test-d.ts
Normal file
138
packages/svelte-query/tests/infiniteQueryOptions.test-d.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { assertType, describe, expectTypeOf, it } from 'vitest'
|
||||
import { QueryClient, skipToken } from '@tanstack/query-core'
|
||||
import { queryKey } from '@tanstack/query-test-utils'
|
||||
import { createInfiniteQuery, infiniteQueryOptions } from '../src/index.js'
|
||||
import type { InfiniteData } from '@tanstack/query-core'
|
||||
|
||||
describe('infiniteQueryOptions', () => {
|
||||
it('should not allow excess properties', () => {
|
||||
const key = queryKey()
|
||||
assertType(
|
||||
infiniteQueryOptions({
|
||||
queryKey: key,
|
||||
queryFn: () => Promise.resolve('data'),
|
||||
getNextPageParam: () => 1,
|
||||
initialPageParam: 1,
|
||||
// @ts-expect-error this is a good error, because stallTime does not exist!
|
||||
stallTime: 1000,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should infer types for callbacks', () => {
|
||||
const key = queryKey()
|
||||
infiniteQueryOptions({
|
||||
queryKey: key,
|
||||
queryFn: () => Promise.resolve('data'),
|
||||
staleTime: 1000,
|
||||
getNextPageParam: () => 1,
|
||||
initialPageParam: 1,
|
||||
select: (data) => {
|
||||
expectTypeOf(data).toEqualTypeOf<InfiniteData<string, number>>()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should work when passed to createInfiniteQuery', () => {
|
||||
const key = queryKey()
|
||||
const options = infiniteQueryOptions({
|
||||
queryKey: key,
|
||||
queryFn: () => Promise.resolve('string'),
|
||||
getNextPageParam: () => 1,
|
||||
initialPageParam: 1,
|
||||
})
|
||||
|
||||
const query = createInfiniteQuery(() => options)
|
||||
|
||||
// known issue: type of pageParams is unknown when returned from useInfiniteQuery
|
||||
expectTypeOf(query.data).toEqualTypeOf<
|
||||
InfiniteData<string, unknown> | undefined
|
||||
>()
|
||||
})
|
||||
|
||||
it('should work when passed to createInfiniteQuery with initialData', () => {
|
||||
const key = queryKey()
|
||||
const options = infiniteQueryOptions({
|
||||
queryKey: key,
|
||||
queryFn: () => Promise.resolve('string'),
|
||||
getNextPageParam: () => 1,
|
||||
initialPageParam: 1,
|
||||
initialData: { pages: ['string'], pageParams: [1] },
|
||||
})
|
||||
|
||||
const query = createInfiniteQuery(() => options)
|
||||
|
||||
// known issue: type of pageParams is unknown when returned from createInfiniteQuery
|
||||
expectTypeOf(query.data).toEqualTypeOf<InfiniteData<string, unknown>>()
|
||||
})
|
||||
|
||||
it('should work when passed to infiniteQuery', async () => {
|
||||
const options = infiniteQueryOptions({
|
||||
queryKey: ['key'],
|
||||
queryFn: () => Promise.resolve('string'),
|
||||
getNextPageParam: () => 1,
|
||||
initialPageParam: 1,
|
||||
})
|
||||
|
||||
const data = await new QueryClient().infiniteQuery(options)
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<InfiniteData<string, number>>()
|
||||
})
|
||||
|
||||
it('should work when passed to infiniteQuery with select', async () => {
|
||||
const options = infiniteQueryOptions({
|
||||
queryKey: ['key'],
|
||||
queryFn: () => Promise.resolve('string'),
|
||||
getNextPageParam: () => 1,
|
||||
initialPageParam: 1,
|
||||
select: (data) => data.pages,
|
||||
})
|
||||
|
||||
const data = await new QueryClient().infiniteQuery(options)
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<Array<string>>()
|
||||
})
|
||||
|
||||
it('should work when passed to infiniteQuery with enabled: false', async () => {
|
||||
const options = infiniteQueryOptions({
|
||||
queryKey: ['key'],
|
||||
queryFn: () => Promise.resolve('string'),
|
||||
getNextPageParam: () => 1,
|
||||
initialPageParam: 1,
|
||||
enabled: false,
|
||||
})
|
||||
|
||||
const data = await new QueryClient().infiniteQuery(options)
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<InfiniteData<string, number>>()
|
||||
})
|
||||
|
||||
it('should work when passed to infiniteQuery with skipToken', async () => {
|
||||
const options = infiniteQueryOptions({
|
||||
queryKey: ['key'],
|
||||
queryFn: skipToken,
|
||||
getNextPageParam: () => 1,
|
||||
initialPageParam: 1,
|
||||
})
|
||||
|
||||
const data = await new QueryClient().infiniteQuery(options)
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<InfiniteData<unknown, number>>()
|
||||
})
|
||||
|
||||
it('should work when passed to fetchInfiniteQuery', async () => {
|
||||
const key = queryKey()
|
||||
const options = infiniteQueryOptions({
|
||||
queryKey: key,
|
||||
queryFn: () => Promise.resolve('string'),
|
||||
getNextPageParam: () => 1,
|
||||
initialPageParam: 1,
|
||||
})
|
||||
|
||||
// grandfathered direct test
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const data = await new QueryClient().fetchInfiniteQuery(options)
|
||||
|
||||
expectTypeOf(data).toEqualTypeOf<InfiniteData<string, number>>()
|
||||
})
|
||||
})
|
||||
60
packages/svelte-query/tests/mutationOptions/Base.svelte
Normal file
60
packages/svelte-query/tests/mutationOptions/Base.svelte
Normal file
@@ -0,0 +1,60 @@
|
||||
<script lang="ts">
|
||||
import type { QueryClient } from '@tanstack/query-core'
|
||||
import {
|
||||
createMutation,
|
||||
setQueryClientContext,
|
||||
useIsMutating,
|
||||
useMutationState,
|
||||
} from '../../src/index.js'
|
||||
import type {
|
||||
Accessor,
|
||||
CreateMutationOptions,
|
||||
MutationStateOptions,
|
||||
} from '../../src/index.js'
|
||||
import type { MutationFilters } from '@tanstack/query-core'
|
||||
|
||||
type Props = {
|
||||
queryClient: QueryClient
|
||||
mutationOpts: Accessor<CreateMutationOptions<string>>
|
||||
isMutatingFilters?: MutationFilters
|
||||
mutationStateOpts?: MutationStateOptions
|
||||
}
|
||||
|
||||
let {
|
||||
queryClient,
|
||||
mutationOpts,
|
||||
isMutatingFilters,
|
||||
mutationStateOpts,
|
||||
}: Props = $props()
|
||||
|
||||
setQueryClientContext(queryClient)
|
||||
|
||||
const mutation = createMutation(mutationOpts)
|
||||
const isMutating = useIsMutating(isMutatingFilters)
|
||||
const mutationState = useMutationState(mutationStateOpts)
|
||||
|
||||
let clientIsMutating = $state(0)
|
||||
|
||||
$effect(() => {
|
||||
const mutationCache = queryClient.getMutationCache()
|
||||
clientIsMutating = isMutatingFilters
|
||||
? queryClient.isMutating(isMutatingFilters)
|
||||
: queryClient.isMutating()
|
||||
|
||||
const unsubscribe = mutationCache.subscribe(() => {
|
||||
clientIsMutating = isMutatingFilters
|
||||
? queryClient.isMutating(isMutatingFilters)
|
||||
: queryClient.isMutating()
|
||||
})
|
||||
|
||||
return unsubscribe
|
||||
})
|
||||
</script>
|
||||
|
||||
<button onclick={() => mutation.mutate()}>mutate</button>
|
||||
|
||||
<div>isMutating: {isMutating.current}</div>
|
||||
<div>clientIsMutating: {clientIsMutating}</div>
|
||||
<div>
|
||||
mutationState: {JSON.stringify(mutationState.map((state) => state.data))}
|
||||
</div>
|
||||
64
packages/svelte-query/tests/mutationOptions/Multi.svelte
Normal file
64
packages/svelte-query/tests/mutationOptions/Multi.svelte
Normal file
@@ -0,0 +1,64 @@
|
||||
<script lang="ts">
|
||||
import type { QueryClient } from '@tanstack/query-core'
|
||||
import {
|
||||
createMutation,
|
||||
setQueryClientContext,
|
||||
useIsMutating,
|
||||
useMutationState,
|
||||
} from '../../src/index.js'
|
||||
import type {
|
||||
Accessor,
|
||||
CreateMutationOptions,
|
||||
MutationStateOptions,
|
||||
} from '../../src/index.js'
|
||||
import type { MutationFilters } from '@tanstack/query-core'
|
||||
|
||||
type Props = {
|
||||
queryClient: QueryClient
|
||||
mutationOpts1: Accessor<CreateMutationOptions<string>>
|
||||
mutationOpts2: Accessor<CreateMutationOptions<string>>
|
||||
isMutatingFilters?: MutationFilters
|
||||
mutationStateOpts?: MutationStateOptions
|
||||
}
|
||||
|
||||
let {
|
||||
queryClient,
|
||||
mutationOpts1,
|
||||
mutationOpts2,
|
||||
isMutatingFilters,
|
||||
mutationStateOpts,
|
||||
}: Props = $props()
|
||||
|
||||
setQueryClientContext(queryClient)
|
||||
|
||||
const mutation1 = createMutation(mutationOpts1)
|
||||
const mutation2 = createMutation(mutationOpts2)
|
||||
const isMutating = useIsMutating(isMutatingFilters)
|
||||
const mutationState = useMutationState(mutationStateOpts)
|
||||
|
||||
let clientIsMutating = $state(0)
|
||||
|
||||
$effect(() => {
|
||||
const mutationCache = queryClient.getMutationCache()
|
||||
clientIsMutating = isMutatingFilters
|
||||
? queryClient.isMutating(isMutatingFilters)
|
||||
: queryClient.isMutating()
|
||||
|
||||
const unsubscribe = mutationCache.subscribe(() => {
|
||||
clientIsMutating = isMutatingFilters
|
||||
? queryClient.isMutating(isMutatingFilters)
|
||||
: queryClient.isMutating()
|
||||
})
|
||||
|
||||
return unsubscribe
|
||||
})
|
||||
</script>
|
||||
|
||||
<button onclick={() => mutation1.mutate()}>mutate1</button>
|
||||
<button onclick={() => mutation2.mutate()}>mutate2</button>
|
||||
|
||||
<div>isMutating: {isMutating.current}</div>
|
||||
<div>clientIsMutating: {clientIsMutating}</div>
|
||||
<div>
|
||||
mutationState: {JSON.stringify(mutationState.map((state) => state.data))}
|
||||
</div>
|
||||
@@ -0,0 +1,344 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { fireEvent, render } from '@testing-library/svelte'
|
||||
import { QueryClient } from '@tanstack/query-core'
|
||||
import { queryKey, sleep } from '@tanstack/query-test-utils'
|
||||
import { mutationOptions } from '../../src/index.js'
|
||||
import Base from './Base.svelte'
|
||||
import Multi from './Multi.svelte'
|
||||
|
||||
describe('mutationOptions', () => {
|
||||
let queryClient: QueryClient
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
queryClient = new QueryClient()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
queryClient.clear()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('should return the object received as a parameter without any modification (with mutationKey in mutationOptions)', () => {
|
||||
const object = {
|
||||
mutationKey: ['key'],
|
||||
mutationFn: () => sleep(10).then(() => 5),
|
||||
} as const
|
||||
|
||||
expect(mutationOptions(object)).toBe(object)
|
||||
})
|
||||
|
||||
it('should return the object received as a parameter without any modification (without mutationKey in mutationOptions)', () => {
|
||||
const object = {
|
||||
mutationFn: () => sleep(10).then(() => 5),
|
||||
} as const
|
||||
|
||||
expect(mutationOptions(object)).toBe(object)
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with useIsMutating (with mutationKey in mutationOptions)', async () => {
|
||||
const key = queryKey()
|
||||
const mutationOpts = mutationOptions({
|
||||
mutationKey: key,
|
||||
mutationFn: () => sleep(50).then(() => 'data'),
|
||||
})
|
||||
|
||||
const rendered = render(Base, {
|
||||
props: { queryClient, mutationOpts: () => mutationOpts },
|
||||
})
|
||||
|
||||
expect(rendered.getByText('isMutating: 0')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(rendered.getByText('isMutating: 1')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(50)
|
||||
expect(rendered.getByText('isMutating: 0')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with useIsMutating (without mutationKey in mutationOptions)', async () => {
|
||||
const mutationOpts = mutationOptions({
|
||||
mutationFn: () => sleep(50).then(() => 'data'),
|
||||
})
|
||||
|
||||
const rendered = render(Base, {
|
||||
props: { queryClient, mutationOpts: () => mutationOpts },
|
||||
})
|
||||
|
||||
expect(rendered.getByText('isMutating: 0')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(rendered.getByText('isMutating: 1')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(50)
|
||||
expect(rendered.getByText('isMutating: 0')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with useIsMutating', async () => {
|
||||
const key = queryKey()
|
||||
const mutationOpts1 = mutationOptions({
|
||||
mutationKey: key,
|
||||
mutationFn: () => sleep(50).then(() => 'data1'),
|
||||
})
|
||||
const mutationOpts2 = mutationOptions({
|
||||
mutationFn: () => sleep(50).then(() => 'data2'),
|
||||
})
|
||||
|
||||
const rendered = render(Multi, {
|
||||
props: {
|
||||
queryClient,
|
||||
mutationOpts1: () => mutationOpts1,
|
||||
mutationOpts2: () => mutationOpts2,
|
||||
},
|
||||
})
|
||||
|
||||
expect(rendered.getByText('isMutating: 0')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate1/i }))
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate2/i }))
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(rendered.getByText('isMutating: 2')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(50)
|
||||
expect(rendered.getByText('isMutating: 0')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with useIsMutating (filter mutationOpts1.mutationKey)', async () => {
|
||||
const key = queryKey()
|
||||
const mutationOpts1 = mutationOptions({
|
||||
mutationKey: key,
|
||||
mutationFn: () => sleep(50).then(() => 'data1'),
|
||||
})
|
||||
const mutationOpts2 = mutationOptions({
|
||||
mutationFn: () => sleep(50).then(() => 'data2'),
|
||||
})
|
||||
|
||||
const rendered = render(Multi, {
|
||||
props: {
|
||||
queryClient,
|
||||
mutationOpts1: () => mutationOpts1,
|
||||
mutationOpts2: () => mutationOpts2,
|
||||
isMutatingFilters: { mutationKey: mutationOpts1.mutationKey },
|
||||
},
|
||||
})
|
||||
|
||||
expect(rendered.getByText('isMutating: 0')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate1/i }))
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate2/i }))
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(rendered.getByText('isMutating: 1')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(50)
|
||||
expect(rendered.getByText('isMutating: 0')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with queryClient.isMutating (with mutationKey in mutationOptions)', async () => {
|
||||
const key = queryKey()
|
||||
const mutationOpts = mutationOptions({
|
||||
mutationKey: key,
|
||||
mutationFn: () => sleep(500).then(() => 'data'),
|
||||
})
|
||||
|
||||
const rendered = render(Base, {
|
||||
props: {
|
||||
queryClient,
|
||||
mutationOpts: () => mutationOpts,
|
||||
isMutatingFilters: { mutationKey: mutationOpts.mutationKey },
|
||||
},
|
||||
})
|
||||
|
||||
expect(rendered.getByText('clientIsMutating: 0')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(rendered.getByText('clientIsMutating: 1')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(500)
|
||||
expect(rendered.getByText('clientIsMutating: 0')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with queryClient.isMutating (without mutationKey in mutationOptions)', async () => {
|
||||
const mutationOpts = mutationOptions({
|
||||
mutationFn: () => sleep(500).then(() => 'data'),
|
||||
})
|
||||
|
||||
const rendered = render(Base, {
|
||||
props: { queryClient, mutationOpts: () => mutationOpts },
|
||||
})
|
||||
|
||||
expect(rendered.getByText('clientIsMutating: 0')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(rendered.getByText('clientIsMutating: 1')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(500)
|
||||
expect(rendered.getByText('clientIsMutating: 0')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with queryClient.isMutating', async () => {
|
||||
const key = queryKey()
|
||||
const mutationOpts1 = mutationOptions({
|
||||
mutationKey: key,
|
||||
mutationFn: () => sleep(500).then(() => 'data1'),
|
||||
})
|
||||
const mutationOpts2 = mutationOptions({
|
||||
mutationFn: () => sleep(500).then(() => 'data2'),
|
||||
})
|
||||
|
||||
const rendered = render(Multi, {
|
||||
props: {
|
||||
queryClient,
|
||||
mutationOpts1: () => mutationOpts1,
|
||||
mutationOpts2: () => mutationOpts2,
|
||||
},
|
||||
})
|
||||
|
||||
expect(rendered.getByText('clientIsMutating: 0')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate1/i }))
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate2/i }))
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(rendered.getByText('clientIsMutating: 2')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(500)
|
||||
expect(rendered.getByText('clientIsMutating: 0')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should return the number of fetching mutations when used with queryClient.isMutating (filter mutationOpts1.mutationKey)', async () => {
|
||||
const key = queryKey()
|
||||
const mutationOpts1 = mutationOptions({
|
||||
mutationKey: key,
|
||||
mutationFn: () => sleep(500).then(() => 'data1'),
|
||||
})
|
||||
const mutationOpts2 = mutationOptions({
|
||||
mutationFn: () => sleep(500).then(() => 'data2'),
|
||||
})
|
||||
|
||||
const rendered = render(Multi, {
|
||||
props: {
|
||||
queryClient,
|
||||
mutationOpts1: () => mutationOpts1,
|
||||
mutationOpts2: () => mutationOpts2,
|
||||
isMutatingFilters: { mutationKey: mutationOpts1.mutationKey },
|
||||
},
|
||||
})
|
||||
|
||||
expect(rendered.getByText('clientIsMutating: 0')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate1/i }))
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate2/i }))
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(rendered.getByText('clientIsMutating: 1')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(500)
|
||||
expect(rendered.getByText('clientIsMutating: 0')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should return mutation states when used with useMutationState (with mutationKey in mutationOptions)', async () => {
|
||||
const key = queryKey()
|
||||
const mutationOpts = mutationOptions({
|
||||
mutationKey: key,
|
||||
mutationFn: () => sleep(10).then(() => 'data'),
|
||||
})
|
||||
|
||||
const rendered = render(Base, {
|
||||
props: {
|
||||
queryClient,
|
||||
mutationOpts: () => mutationOpts,
|
||||
mutationStateOpts: {
|
||||
filters: {
|
||||
mutationKey: mutationOpts.mutationKey,
|
||||
status: 'success',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(rendered.getByText('mutationState: []')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('mutationState: ["data"]')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should return mutation states when used with useMutationState (without mutationKey in mutationOptions)', async () => {
|
||||
const mutationOpts = mutationOptions({
|
||||
mutationFn: () => sleep(10).then(() => 'data'),
|
||||
})
|
||||
|
||||
const rendered = render(Base, {
|
||||
props: {
|
||||
queryClient,
|
||||
mutationOpts: () => mutationOpts,
|
||||
mutationStateOpts: {
|
||||
filters: { status: 'success' },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(rendered.getByText('mutationState: []')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('mutationState: ["data"]')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should return mutation states when used with useMutationState', async () => {
|
||||
const key = queryKey()
|
||||
const mutationOpts1 = mutationOptions({
|
||||
mutationKey: key,
|
||||
mutationFn: () => sleep(10).then(() => 'data1'),
|
||||
})
|
||||
const mutationOpts2 = mutationOptions({
|
||||
mutationFn: () => sleep(10).then(() => 'data2'),
|
||||
})
|
||||
|
||||
const rendered = render(Multi, {
|
||||
props: {
|
||||
queryClient,
|
||||
mutationOpts1: () => mutationOpts1,
|
||||
mutationOpts2: () => mutationOpts2,
|
||||
mutationStateOpts: {
|
||||
filters: { status: 'success' },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(rendered.getByText('mutationState: []')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate1/i }))
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate2/i }))
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(
|
||||
rendered.getByText('mutationState: ["data1","data2"]'),
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should return mutation states when used with useMutationState (filter mutationOpts1.mutationKey)', async () => {
|
||||
const key = queryKey()
|
||||
const mutationOpts1 = mutationOptions({
|
||||
mutationKey: key,
|
||||
mutationFn: () => sleep(10).then(() => 'data1'),
|
||||
})
|
||||
const mutationOpts2 = mutationOptions({
|
||||
mutationFn: () => sleep(10).then(() => 'data2'),
|
||||
})
|
||||
|
||||
const rendered = render(Multi, {
|
||||
props: {
|
||||
queryClient,
|
||||
mutationOpts1: () => mutationOpts1,
|
||||
mutationOpts2: () => mutationOpts2,
|
||||
mutationStateOpts: {
|
||||
filters: {
|
||||
mutationKey: mutationOpts1.mutationKey,
|
||||
status: 'success',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(rendered.getByText('mutationState: []')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate1/i }))
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate2/i }))
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('mutationState: ["data1"]')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,246 @@
|
||||
import { assertType, describe, expectTypeOf, it } from 'vitest'
|
||||
import { QueryClient } from '@tanstack/query-core'
|
||||
import { queryKey } from '@tanstack/query-test-utils'
|
||||
import {
|
||||
createMutation,
|
||||
mutationOptions,
|
||||
useIsMutating,
|
||||
useMutationState,
|
||||
} from '../../src/index.js'
|
||||
import type {
|
||||
DefaultError,
|
||||
MutationFunctionContext,
|
||||
MutationState,
|
||||
WithRequired,
|
||||
} from '@tanstack/query-core'
|
||||
import type {
|
||||
CreateMutationOptions,
|
||||
CreateMutationResult,
|
||||
} from '../../src/types.js'
|
||||
|
||||
describe('mutationOptions', () => {
|
||||
it('should not allow excess properties', () => {
|
||||
const key = queryKey()
|
||||
|
||||
// @ts-expect-error this is a good error, because onMutates does not exist!
|
||||
mutationOptions({
|
||||
mutationFn: () => Promise.resolve(5),
|
||||
mutationKey: key,
|
||||
onMutates: 1000,
|
||||
onSuccess: (data) => {
|
||||
expectTypeOf(data).toEqualTypeOf<number>()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should infer types for callbacks', () => {
|
||||
const key = queryKey()
|
||||
|
||||
mutationOptions({
|
||||
mutationFn: () => Promise.resolve(5),
|
||||
mutationKey: key,
|
||||
onSuccess: (data) => {
|
||||
expectTypeOf(data).toEqualTypeOf<number>()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should infer types for onError callback', () => {
|
||||
const key = queryKey()
|
||||
|
||||
mutationOptions({
|
||||
mutationFn: () => {
|
||||
throw new Error('fail')
|
||||
},
|
||||
mutationKey: key,
|
||||
onError: (error) => {
|
||||
expectTypeOf(error).toEqualTypeOf<DefaultError>()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should infer types for variables', () => {
|
||||
const key = queryKey()
|
||||
|
||||
mutationOptions<number, DefaultError, { id: string }>({
|
||||
mutationFn: (vars) => {
|
||||
expectTypeOf(vars).toEqualTypeOf<{ id: string }>()
|
||||
return Promise.resolve(5)
|
||||
},
|
||||
mutationKey: key,
|
||||
})
|
||||
})
|
||||
|
||||
it('should infer result type correctly', () => {
|
||||
const key = queryKey()
|
||||
|
||||
mutationOptions<number, DefaultError, void, { name: string }>({
|
||||
mutationFn: () => Promise.resolve(5),
|
||||
mutationKey: key,
|
||||
onMutate: () => {
|
||||
return { name: 'onMutateResult' }
|
||||
},
|
||||
onSuccess: (_data, _variables, onMutateResult) => {
|
||||
expectTypeOf(onMutateResult).toEqualTypeOf<{ name: string }>()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should infer context type correctly', () => {
|
||||
const key = queryKey()
|
||||
|
||||
mutationOptions<number>({
|
||||
mutationFn: (_variables, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
return Promise.resolve(5)
|
||||
},
|
||||
mutationKey: key,
|
||||
onMutate: (_variables, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
},
|
||||
onSuccess: (_data, _variables, _onMutateResult, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
},
|
||||
onError: (_error, _variables, _onMutateResult, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
},
|
||||
onSettled: (_data, _error, _variables, _onMutateResult, context) => {
|
||||
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should error if mutationFn return type mismatches TData', () => {
|
||||
assertType(
|
||||
mutationOptions<number>({
|
||||
// @ts-expect-error this is a good error, because return type is string, not number
|
||||
mutationFn: async () => Promise.resolve('wrong return'),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should allow mutationKey to be omitted', () => {
|
||||
return mutationOptions({
|
||||
mutationFn: () => Promise.resolve(123),
|
||||
onSuccess: (data) => {
|
||||
expectTypeOf(data).toEqualTypeOf<number>()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should infer all types when not explicitly provided', () => {
|
||||
const key = queryKey()
|
||||
|
||||
expectTypeOf(
|
||||
mutationOptions({
|
||||
mutationFn: (id: string) => Promise.resolve(id.length),
|
||||
mutationKey: key,
|
||||
onSuccess: (data) => {
|
||||
expectTypeOf(data).toEqualTypeOf<number>()
|
||||
},
|
||||
}),
|
||||
).toEqualTypeOf<
|
||||
WithRequired<
|
||||
CreateMutationOptions<number, DefaultError, string>,
|
||||
'mutationKey'
|
||||
>
|
||||
>()
|
||||
expectTypeOf(
|
||||
mutationOptions({
|
||||
mutationFn: (id: string) => Promise.resolve(id.length),
|
||||
onSuccess: (data) => {
|
||||
expectTypeOf(data).toEqualTypeOf<number>()
|
||||
},
|
||||
}),
|
||||
).toEqualTypeOf<
|
||||
Omit<CreateMutationOptions<number, DefaultError, string>, 'mutationKey'>
|
||||
>()
|
||||
})
|
||||
|
||||
it('should work when used with createMutation', () => {
|
||||
const key = queryKey()
|
||||
|
||||
const mutation = createMutation(() =>
|
||||
mutationOptions({
|
||||
mutationKey: key,
|
||||
mutationFn: () => Promise.resolve('data'),
|
||||
onSuccess: (data) => {
|
||||
expectTypeOf(data).toEqualTypeOf<string>()
|
||||
},
|
||||
}),
|
||||
)
|
||||
expectTypeOf(mutation).toEqualTypeOf<
|
||||
CreateMutationResult<string, DefaultError, void, unknown>
|
||||
>()
|
||||
|
||||
createMutation(() =>
|
||||
// should allow when used with createMutation without mutationKey
|
||||
mutationOptions({
|
||||
mutationFn: () => Promise.resolve('data'),
|
||||
onSuccess: (data) => {
|
||||
expectTypeOf(data).toEqualTypeOf<string>()
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should work when used with useIsMutating', () => {
|
||||
const key = queryKey()
|
||||
|
||||
const isMutating = useIsMutating(
|
||||
mutationOptions({
|
||||
mutationKey: key,
|
||||
mutationFn: () => Promise.resolve(5),
|
||||
}),
|
||||
)
|
||||
expectTypeOf(isMutating.current).toEqualTypeOf<number>()
|
||||
|
||||
useIsMutating(
|
||||
// @ts-expect-error filters should have mutationKey
|
||||
mutationOptions({
|
||||
mutationFn: () => Promise.resolve(5),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should work when used with queryClient.isMutating', () => {
|
||||
const key = queryKey()
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
const isMutating = queryClient.isMutating(
|
||||
mutationOptions({
|
||||
mutationKey: key,
|
||||
mutationFn: () => Promise.resolve(5),
|
||||
}),
|
||||
)
|
||||
expectTypeOf(isMutating).toEqualTypeOf<number>()
|
||||
|
||||
queryClient.isMutating(
|
||||
// @ts-expect-error filters should have mutationKey
|
||||
mutationOptions({
|
||||
mutationFn: () => Promise.resolve(5),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should work when used with useMutationState', () => {
|
||||
const key = queryKey()
|
||||
|
||||
const mutationState = useMutationState({
|
||||
filters: mutationOptions({
|
||||
mutationKey: key,
|
||||
mutationFn: () => Promise.resolve(5),
|
||||
}),
|
||||
})
|
||||
expectTypeOf(mutationState).toEqualTypeOf<
|
||||
Array<MutationState<unknown, Error, unknown, unknown>>
|
||||
>()
|
||||
|
||||
useMutationState({
|
||||
// @ts-expect-error filters should have mutationKey
|
||||
filters: mutationOptions({
|
||||
mutationFn: () => Promise.resolve(5),
|
||||
}),
|
||||
})
|
||||
})
|
||||
})
|
||||
13
packages/svelte-query/tests/queryOptions.svelte.test.ts
Normal file
13
packages/svelte-query/tests/queryOptions.svelte.test.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { queryOptions } from '../src/index.js'
|
||||
|
||||
describe('queryOptions', () => {
|
||||
it('should return the object received as a parameter without any modification.', () => {
|
||||
const object = {
|
||||
queryKey: ['key'],
|
||||
queryFn: () => Promise.resolve(5),
|
||||
} as const
|
||||
|
||||
expect(queryOptions(object)).toBe(object)
|
||||
})
|
||||
})
|
||||
271
packages/svelte-query/tests/queryOptions.test-d.ts
Normal file
271
packages/svelte-query/tests/queryOptions.test-d.ts
Normal file
@@ -0,0 +1,271 @@
|
||||
import { assertType, describe, expectTypeOf, it } from 'vitest'
|
||||
import {
|
||||
QueriesObserver,
|
||||
QueryClient,
|
||||
dataTagSymbol,
|
||||
skipToken,
|
||||
} from '@tanstack/query-core'
|
||||
import { queryKey } from '@tanstack/query-test-utils'
|
||||
import { createQueries, queryOptions } from '../src/index.js'
|
||||
import type { QueryObserverResult } from '@tanstack/query-core'
|
||||
|
||||
// Regression test for exported queryOptions inference under declaration emit.
|
||||
// TypeScript should be able to name the return type without expanding the
|
||||
// internal data tag symbols into the consumer's .d.ts output.
|
||||
export const exportedQueryOptions = queryOptions({
|
||||
queryKey: ['invalid'],
|
||||
})
|
||||
|
||||
describe('queryOptions', () => {
|
||||
it('should not allow excess properties', () => {
|
||||
const key = queryKey()
|
||||
assertType(
|
||||
queryOptions({
|
||||
queryKey: key,
|
||||
queryFn: () => Promise.resolve(5),
|
||||
// @ts-expect-error this is a good error, because stallTime does not exist!
|
||||
stallTime: 1000,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should infer types for callbacks', () => {
|
||||
const key = queryKey()
|
||||
queryOptions({
|
||||
queryKey: key,
|
||||
queryFn: () => Promise.resolve(5),
|
||||
staleTime: 1000,
|
||||
select: (data) => {
|
||||
expectTypeOf(data).toEqualTypeOf<number>()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should work when passed to query', async () => {
|
||||
const options = queryOptions({
|
||||
queryKey: ['key'],
|
||||
queryFn: () => Promise.resolve(5),
|
||||
})
|
||||
|
||||
const data = await new QueryClient().query(options)
|
||||
expectTypeOf(data).toEqualTypeOf<number>()
|
||||
})
|
||||
|
||||
it('should work when passed to query with select', async () => {
|
||||
const options = queryOptions({
|
||||
queryKey: ['key'],
|
||||
queryFn: () => Promise.resolve(5),
|
||||
select: (data) => data.toString(),
|
||||
})
|
||||
|
||||
const data = await new QueryClient().query(options)
|
||||
expectTypeOf(data).toEqualTypeOf<string>()
|
||||
})
|
||||
|
||||
it('should work when passed to query with enabled: false', async () => {
|
||||
const options = queryOptions({
|
||||
queryKey: ['key'],
|
||||
queryFn: () => Promise.resolve(5),
|
||||
enabled: false,
|
||||
})
|
||||
|
||||
const data = await new QueryClient().query(options)
|
||||
expectTypeOf(data).toEqualTypeOf<number>()
|
||||
})
|
||||
|
||||
it('should work when passed to query with skipToken', async () => {
|
||||
const options = queryOptions({
|
||||
queryKey: ['key'],
|
||||
queryFn: skipToken,
|
||||
})
|
||||
|
||||
const data = await new QueryClient().query(options)
|
||||
expectTypeOf(data).toEqualTypeOf<unknown>()
|
||||
})
|
||||
|
||||
it('should work when passed to fetchQuery', async () => {
|
||||
const key = queryKey()
|
||||
const options = queryOptions({
|
||||
queryKey: key,
|
||||
queryFn: () => Promise.resolve(5),
|
||||
})
|
||||
|
||||
// grandfathered direct test
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const data = await new QueryClient().fetchQuery(options)
|
||||
expectTypeOf(data).toEqualTypeOf<number>()
|
||||
})
|
||||
|
||||
it('should work when passed to createQueries', () => {
|
||||
const key = queryKey()
|
||||
const options = queryOptions({
|
||||
queryKey: key,
|
||||
queryFn: () => Promise.resolve(5),
|
||||
})
|
||||
|
||||
const queries = createQueries(() => ({
|
||||
queries: [options],
|
||||
}))
|
||||
|
||||
expectTypeOf(queries[0].data).toEqualTypeOf<number | undefined>()
|
||||
})
|
||||
|
||||
it('should tag the queryKey with the result type of the QueryFn', () => {
|
||||
const key = queryKey()
|
||||
const { queryKey: tagged } = queryOptions({
|
||||
queryKey: key,
|
||||
queryFn: () => Promise.resolve(5),
|
||||
})
|
||||
|
||||
expectTypeOf(tagged[dataTagSymbol]).toEqualTypeOf<number>()
|
||||
})
|
||||
|
||||
it('should tag the queryKey even if no promise is returned', () => {
|
||||
const key = queryKey()
|
||||
const { queryKey: tagged } = queryOptions({
|
||||
queryKey: key,
|
||||
queryFn: () => 5,
|
||||
})
|
||||
|
||||
expectTypeOf(tagged[dataTagSymbol]).toEqualTypeOf<number>()
|
||||
})
|
||||
|
||||
it('should tag the queryKey with unknown if there is no queryFn', () => {
|
||||
const key = queryKey()
|
||||
const { queryKey: tagged } = queryOptions({
|
||||
queryKey: key,
|
||||
})
|
||||
|
||||
expectTypeOf(tagged[dataTagSymbol]).toEqualTypeOf<unknown>()
|
||||
})
|
||||
|
||||
it('should tag the queryKey with the result type of the QueryFn if select is used', () => {
|
||||
const key = queryKey()
|
||||
const { queryKey: tagged } = queryOptions({
|
||||
queryKey: key,
|
||||
queryFn: () => Promise.resolve(5),
|
||||
select: (data) => data.toString(),
|
||||
})
|
||||
|
||||
expectTypeOf(tagged[dataTagSymbol]).toEqualTypeOf<number>()
|
||||
})
|
||||
|
||||
it('should return the proper type when passed to getQueryData', () => {
|
||||
const key = queryKey()
|
||||
const { queryKey: tagged } = queryOptions({
|
||||
queryKey: key,
|
||||
queryFn: () => Promise.resolve(5),
|
||||
})
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
const data = queryClient.getQueryData(tagged)
|
||||
expectTypeOf(data).toEqualTypeOf<number | undefined>()
|
||||
})
|
||||
|
||||
it('should return the proper type when passed to getQueryState', () => {
|
||||
const key = queryKey()
|
||||
const { queryKey: tagged } = queryOptions({
|
||||
queryKey: key,
|
||||
queryFn: () => Promise.resolve(5),
|
||||
})
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
const state = queryClient.getQueryState(tagged)
|
||||
expectTypeOf(state?.data).toEqualTypeOf<number | undefined>()
|
||||
})
|
||||
|
||||
it('should properly type updaterFn when passed to setQueryData', () => {
|
||||
const key = queryKey()
|
||||
const { queryKey: tagged } = queryOptions({
|
||||
queryKey: key,
|
||||
queryFn: () => Promise.resolve(5),
|
||||
})
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
const data = queryClient.setQueryData(tagged, (prev) => {
|
||||
expectTypeOf(prev).toEqualTypeOf<number | undefined>()
|
||||
return prev
|
||||
})
|
||||
expectTypeOf(data).toEqualTypeOf<number | undefined>()
|
||||
})
|
||||
|
||||
it('should properly type value when passed to setQueryData', () => {
|
||||
const key = queryKey()
|
||||
const { queryKey: tagged } = queryOptions({
|
||||
queryKey: key,
|
||||
queryFn: () => Promise.resolve(5),
|
||||
})
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
// @ts-expect-error value should be a number
|
||||
queryClient.setQueryData(tagged, '5')
|
||||
// @ts-expect-error value should be a number
|
||||
queryClient.setQueryData(tagged, () => '5')
|
||||
|
||||
const data = queryClient.setQueryData(tagged, 5)
|
||||
expectTypeOf(data).toEqualTypeOf<number | undefined>()
|
||||
})
|
||||
|
||||
it('should infer even if there is a conditional skipToken', () => {
|
||||
const key = queryKey()
|
||||
const options = queryOptions({
|
||||
queryKey: key,
|
||||
queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5),
|
||||
})
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
const data = queryClient.getQueryData(options.queryKey)
|
||||
expectTypeOf(data).toEqualTypeOf<number | undefined>()
|
||||
})
|
||||
|
||||
it('should infer to unknown if we disable a query with just a skipToken', () => {
|
||||
const key = queryKey()
|
||||
const options = queryOptions({
|
||||
queryKey: key,
|
||||
queryFn: skipToken,
|
||||
})
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
const data = queryClient.getQueryData(options.queryKey)
|
||||
expectTypeOf(data).toEqualTypeOf<unknown>()
|
||||
})
|
||||
|
||||
it('should return the proper type when passed to QueriesObserver', () => {
|
||||
const key = queryKey()
|
||||
const options = queryOptions({
|
||||
queryKey: key,
|
||||
queryFn: () => Promise.resolve(5),
|
||||
})
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
const queriesObserver = new QueriesObserver(queryClient, [options])
|
||||
expectTypeOf(queriesObserver).toEqualTypeOf<
|
||||
QueriesObserver<Array<QueryObserverResult>>
|
||||
>()
|
||||
})
|
||||
|
||||
it('should allow undefined response in initialData', () => {
|
||||
const key = queryKey()
|
||||
const options = (id: string | null) =>
|
||||
queryOptions({
|
||||
queryKey: [...key, id],
|
||||
queryFn: () =>
|
||||
Promise.resolve({
|
||||
id: '1',
|
||||
title: 'Do Laundry',
|
||||
}),
|
||||
initialData: () =>
|
||||
!id
|
||||
? undefined
|
||||
: {
|
||||
id,
|
||||
title: 'Initial Data',
|
||||
},
|
||||
})
|
||||
|
||||
expectTypeOf(options(null).initialData).returns.toEqualTypeOf<
|
||||
{ id: string; title: string } | undefined
|
||||
>()
|
||||
})
|
||||
})
|
||||
1
packages/svelte-query/tests/test-setup.ts
Normal file
1
packages/svelte-query/tests/test-setup.ts
Normal file
@@ -0,0 +1 @@
|
||||
import '@testing-library/jest-dom/vitest'
|
||||
29
packages/svelte-query/tests/useIsFetching/Base.svelte
Normal file
29
packages/svelte-query/tests/useIsFetching/Base.svelte
Normal file
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import type { QueryClient } from '@tanstack/query-core'
|
||||
import { queryKey, sleep } from '@tanstack/query-test-utils'
|
||||
import { setQueryClientContext } from '../../src/context.js'
|
||||
import { createQuery, useIsFetching } from '../../src/index.js'
|
||||
|
||||
type Props = {
|
||||
queryClient: QueryClient
|
||||
}
|
||||
|
||||
let { queryClient }: Props = $props()
|
||||
|
||||
setQueryClientContext(queryClient)
|
||||
|
||||
let ready = $state(false)
|
||||
|
||||
const query = createQuery(() => ({
|
||||
queryKey: queryKey(),
|
||||
queryFn: () => sleep(10).then(() => 'test'),
|
||||
enabled: ready,
|
||||
}))
|
||||
|
||||
const isFetching = useIsFetching()
|
||||
</script>
|
||||
|
||||
<button onclick={() => (ready = true)}>setReady</button>
|
||||
|
||||
<div>isFetching: {isFetching.current}</div>
|
||||
<div>Data: {query.data ?? 'undefined'}</div>
|
||||
@@ -0,0 +1,32 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { fireEvent, render } from '@testing-library/svelte'
|
||||
import { QueryClient } from '@tanstack/query-core'
|
||||
import Base from './Base.svelte'
|
||||
|
||||
describe('useIsFetching', () => {
|
||||
let queryClient: QueryClient
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
queryClient = new QueryClient()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
queryClient.clear()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('should update as queries start and stop fetching', async () => {
|
||||
const rendered = render(Base, {
|
||||
props: { queryClient },
|
||||
})
|
||||
|
||||
expect(rendered.getByText('isFetching: 0')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /setReady/i }))
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(rendered.getByText('isFetching: 1')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('isFetching: 0')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
25
packages/svelte-query/tests/useIsMutating/Base.svelte
Normal file
25
packages/svelte-query/tests/useIsMutating/Base.svelte
Normal file
@@ -0,0 +1,25 @@
|
||||
<script lang="ts">
|
||||
import type { QueryClient } from '@tanstack/query-core'
|
||||
import { queryKey, sleep } from '@tanstack/query-test-utils'
|
||||
import { setQueryClientContext } from '../../src/context.js'
|
||||
import { createMutation, useIsMutating } from '../../src/index.js'
|
||||
|
||||
type Props = {
|
||||
queryClient: QueryClient
|
||||
}
|
||||
|
||||
let { queryClient }: Props = $props()
|
||||
|
||||
setQueryClientContext(queryClient)
|
||||
|
||||
const mutation = createMutation(() => ({
|
||||
mutationKey: queryKey(),
|
||||
mutationFn: () => sleep(10).then(() => 'data'),
|
||||
}))
|
||||
|
||||
const isMutating = useIsMutating()
|
||||
</script>
|
||||
|
||||
<button onclick={() => mutation.mutate()}>Trigger</button>
|
||||
|
||||
<div>isMutating: {isMutating.current}</div>
|
||||
@@ -0,0 +1,32 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { fireEvent, render } from '@testing-library/svelte'
|
||||
import { QueryClient } from '@tanstack/query-core'
|
||||
import Base from './Base.svelte'
|
||||
|
||||
describe('useIsMutating', () => {
|
||||
let queryClient: QueryClient
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
queryClient = new QueryClient()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
queryClient.clear()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('should update as queries start and stop mutating', async () => {
|
||||
const rendered = render(Base, {
|
||||
props: { queryClient },
|
||||
})
|
||||
|
||||
expect(rendered.getByText('isMutating: 0')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /Trigger/i }))
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(rendered.getByText('isMutating: 1')).toBeInTheDocument()
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('isMutating: 0')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
41
packages/svelte-query/tests/useMutationState/Base.svelte
Normal file
41
packages/svelte-query/tests/useMutationState/Base.svelte
Normal file
@@ -0,0 +1,41 @@
|
||||
<script lang="ts">
|
||||
import type { QueryClient } from '@tanstack/query-core'
|
||||
import {
|
||||
createMutation,
|
||||
setQueryClientContext,
|
||||
useMutationState,
|
||||
} from '../../src/index.js'
|
||||
import type {
|
||||
Accessor,
|
||||
CreateMutationOptions,
|
||||
MutationStateOptions,
|
||||
} from '../../src/index.js'
|
||||
|
||||
type Props = {
|
||||
queryClient: QueryClient
|
||||
successMutationOpts: Accessor<CreateMutationOptions<string>>
|
||||
errorMutationOpts: Accessor<CreateMutationOptions<string>>
|
||||
mutationStateOpts?: MutationStateOptions
|
||||
}
|
||||
|
||||
let {
|
||||
queryClient,
|
||||
successMutationOpts,
|
||||
errorMutationOpts,
|
||||
mutationStateOpts,
|
||||
}: Props = $props()
|
||||
|
||||
setQueryClientContext(queryClient)
|
||||
|
||||
const successMutation = createMutation(successMutationOpts)
|
||||
const errorMutation = createMutation(errorMutationOpts)
|
||||
|
||||
const mutationState = useMutationState(mutationStateOpts)
|
||||
</script>
|
||||
|
||||
<button onclick={() => successMutation.mutate()}>Success</button>
|
||||
<button onclick={() => errorMutation.mutate()}>Error</button>
|
||||
|
||||
<div>
|
||||
Data: {JSON.stringify(mutationState.map((state) => state.status))}
|
||||
</div>
|
||||
33
packages/svelte-query/tests/useMutationState/Select.svelte
Normal file
33
packages/svelte-query/tests/useMutationState/Select.svelte
Normal file
@@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import type { QueryClient } from '@tanstack/query-core'
|
||||
import {
|
||||
createMutation,
|
||||
setQueryClientContext,
|
||||
useMutationState,
|
||||
} from '../../src/index.js'
|
||||
import type {
|
||||
Accessor,
|
||||
CreateMutationOptions,
|
||||
MutationStateOptions,
|
||||
} from '../../src/index.js'
|
||||
|
||||
type Props = {
|
||||
queryClient: QueryClient
|
||||
mutationOpts: Accessor<CreateMutationOptions<string>>
|
||||
mutationStateOpts: MutationStateOptions<string>
|
||||
}
|
||||
|
||||
let { queryClient, mutationOpts, mutationStateOpts }: Props = $props()
|
||||
|
||||
setQueryClientContext(queryClient)
|
||||
|
||||
const mutation = createMutation(mutationOpts)
|
||||
|
||||
const variables = useMutationState(mutationStateOpts)
|
||||
</script>
|
||||
|
||||
<button onclick={() => mutation.mutate()}>mutate</button>
|
||||
|
||||
<div>
|
||||
Variables: {JSON.stringify(variables)}
|
||||
</div>
|
||||
@@ -0,0 +1,195 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { fireEvent, render } from '@testing-library/svelte'
|
||||
import { QueryClient } from '@tanstack/query-core'
|
||||
import { queryKey, sleep } from '@tanstack/query-test-utils'
|
||||
import Base from './Base.svelte'
|
||||
import Select from './Select.svelte'
|
||||
import type { Mutation } from '@tanstack/query-core'
|
||||
|
||||
describe('useMutationState', () => {
|
||||
let queryClient: QueryClient
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
queryClient = new QueryClient()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
queryClient.clear()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('should run few mutation functions and check from useMutationState', async () => {
|
||||
const successKey = queryKey()
|
||||
const errorKey = queryKey()
|
||||
const successMutationFn = vi.fn(() => sleep(10).then(() => 'data'))
|
||||
const errorMutationFn = vi
|
||||
.fn()
|
||||
.mockImplementation(() =>
|
||||
sleep(20).then(() => Promise.reject(new Error('error'))),
|
||||
)
|
||||
|
||||
const rendered = render(Base, {
|
||||
props: {
|
||||
queryClient,
|
||||
successMutationOpts: () => ({
|
||||
mutationKey: successKey,
|
||||
mutationFn: successMutationFn,
|
||||
}),
|
||||
errorMutationOpts: () => ({
|
||||
mutationKey: errorKey,
|
||||
mutationFn: errorMutationFn,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /Success/i }))
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(successMutationFn).toHaveBeenCalledTimes(1)
|
||||
expect(rendered.getByText('Data: ["success"]')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /Error/i }))
|
||||
await vi.advanceTimersByTimeAsync(20)
|
||||
expect(errorMutationFn).toHaveBeenCalledTimes(1)
|
||||
expect(rendered.getByText('Data: ["success","error"]')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should select specific type of mutation ( i.e: error only )', async () => {
|
||||
const successKey = queryKey()
|
||||
const errorKey = queryKey()
|
||||
const successMutationFn = vi.fn(() => sleep(10).then(() => 'data'))
|
||||
const errorMutationFn = vi
|
||||
.fn()
|
||||
.mockImplementation(() =>
|
||||
sleep(20).then(() => Promise.reject(new Error('error'))),
|
||||
)
|
||||
|
||||
const rendered = render(Base, {
|
||||
props: {
|
||||
queryClient,
|
||||
successMutationOpts: () => ({
|
||||
mutationKey: successKey,
|
||||
mutationFn: successMutationFn,
|
||||
}),
|
||||
errorMutationOpts: () => ({
|
||||
mutationKey: errorKey,
|
||||
mutationFn: errorMutationFn,
|
||||
}),
|
||||
mutationStateOpts: {
|
||||
filters: { status: 'error' },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /Success/i }))
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(successMutationFn).toHaveBeenCalledTimes(1)
|
||||
expect(rendered.getByText('Data: []')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /Error/i }))
|
||||
await vi.advanceTimersByTimeAsync(20)
|
||||
expect(errorMutationFn).toHaveBeenCalledTimes(1)
|
||||
expect(rendered.getByText('Data: ["error"]')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should return selected value when using select option', async () => {
|
||||
const mutationKey = queryKey()
|
||||
|
||||
const rendered = render(Select, {
|
||||
props: {
|
||||
queryClient,
|
||||
mutationOpts: () => ({
|
||||
mutationKey,
|
||||
mutationFn: () => sleep(10).then(() => 'data'),
|
||||
}),
|
||||
mutationStateOpts: {
|
||||
filters: { mutationKey },
|
||||
select: (mutation: Mutation) => mutation.state.status,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(rendered.getByText('Variables: []')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(rendered.getByText('Variables: ["pending"]')).toBeInTheDocument()
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(rendered.getByText('Variables: ["success"]')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should select specific mutation using mutation key', async () => {
|
||||
const successKey = queryKey()
|
||||
const errorKey = queryKey()
|
||||
const successMutationFn = vi.fn(() => sleep(10).then(() => 'data'))
|
||||
const errorMutationFn = vi
|
||||
.fn()
|
||||
.mockImplementation(() =>
|
||||
sleep(20).then(() => Promise.reject(new Error('error'))),
|
||||
)
|
||||
|
||||
const rendered = render(Base, {
|
||||
props: {
|
||||
queryClient,
|
||||
successMutationOpts: () => ({
|
||||
mutationKey: successKey,
|
||||
mutationFn: successMutationFn,
|
||||
}),
|
||||
errorMutationOpts: () => ({
|
||||
mutationKey: errorKey,
|
||||
mutationFn: errorMutationFn,
|
||||
}),
|
||||
mutationStateOpts: {
|
||||
filters: { mutationKey: successKey },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /Success/i }))
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
expect(successMutationFn).toHaveBeenCalledTimes(1)
|
||||
expect(rendered.getByText('Data: ["success"]')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /Error/i }))
|
||||
await vi.advanceTimersByTimeAsync(20)
|
||||
expect(errorMutationFn).toHaveBeenCalledTimes(1)
|
||||
expect(rendered.getByText('Data: ["success"]')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should remove mutations that no longer match the filter', async () => {
|
||||
const firstKey = queryKey()
|
||||
const secondKey = queryKey()
|
||||
const mutationFn = vi.fn(() => sleep(10).then(() => 'data'))
|
||||
|
||||
const rendered = render(Base, {
|
||||
props: {
|
||||
queryClient,
|
||||
successMutationOpts: () => ({
|
||||
mutationKey: firstKey,
|
||||
mutationFn,
|
||||
}),
|
||||
errorMutationOpts: () => ({
|
||||
mutationKey: secondKey,
|
||||
mutationFn,
|
||||
}),
|
||||
mutationStateOpts: {
|
||||
filters: { status: 'pending' },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
fireEvent.click(rendered.getByRole('button', { name: /Success/i }))
|
||||
fireEvent.click(rendered.getByRole('button', { name: /Error/i }))
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(
|
||||
rendered.getByText('Data: ["pending","pending"]'),
|
||||
).toBeInTheDocument()
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10)
|
||||
|
||||
expect(rendered.getByText('Data: []')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,24 @@
|
||||
import { describe, expectTypeOf, it } from 'vitest'
|
||||
import { useMutationState } from '../../src/index.js'
|
||||
import type { MutationState, MutationStatus } from '@tanstack/query-core'
|
||||
|
||||
describe('useMutationState', () => {
|
||||
it('should default to MutationState', () => {
|
||||
const result = useMutationState({
|
||||
filters: { status: 'pending' },
|
||||
})
|
||||
|
||||
expectTypeOf(result).toEqualTypeOf<
|
||||
Array<MutationState<unknown, Error, unknown, unknown>>
|
||||
>()
|
||||
})
|
||||
|
||||
it('should infer with select', () => {
|
||||
const result = useMutationState({
|
||||
filters: { status: 'pending' },
|
||||
select: (mutation) => mutation.state.status,
|
||||
})
|
||||
|
||||
expectTypeOf(result).toEqualTypeOf<Array<MutationStatus>>()
|
||||
})
|
||||
})
|
||||
79
packages/svelte-query/tests/utils.svelte.test.ts
Normal file
79
packages/svelte-query/tests/utils.svelte.test.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { flushSync } from 'svelte'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { watchChanges } from '../src/utils.svelte.js'
|
||||
import { ref, withEffectRoot } from './utils.svelte.js'
|
||||
|
||||
describe('watchChanges', () => {
|
||||
it(
|
||||
'should skip the first run and only call the effect on subsequent changes',
|
||||
withEffectRoot(() => {
|
||||
const source = ref(0)
|
||||
const effect = vi.fn()
|
||||
|
||||
watchChanges(() => source.value, 'pre', effect)
|
||||
|
||||
// first run only records previousValues, effect is not called
|
||||
flushSync()
|
||||
expect(effect).not.toHaveBeenCalled()
|
||||
|
||||
source.value = 1
|
||||
flushSync()
|
||||
expect(effect).toHaveBeenCalledExactlyOnceWith(1, 0)
|
||||
}),
|
||||
)
|
||||
|
||||
it(
|
||||
'should run with the "post" flush timing',
|
||||
withEffectRoot(() => {
|
||||
const source = ref(0)
|
||||
const effect = vi.fn()
|
||||
|
||||
watchChanges(() => source.value, 'post', effect)
|
||||
|
||||
flushSync()
|
||||
expect(effect).not.toHaveBeenCalled()
|
||||
|
||||
source.value = 1
|
||||
flushSync()
|
||||
expect(effect).toHaveBeenCalledExactlyOnceWith(1, 0)
|
||||
}),
|
||||
)
|
||||
|
||||
it(
|
||||
'should track an array of sources and pass arrays of values',
|
||||
withEffectRoot(() => {
|
||||
const a = ref(1)
|
||||
const b = ref(2)
|
||||
const effect = vi.fn()
|
||||
|
||||
watchChanges([() => a.value, () => b.value], 'pre', effect)
|
||||
|
||||
flushSync()
|
||||
expect(effect).not.toHaveBeenCalled()
|
||||
|
||||
a.value = 10
|
||||
flushSync()
|
||||
expect(effect).toHaveBeenCalledExactlyOnceWith([10, 2], [1, 2])
|
||||
}),
|
||||
)
|
||||
|
||||
it(
|
||||
'should run the returned cleanup before the next effect run',
|
||||
withEffectRoot(() => {
|
||||
const source = ref(0)
|
||||
const cleanup = vi.fn()
|
||||
const effect = vi.fn(() => cleanup)
|
||||
|
||||
watchChanges(() => source.value, 'pre', effect)
|
||||
|
||||
flushSync()
|
||||
source.value = 1
|
||||
flushSync()
|
||||
expect(cleanup).not.toHaveBeenCalled()
|
||||
|
||||
source.value = 2
|
||||
flushSync()
|
||||
expect(cleanup).toHaveBeenCalledOnce()
|
||||
}),
|
||||
)
|
||||
})
|
||||
33
packages/svelte-query/tests/utils.svelte.ts
Normal file
33
packages/svelte-query/tests/utils.svelte.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
export function ref<T>(initial: T) {
|
||||
let value = $state(initial)
|
||||
|
||||
return {
|
||||
get value() {
|
||||
return value
|
||||
},
|
||||
set value(newValue) {
|
||||
value = newValue
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function promiseWithResolvers<T>() {
|
||||
let resolve: (value: T) => void
|
||||
let reject: (reason?: any) => void
|
||||
const promise = new Promise<T>((res, rej) => {
|
||||
resolve = res
|
||||
reject = rej
|
||||
})
|
||||
return { promise, resolve: resolve!, reject: reject! }
|
||||
}
|
||||
|
||||
export function withEffectRoot(fn: () => void | Promise<void>) {
|
||||
return async () => {
|
||||
let promise: void | Promise<void> = Promise.resolve()
|
||||
const cleanup = $effect.root(() => {
|
||||
promise = fn()
|
||||
})
|
||||
await promise
|
||||
cleanup()
|
||||
}
|
||||
}
|
||||
8
packages/svelte-query/tsconfig.json
Normal file
8
packages/svelte-query/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist-ts",
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": ["src", "tests", "*.config.ts", "*.config.js", "package.json"]
|
||||
}
|
||||
33
packages/svelte-query/vite.config.ts
Normal file
33
packages/svelte-query/vite.config.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import { svelteTesting } from '@testing-library/svelte/vite'
|
||||
|
||||
import packageJson from './package.json'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [svelte(), svelteTesting()],
|
||||
// fix from https://github.com/vitest-dev/vitest/issues/6992#issuecomment-2509408660
|
||||
resolve: {
|
||||
conditions: ['@tanstack/custom-condition'],
|
||||
},
|
||||
environments: {
|
||||
ssr: {
|
||||
resolve: {
|
||||
conditions: ['@tanstack/custom-condition'],
|
||||
},
|
||||
},
|
||||
},
|
||||
test: {
|
||||
name: packageJson.name,
|
||||
dir: './tests',
|
||||
watch: false,
|
||||
environment: 'jsdom',
|
||||
setupFiles: ['./tests/test-setup.ts'],
|
||||
coverage: {
|
||||
enabled: !!process.env.CI,
|
||||
provider: 'istanbul',
|
||||
include: ['src/**/*'],
|
||||
},
|
||||
typecheck: { enabled: true },
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user