From 01bbba9bb3aa0401053af689422389bb151b9762 Mon Sep 17 00:00:00 2001 From: Damian Osipiuk Date: Thu, 29 Sep 2022 23:14:03 +0200 Subject: [PATCH 1/8] feat: vue-query adapter --- packages/vue-query/.eslintrc | 9 + packages/vue-query/__mocks__/vue-demi.ts | 9 + packages/vue-query/package.json | 55 ++ .../vue-query/src/__mocks__/useBaseQuery.ts | 3 + .../vue-query/src/__mocks__/useQueryClient.ts | 15 + .../src/__tests__/mutationCache.test.ts | 40 ++ .../src/__tests__/queryCache.test.ts | 54 ++ .../src/__tests__/queryClient.test.ts | 546 +++++++++++++++++ .../vue-query/src/__tests__/test-utils.ts | 53 ++ .../src/__tests__/useInfiniteQuery.test.ts | 34 ++ .../src/__tests__/useIsFetching.test.ts | 91 +++ .../src/__tests__/useIsMutating.test.ts | 97 +++ .../src/__tests__/useMutation.test.ts | 289 +++++++++ .../src/__tests__/useQueries.test.ts | 193 ++++++ .../vue-query/src/__tests__/useQuery.test.ts | 283 +++++++++ .../src/__tests__/useQueryClient.test.ts | 49 ++ .../vue-query/src/__tests__/utils.test.ts | 154 +++++ .../src/__tests__/vueQueryPlugin.test.ts | 256 ++++++++ packages/vue-query/src/devtools/devtools.ts | 199 ++++++ packages/vue-query/src/devtools/utils.ts | 99 +++ packages/vue-query/src/index.ts | 26 + packages/vue-query/src/mutationCache.ts | 16 + packages/vue-query/src/queryCache.ts | 36 ++ packages/vue-query/src/queryClient.ts | 568 ++++++++++++++++++ packages/vue-query/src/types.ts | 91 +++ packages/vue-query/src/useBaseQuery.ts | 122 ++++ packages/vue-query/src/useInfiniteQuery.ts | 114 ++++ packages/vue-query/src/useIsFetching.ts | 59 ++ packages/vue-query/src/useIsMutating.ts | 59 ++ packages/vue-query/src/useMutation.ts | 189 ++++++ packages/vue-query/src/useQueries.ts | 167 +++++ packages/vue-query/src/useQuery.ts | 174 ++++++ packages/vue-query/src/useQueryClient.ts | 23 + packages/vue-query/src/utils.ts | 67 +++ packages/vue-query/src/vueQueryPlugin.ts | 106 ++++ packages/vue-query/tsconfig.json | 14 + packages/vue-query/tsconfig.lint.json | 13 + pnpm-lock.yaml | 174 +++++- pnpm-workspace.yaml | 1 + rollup.config.ts | 26 +- scripts/config.ts | 5 + tsconfig.base.json | 1 + tsconfig.json | 1 + 43 files changed, 4560 insertions(+), 20 deletions(-) create mode 100644 packages/vue-query/.eslintrc create mode 100644 packages/vue-query/__mocks__/vue-demi.ts create mode 100644 packages/vue-query/package.json create mode 100644 packages/vue-query/src/__mocks__/useBaseQuery.ts create mode 100644 packages/vue-query/src/__mocks__/useQueryClient.ts create mode 100644 packages/vue-query/src/__tests__/mutationCache.test.ts create mode 100644 packages/vue-query/src/__tests__/queryCache.test.ts create mode 100644 packages/vue-query/src/__tests__/queryClient.test.ts create mode 100644 packages/vue-query/src/__tests__/test-utils.ts create mode 100644 packages/vue-query/src/__tests__/useInfiniteQuery.test.ts create mode 100644 packages/vue-query/src/__tests__/useIsFetching.test.ts create mode 100644 packages/vue-query/src/__tests__/useIsMutating.test.ts create mode 100644 packages/vue-query/src/__tests__/useMutation.test.ts create mode 100644 packages/vue-query/src/__tests__/useQueries.test.ts create mode 100644 packages/vue-query/src/__tests__/useQuery.test.ts create mode 100644 packages/vue-query/src/__tests__/useQueryClient.test.ts create mode 100644 packages/vue-query/src/__tests__/utils.test.ts create mode 100644 packages/vue-query/src/__tests__/vueQueryPlugin.test.ts create mode 100644 packages/vue-query/src/devtools/devtools.ts create mode 100644 packages/vue-query/src/devtools/utils.ts create mode 100644 packages/vue-query/src/index.ts create mode 100644 packages/vue-query/src/mutationCache.ts create mode 100644 packages/vue-query/src/queryCache.ts create mode 100644 packages/vue-query/src/queryClient.ts create mode 100644 packages/vue-query/src/types.ts create mode 100644 packages/vue-query/src/useBaseQuery.ts create mode 100644 packages/vue-query/src/useInfiniteQuery.ts create mode 100644 packages/vue-query/src/useIsFetching.ts create mode 100644 packages/vue-query/src/useIsMutating.ts create mode 100644 packages/vue-query/src/useMutation.ts create mode 100644 packages/vue-query/src/useQueries.ts create mode 100644 packages/vue-query/src/useQuery.ts create mode 100644 packages/vue-query/src/useQueryClient.ts create mode 100644 packages/vue-query/src/utils.ts create mode 100644 packages/vue-query/src/vueQueryPlugin.ts create mode 100644 packages/vue-query/tsconfig.json create mode 100644 packages/vue-query/tsconfig.lint.json diff --git a/packages/vue-query/.eslintrc b/packages/vue-query/.eslintrc new file mode 100644 index 00000000000..c1895932146 --- /dev/null +++ b/packages/vue-query/.eslintrc @@ -0,0 +1,9 @@ +{ + "parserOptions": { + "project": "./tsconfig.lint.json", + "sourceType": "module" + }, + "rules": { + "react-hooks/rules-of-hooks": "off" + } +} diff --git a/packages/vue-query/__mocks__/vue-demi.ts b/packages/vue-query/__mocks__/vue-demi.ts new file mode 100644 index 00000000000..dde832fe382 --- /dev/null +++ b/packages/vue-query/__mocks__/vue-demi.ts @@ -0,0 +1,9 @@ +const vue = jest.requireActual("vue-demi"); + +module.exports = { + ...vue, + inject: jest.fn(), + provide: jest.fn(), + onScopeDispose: jest.fn(), + getCurrentInstance: jest.fn(() => ({ proxy: {} })), +}; diff --git a/packages/vue-query/package.json b/packages/vue-query/package.json new file mode 100644 index 00000000000..670fce4dad9 --- /dev/null +++ b/packages/vue-query/package.json @@ -0,0 +1,55 @@ +{ + "name": "@tanstack/vue-query", + "version": "4.7.2", + "description": "Hooks for managing, caching and syncing asynchronous and remote data in Vue", + "author": "Damian Osipiuk", + "license": "MIT", + "repository": "tanstack/query", + "homepage": "https://tanstack.com/query", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "types": "build/lib/index.d.ts", + "main": "build/lib/index.js", + "module": "build/lib/index.esm.js", + "exports": { + ".": { + "types": "./build/lib/index.d.ts", + "import": "./build/lib/index.mjs", + "default": "./build/lib/index.js" + }, + "./package.json": "./package.json" + }, + "sideEffects": false, + "scripts": { + "clean": "rm -rf ./build", + "test:eslint": "../../node_modules/.bin/eslint --ext .ts ./src", + "test:jest": "../../node_modules/.bin/jest --config jest.config.js", + "test:jest:dev": "pnpm test:jest --watch" + }, + "files": [ + "build/lib/*", + "build/umd/*" + ], + "devDependencies": { + "@vue/composition-api": "1.7.1", + "vue": "^3.2.40", + "vue2": "npm:vue@2" + }, + "dependencies": { + "@tanstack/query-core": "workspace:*", + "@vue/devtools-api": "^6.4.2", + "match-sorter": "^6.3.1", + "vue-demi": "^0.13.11" + }, + "peerDependencies": { + "@vue/composition-api": "^1.1.2", + "vue": "^2.5.0 || ^3.0.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } +} diff --git a/packages/vue-query/src/__mocks__/useBaseQuery.ts b/packages/vue-query/src/__mocks__/useBaseQuery.ts new file mode 100644 index 00000000000..4072f1b692d --- /dev/null +++ b/packages/vue-query/src/__mocks__/useBaseQuery.ts @@ -0,0 +1,3 @@ +const { useBaseQuery: originImpl } = jest.requireActual('../useBaseQuery') + +export const useBaseQuery = jest.fn(originImpl) diff --git a/packages/vue-query/src/__mocks__/useQueryClient.ts b/packages/vue-query/src/__mocks__/useQueryClient.ts new file mode 100644 index 00000000000..cd7484f8ba6 --- /dev/null +++ b/packages/vue-query/src/__mocks__/useQueryClient.ts @@ -0,0 +1,15 @@ +import { QueryClient } from '../queryClient' + +const queryClient = new QueryClient({ + logger: { + ...console, + error: () => { + // Noop + }, + }, + defaultOptions: { + queries: { retry: false, cacheTime: Infinity }, + }, +}) + +export const useQueryClient = jest.fn(() => queryClient) diff --git a/packages/vue-query/src/__tests__/mutationCache.test.ts b/packages/vue-query/src/__tests__/mutationCache.test.ts new file mode 100644 index 00000000000..dc617dd8632 --- /dev/null +++ b/packages/vue-query/src/__tests__/mutationCache.test.ts @@ -0,0 +1,40 @@ +import { ref } from 'vue-demi' +import { MutationCache as MutationCacheOrigin } from '@tanstack/query-core' + +import { MutationCache } from '../mutationCache' + +describe('MutationCache', () => { + beforeAll(() => { + jest.spyOn(MutationCacheOrigin.prototype, 'find') + jest.spyOn(MutationCacheOrigin.prototype, 'findAll') + }) + + describe('find', () => { + test('should properly unwrap parameters', async () => { + const mutationCache = new MutationCache() + + mutationCache.find({ + mutationKey: ref(['baz']), + }) + + expect(MutationCacheOrigin.prototype.find).toBeCalledWith({ + exact: true, + mutationKey: ['baz'], + }) + }) + }) + + describe('findAll', () => { + test('should properly unwrap parameters', async () => { + const mutationCache = new MutationCache() + + mutationCache.findAll({ + mutationKey: ref(['baz']), + }) + + expect(MutationCacheOrigin.prototype.findAll).toBeCalledWith({ + mutationKey: ['baz'], + }) + }) + }) +}) diff --git a/packages/vue-query/src/__tests__/queryCache.test.ts b/packages/vue-query/src/__tests__/queryCache.test.ts new file mode 100644 index 00000000000..7c28b91480e --- /dev/null +++ b/packages/vue-query/src/__tests__/queryCache.test.ts @@ -0,0 +1,54 @@ +import { ref } from 'vue-demi' +import { QueryCache as QueryCacheOrigin } from '@tanstack/query-core' + +import { QueryCache } from '../queryCache' + +describe('QueryCache', () => { + beforeAll(() => { + jest.spyOn(QueryCacheOrigin.prototype, 'find') + jest.spyOn(QueryCacheOrigin.prototype, 'findAll') + }) + + describe('find', () => { + test('should properly unwrap parameters', async () => { + const queryCache = new QueryCache() + + queryCache.find(['foo', ref('bar')], { + queryKey: ref(['baz']), + }) + + expect(QueryCacheOrigin.prototype.find).toBeCalledWith(['foo', 'bar'], { + queryKey: ['baz'], + }) + }) + }) + + describe('findAll', () => { + test('should properly unwrap two parameters', async () => { + const queryCache = new QueryCache() + + queryCache.findAll(['foo', ref('bar')], { + queryKey: ref(['baz']), + }) + + expect(QueryCacheOrigin.prototype.findAll).toBeCalledWith( + ['foo', 'bar'], + { + queryKey: ['baz'], + }, + ) + }) + + test('should properly unwrap one parameter', async () => { + const queryCache = new QueryCache() + + queryCache.findAll({ + queryKey: ref(['baz']), + }) + + expect(QueryCacheOrigin.prototype.findAll).toBeCalledWith({ + queryKey: ['baz'], + }) + }) + }) +}) diff --git a/packages/vue-query/src/__tests__/queryClient.test.ts b/packages/vue-query/src/__tests__/queryClient.test.ts new file mode 100644 index 00000000000..4f6728d4ae2 --- /dev/null +++ b/packages/vue-query/src/__tests__/queryClient.test.ts @@ -0,0 +1,546 @@ +import { ref } from 'vue-demi' +import { QueryClient as QueryClientOrigin } from '@tanstack/query-core' + +import { QueryClient } from '../queryClient' + +jest.mock('@tanstack/query-core') + +const queryKeyRef = ['foo', ref('bar')] +const queryKeyUnref = ['foo', 'bar'] + +const fn = () => 'mock' + +describe('QueryCache', () => { + // beforeAll(() => { + // jest.spyOn(QueryCacheOrigin.prototype, "find"); + // jest.spyOn(QueryCacheOrigin.prototype, "findAll"); + // }); + + describe('isFetching', () => { + test('should properly unwrap 1 parameter', async () => { + const queryClient = new QueryClient() + + queryClient.isFetching({ + queryKey: queryKeyRef, + }) + + expect(QueryClientOrigin.prototype.isFetching).toBeCalledWith({ + queryKey: queryKeyUnref, + }) + }) + + test('should properly unwrap 2 parameters', async () => { + const queryClient = new QueryClient() + + queryClient.isFetching(queryKeyRef, { + queryKey: queryKeyRef, + }) + + expect(QueryClientOrigin.prototype.isFetching).toBeCalledWith( + queryKeyUnref, + { + queryKey: queryKeyUnref, + }, + ) + }) + }) + + describe('isMutating', () => { + test('should properly unwrap 1 parameter', async () => { + const queryClient = new QueryClient() + + queryClient.isMutating({ + mutationKey: queryKeyRef, + }) + + expect(QueryClientOrigin.prototype.isMutating).toBeCalledWith({ + mutationKey: queryKeyUnref, + }) + }) + }) + + describe('getQueryData', () => { + test('should properly unwrap 2 parameter', async () => { + const queryClient = new QueryClient() + + queryClient.getQueryData(queryKeyRef, { + queryKey: queryKeyRef, + }) + + expect(QueryClientOrigin.prototype.getQueryData).toBeCalledWith( + queryKeyUnref, + { + queryKey: queryKeyUnref, + }, + ) + }) + }) + + describe('getQueriesData', () => { + test('should properly unwrap queryKey param', async () => { + const queryClient = new QueryClient() + + queryClient.getQueriesData(queryKeyRef) + + expect(QueryClientOrigin.prototype.getQueriesData).toBeCalledWith( + queryKeyUnref, + ) + }) + + test('should properly unwrap filters param', async () => { + const queryClient = new QueryClient() + + queryClient.getQueriesData({ queryKey: queryKeyRef }) + + expect(QueryClientOrigin.prototype.getQueriesData).toBeCalledWith({ + queryKey: queryKeyUnref, + }) + }) + }) + + describe('setQueryData', () => { + test('should properly unwrap 3 parameter', async () => { + const queryClient = new QueryClient() + + queryClient.setQueryData(queryKeyRef, fn, { updatedAt: ref(3) }) + + expect(QueryClientOrigin.prototype.setQueryData).toBeCalledWith( + queryKeyUnref, + fn, + { updatedAt: 3 }, + ) + }) + }) + + describe('setQueriesData', () => { + test('should properly unwrap params with queryKey', async () => { + const queryClient = new QueryClient() + + queryClient.setQueriesData(queryKeyRef, fn, { updatedAt: ref(3) }) + + expect(QueryClientOrigin.prototype.setQueriesData).toBeCalledWith( + queryKeyUnref, + fn, + { updatedAt: 3 }, + ) + }) + + test('should properly unwrap params with filters', async () => { + const queryClient = new QueryClient() + + queryClient.setQueriesData({ queryKey: queryKeyRef }, fn, { + updatedAt: ref(3), + }) + + expect(QueryClientOrigin.prototype.setQueriesData).toBeCalledWith( + { queryKey: queryKeyUnref }, + fn, + { updatedAt: 3 }, + ) + }) + }) + + describe('getQueryState', () => { + test('should properly unwrap 2 parameter', async () => { + const queryClient = new QueryClient() + + queryClient.getQueryState(queryKeyRef, { queryKey: queryKeyRef }) + + expect(QueryClientOrigin.prototype.getQueryState).toBeCalledWith( + queryKeyUnref, + { queryKey: queryKeyUnref }, + ) + }) + }) + + describe('removeQueries', () => { + test('should properly unwrap 1 parameter', async () => { + const queryClient = new QueryClient() + + queryClient.removeQueries({ + queryKey: queryKeyRef, + }) + + expect(QueryClientOrigin.prototype.removeQueries).toBeCalledWith({ + queryKey: queryKeyUnref, + }) + }) + + test('should properly unwrap 2 parameter', async () => { + const queryClient = new QueryClient() + + queryClient.removeQueries(queryKeyRef, { + queryKey: queryKeyRef, + }) + + expect(QueryClientOrigin.prototype.removeQueries).toBeCalledWith( + queryKeyUnref, + { + queryKey: queryKeyUnref, + }, + ) + }) + }) + + describe('resetQueries', () => { + test('should properly unwrap 2 parameter', async () => { + const queryClient = new QueryClient() + + queryClient.resetQueries( + { + queryKey: queryKeyRef, + }, + { cancelRefetch: ref(false) }, + ) + + expect(QueryClientOrigin.prototype.resetQueries).toBeCalledWith( + { + queryKey: queryKeyUnref, + }, + { cancelRefetch: false }, + ) + }) + + test('should properly unwrap 3 parameters', async () => { + const queryClient = new QueryClient() + + queryClient.resetQueries( + queryKeyRef, + { + queryKey: queryKeyRef, + }, + { cancelRefetch: ref(false) }, + ) + + expect(QueryClientOrigin.prototype.resetQueries).toBeCalledWith( + queryKeyUnref, + { + queryKey: queryKeyUnref, + }, + { cancelRefetch: false }, + ) + }) + }) + + describe('cancelQueries', () => { + test('should properly unwrap 2 parameter', async () => { + const queryClient = new QueryClient() + + queryClient.cancelQueries( + { + queryKey: queryKeyRef, + }, + { revert: ref(false) }, + ) + + expect(QueryClientOrigin.prototype.cancelQueries).toBeCalledWith( + { + queryKey: queryKeyUnref, + }, + { revert: false }, + ) + }) + + test('should properly unwrap 3 parameters', async () => { + const queryClient = new QueryClient() + + queryClient.cancelQueries( + queryKeyRef, + { + queryKey: queryKeyRef, + }, + { revert: ref(false) }, + ) + + expect(QueryClientOrigin.prototype.cancelQueries).toBeCalledWith( + queryKeyUnref, + { + queryKey: queryKeyUnref, + }, + { revert: false }, + ) + }) + }) + + describe('invalidateQueries', () => { + test('should properly unwrap 2 parameter', async () => { + const queryClient = new QueryClient() + + queryClient.invalidateQueries( + { + queryKey: queryKeyRef, + }, + { cancelRefetch: ref(false) }, + ) + + expect(QueryClientOrigin.prototype.invalidateQueries).toBeCalledWith( + { + queryKey: queryKeyUnref, + }, + { cancelRefetch: false }, + ) + }) + + test('should properly unwrap 3 parameters', async () => { + const queryClient = new QueryClient() + + queryClient.invalidateQueries( + queryKeyRef, + { + queryKey: queryKeyRef, + }, + { cancelRefetch: ref(false) }, + ) + + expect(QueryClientOrigin.prototype.invalidateQueries).toBeCalledWith( + queryKeyUnref, + { + queryKey: queryKeyUnref, + }, + { cancelRefetch: false }, + ) + }) + }) + + describe('refetchQueries', () => { + test('should properly unwrap 2 parameter', async () => { + const queryClient = new QueryClient() + + queryClient.refetchQueries( + { + queryKey: queryKeyRef, + }, + { cancelRefetch: ref(false) }, + ) + + expect(QueryClientOrigin.prototype.refetchQueries).toBeCalledWith( + { + queryKey: queryKeyUnref, + }, + { cancelRefetch: false }, + ) + }) + + test('should properly unwrap 3 parameters', async () => { + const queryClient = new QueryClient() + + queryClient.refetchQueries( + queryKeyRef, + { + queryKey: queryKeyRef, + }, + { cancelRefetch: ref(false) }, + ) + + expect(QueryClientOrigin.prototype.refetchQueries).toBeCalledWith( + queryKeyUnref, + { + queryKey: queryKeyUnref, + }, + { cancelRefetch: false }, + ) + }) + }) + + describe('fetchQuery', () => { + test('should properly unwrap 1 parameter', async () => { + const queryClient = new QueryClient() + + queryClient.fetchQuery({ + queryKey: queryKeyRef, + }) + + expect(QueryClientOrigin.prototype.fetchQuery).toBeCalledWith({ + queryKey: queryKeyUnref, + }) + }) + + test('should properly unwrap 2 parameters', async () => { + const queryClient = new QueryClient() + + queryClient.fetchQuery(queryKeyRef, { + queryKey: queryKeyRef, + }) + + expect(QueryClientOrigin.prototype.fetchQuery).toBeCalledWith( + queryKeyUnref, + { + queryKey: queryKeyUnref, + }, + undefined, + ) + }) + + test('should properly unwrap 3 parameters', async () => { + const queryClient = new QueryClient() + + queryClient.fetchQuery(queryKeyRef, fn, { + queryKey: queryKeyRef, + }) + + expect(QueryClientOrigin.prototype.fetchQuery).toBeCalledWith( + queryKeyUnref, + fn, + { + queryKey: queryKeyUnref, + }, + ) + }) + }) + + describe('prefetchQuery', () => { + test('should properly unwrap parameters', async () => { + const queryClient = new QueryClient() + + queryClient.prefetchQuery(queryKeyRef, fn, { queryKey: queryKeyRef }) + + expect(QueryClientOrigin.prototype.prefetchQuery).toBeCalledWith( + queryKeyUnref, + fn, + { + queryKey: queryKeyUnref, + }, + ) + }) + }) + + describe('fetchInfiniteQuery', () => { + test('should properly unwrap 1 parameter', async () => { + const queryClient = new QueryClient() + + queryClient.fetchInfiniteQuery({ + queryKey: queryKeyRef, + }) + + expect(QueryClientOrigin.prototype.fetchInfiniteQuery).toBeCalledWith({ + queryKey: queryKeyUnref, + }) + }) + + test('should properly unwrap 2 parameters', async () => { + const queryClient = new QueryClient() + + queryClient.fetchInfiniteQuery(queryKeyRef, { + queryKey: queryKeyRef, + }) + + expect(QueryClientOrigin.prototype.fetchInfiniteQuery).toBeCalledWith( + queryKeyUnref, + { + queryKey: queryKeyUnref, + }, + undefined, + ) + }) + + test('should properly unwrap 3 parameters', async () => { + const queryClient = new QueryClient() + + queryClient.fetchInfiniteQuery(queryKeyRef, fn, { + queryKey: queryKeyRef, + }) + + expect(QueryClientOrigin.prototype.fetchInfiniteQuery).toBeCalledWith( + queryKeyUnref, + fn, + { + queryKey: queryKeyUnref, + }, + ) + }) + }) + + describe('prefetchInfiniteQuery', () => { + test('should properly unwrap parameters', async () => { + const queryClient = new QueryClient() + + queryClient.prefetchInfiniteQuery(queryKeyRef, fn, { + queryKey: queryKeyRef, + }) + + expect(QueryClientOrigin.prototype.prefetchInfiniteQuery).toBeCalledWith( + queryKeyUnref, + fn, + { + queryKey: queryKeyUnref, + }, + ) + }) + }) + + describe('setDefaultOptions', () => { + test('should properly unwrap parameters', async () => { + const queryClient = new QueryClient() + + queryClient.setDefaultOptions({ + queries: { + enabled: ref(false), + }, + }) + + expect(QueryClientOrigin.prototype.setDefaultOptions).toBeCalledWith({ + queries: { + enabled: false, + }, + }) + }) + }) + + describe('setQueryDefaults', () => { + test('should properly unwrap parameters', async () => { + const queryClient = new QueryClient() + + queryClient.setQueryDefaults(queryKeyRef, { + enabled: ref(false), + }) + + expect(QueryClientOrigin.prototype.setQueryDefaults).toBeCalledWith( + queryKeyUnref, + { + enabled: false, + }, + ) + }) + }) + + describe('getQueryDefaults', () => { + test('should properly unwrap parameters', async () => { + const queryClient = new QueryClient() + + queryClient.getQueryDefaults(queryKeyRef) + + expect(QueryClientOrigin.prototype.getQueryDefaults).toBeCalledWith( + queryKeyUnref, + ) + }) + }) + + describe('setMutationDefaults', () => { + test('should properly unwrap parameters', async () => { + const queryClient = new QueryClient() + + queryClient.setMutationDefaults(queryKeyRef, { + mutationKey: queryKeyRef, + }) + + expect(QueryClientOrigin.prototype.setMutationDefaults).toBeCalledWith( + queryKeyUnref, + { + mutationKey: queryKeyUnref, + }, + ) + }) + }) + + describe('getMutationDefaults', () => { + test('should properly unwrap parameters', async () => { + const queryClient = new QueryClient() + + queryClient.getMutationDefaults(queryKeyRef) + + expect(QueryClientOrigin.prototype.getMutationDefaults).toBeCalledWith( + queryKeyUnref, + ) + }) + }) +}) diff --git a/packages/vue-query/src/__tests__/test-utils.ts b/packages/vue-query/src/__tests__/test-utils.ts new file mode 100644 index 00000000000..7844abdf340 --- /dev/null +++ b/packages/vue-query/src/__tests__/test-utils.ts @@ -0,0 +1,53 @@ +/* istanbul ignore file */ + +export function flushPromises(timeout = 0): Promise { + return new Promise(function (resolve) { + setTimeout(resolve, timeout) + }) +} + +export function simpleFetcher(): Promise { + return new Promise((resolve) => { + setTimeout(() => { + return resolve('Some data') + }, 0) + }) +} + +export function getSimpleFetcherWithReturnData(returnData: unknown) { + return () => + new Promise((resolve) => setTimeout(() => resolve(returnData), 0)) +} + +export function infiniteFetcher({ + pageParam = 0, +}: { + pageParam?: number +}): Promise { + return new Promise((resolve) => { + setTimeout(() => { + return resolve('data on page ' + pageParam) + }, 0) + }) +} + +export function rejectFetcher(): Promise { + return new Promise((resolve, reject) => { + setTimeout(() => { + return reject(new Error('Some error')) + }, 0) + }) +} + +export function successMutator(param: T): Promise { + return new Promise((resolve) => { + setTimeout(() => { + return resolve(param) + }, 0) + }) +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function errorMutator(param: T): Promise { + return rejectFetcher() +} diff --git a/packages/vue-query/src/__tests__/useInfiniteQuery.test.ts b/packages/vue-query/src/__tests__/useInfiniteQuery.test.ts new file mode 100644 index 00000000000..f887f3ce1e1 --- /dev/null +++ b/packages/vue-query/src/__tests__/useInfiniteQuery.test.ts @@ -0,0 +1,34 @@ +import { infiniteFetcher, flushPromises } from './test-utils' +import { useInfiniteQuery } from '../useInfiniteQuery' + +jest.mock('../useQueryClient') + +describe('useQuery', () => { + test('should properly execute infinite query', async () => { + const { data, fetchNextPage, status } = useInfiniteQuery( + ['infiniteQuery'], + infiniteFetcher, + ) + + expect(data.value).toStrictEqual(undefined) + expect(status.value).toStrictEqual('loading') + + await flushPromises() + + expect(data.value).toStrictEqual({ + pageParams: [undefined], + pages: ['data on page 0'], + }) + expect(status.value).toStrictEqual('success') + + fetchNextPage({ pageParam: 12 }) + + await flushPromises() + + expect(data.value).toStrictEqual({ + pageParams: [undefined, 12], + pages: ['data on page 0', 'data on page 12'], + }) + expect(status.value).toStrictEqual('success') + }) +}) diff --git a/packages/vue-query/src/__tests__/useIsFetching.test.ts b/packages/vue-query/src/__tests__/useIsFetching.test.ts new file mode 100644 index 00000000000..8475c0ab92c --- /dev/null +++ b/packages/vue-query/src/__tests__/useIsFetching.test.ts @@ -0,0 +1,91 @@ +import { onScopeDispose, reactive } from 'vue-demi' + +import { flushPromises, simpleFetcher } from './test-utils' +import { useQuery } from '../useQuery' +import { parseFilterArgs, useIsFetching } from '../useIsFetching' + +jest.mock('../useQueryClient') + +describe('useIsFetching', () => { + test('should properly return isFetching state', async () => { + const { isFetching: isFetchingQuery } = useQuery( + ['isFetching1'], + simpleFetcher, + ) + useQuery(['isFetching2'], simpleFetcher) + const isFetching = useIsFetching() + + expect(isFetchingQuery.value).toStrictEqual(true) + expect(isFetching.value).toStrictEqual(2) + + await flushPromises() + + expect(isFetchingQuery.value).toStrictEqual(false) + expect(isFetching.value).toStrictEqual(0) + }) + + test('should stop listening to changes on onScopeDispose', async () => { + const onScopeDisposeMock = onScopeDispose as jest.MockedFunction< + typeof onScopeDispose + > + onScopeDisposeMock.mockImplementation((fn) => fn()) + + const { status } = useQuery(['onScopeDispose'], simpleFetcher) + const isFetching = useIsFetching() + + expect(status.value).toStrictEqual('loading') + expect(isFetching.value).toStrictEqual(1) + + await flushPromises() + + expect(status.value).toStrictEqual('loading') + expect(isFetching.value).toStrictEqual(1) + + await flushPromises() + + expect(status.value).toStrictEqual('loading') + expect(isFetching.value).toStrictEqual(1) + + onScopeDisposeMock.mockReset() + }) + + test('should properly update filters', async () => { + const filter = reactive({ stale: false }) + useQuery( + ['isFetching'], + () => + new Promise((resolve) => { + setTimeout(() => { + return resolve('Some data') + }, 100) + }), + ) + const isFetching = useIsFetching(filter) + + expect(isFetching.value).toStrictEqual(0) + + filter.stale = true + await flushPromises() + + expect(isFetching.value).toStrictEqual(1) + + await flushPromises(100) + }) + + describe('parseFilterArgs', () => { + test('should default to empty filters', () => { + const result = parseFilterArgs(undefined) + + expect(result).toEqual({}) + }) + + test('should merge query key with filters', () => { + const filters = { stale: true } + + const result = parseFilterArgs(['key'], filters) + const expected = { ...filters, queryKey: ['key'] } + + expect(result).toEqual(expected) + }) + }) +}) diff --git a/packages/vue-query/src/__tests__/useIsMutating.test.ts b/packages/vue-query/src/__tests__/useIsMutating.test.ts new file mode 100644 index 00000000000..37091bb198b --- /dev/null +++ b/packages/vue-query/src/__tests__/useIsMutating.test.ts @@ -0,0 +1,97 @@ +import { onScopeDispose, reactive } from 'vue-demi' + +import { flushPromises, successMutator } from './test-utils' +import { useMutation } from '../useMutation' +import { parseMutationFilterArgs, useIsMutating } from '../useIsMutating' +import { useQueryClient } from '../useQueryClient' + +jest.mock('../useQueryClient') + +describe('useIsMutating', () => { + test('should properly return isMutating state', async () => { + const mutation = useMutation((params: string) => successMutator(params)) + const mutation2 = useMutation((params: string) => successMutator(params)) + const isMutating = useIsMutating() + + expect(isMutating.value).toStrictEqual(0) + + mutation.mutateAsync('a') + mutation2.mutateAsync('b') + + await flushPromises() + + expect(isMutating.value).toStrictEqual(2) + + await flushPromises() + + expect(isMutating.value).toStrictEqual(0) + }) + + test('should stop listening to changes on onScopeDispose', async () => { + const onScopeDisposeMock = onScopeDispose as jest.MockedFunction< + typeof onScopeDispose + > + onScopeDisposeMock.mockImplementation((fn) => fn()) + + const mutation = useMutation((params: string) => successMutator(params)) + const mutation2 = useMutation((params: string) => successMutator(params)) + const isMutating = useIsMutating() + + expect(isMutating.value).toStrictEqual(0) + + mutation.mutateAsync('a') + mutation2.mutateAsync('b') + + await flushPromises() + + expect(isMutating.value).toStrictEqual(0) + + await flushPromises() + + expect(isMutating.value).toStrictEqual(0) + + onScopeDisposeMock.mockReset() + }) + + test('should call `useQueryClient` with a proper `queryClientKey`', async () => { + const queryClientKey = 'foo' + useIsMutating({ queryClientKey }) + + expect(useQueryClient).toHaveBeenCalledWith(queryClientKey) + }) + + test('should properly update filters', async () => { + const filter = reactive({ mutationKey: ['foo'] }) + const { mutate } = useMutation(['isMutating'], (params: string) => + successMutator(params), + ) + mutate('foo') + + const isMutating = useIsMutating(filter) + + expect(isMutating.value).toStrictEqual(0) + + filter.mutationKey = ['isMutating'] + + await flushPromises() + + expect(isMutating.value).toStrictEqual(1) + }) + + describe('parseMutationFilterArgs', () => { + test('should default to empty filters', () => { + const result = parseMutationFilterArgs(undefined) + + expect(result).toEqual({}) + }) + + test('should merge mutation key with filters', () => { + const filters = { fetching: true } + + const result = parseMutationFilterArgs(['key'], filters) + const expected = { ...filters, mutationKey: ['key'] } + + expect(result).toEqual(expected) + }) + }) +}) diff --git a/packages/vue-query/src/__tests__/useMutation.test.ts b/packages/vue-query/src/__tests__/useMutation.test.ts new file mode 100644 index 00000000000..f28c74e1895 --- /dev/null +++ b/packages/vue-query/src/__tests__/useMutation.test.ts @@ -0,0 +1,289 @@ +import { reactive } from 'vue-demi' +import { errorMutator, flushPromises, successMutator } from './test-utils' +import { parseMutationArgs, useMutation } from '../useMutation' +import { useQueryClient } from '../useQueryClient' + +jest.mock('../useQueryClient') + +describe('useMutation', () => { + test('should be in idle state initially', () => { + const mutation = useMutation((params) => successMutator(params)) + + expect(mutation).toMatchObject({ + isIdle: { value: true }, + isLoading: { value: false }, + isError: { value: false }, + isSuccess: { value: false }, + }) + }) + + test('should change state after invoking mutate', () => { + const result = 'Mock data' + const mutation = useMutation((params: string) => successMutator(params)) + + mutation.mutate(result) + + expect(mutation).toMatchObject({ + isIdle: { value: false }, + isLoading: { value: true }, + isError: { value: false }, + isSuccess: { value: false }, + data: { value: undefined }, + error: { value: null }, + }) + }) + + test('should return error when request fails', async () => { + const mutation = useMutation(errorMutator) + + mutation.mutate() + + await flushPromises(10) + + expect(mutation).toMatchObject({ + isIdle: { value: false }, + isLoading: { value: false }, + isError: { value: true }, + isSuccess: { value: false }, + data: { value: undefined }, + error: { value: Error('Some error') }, + }) + }) + + test('should return data when request succeeds', async () => { + const result = 'Mock data' + const mutation = useMutation((params: string) => successMutator(params)) + + mutation.mutate(result) + + await flushPromises(10) + + expect(mutation).toMatchObject({ + isIdle: { value: false }, + isLoading: { value: false }, + isError: { value: false }, + isSuccess: { value: true }, + data: { value: 'Mock data' }, + error: { value: null }, + }) + }) + + test('should update reactive options', async () => { + const queryClient = useQueryClient() + const mutationCache = queryClient.getMutationCache() + const options = reactive({ mutationKey: ['foo'] }) + const mutation = useMutation( + (params: string) => successMutator(params), + options, + ) + + options.mutationKey = ['bar'] + await flushPromises() + mutation.mutate('xyz') + + await flushPromises() + + const mutations = mutationCache.find({ mutationKey: ['bar'] }) + + expect(mutations?.options.mutationKey).toEqual(['bar']) + }) + + test('should reset state after invoking mutation.reset', async () => { + const mutation = useMutation((params: string) => errorMutator(params)) + + mutation.mutate('') + + await flushPromises(10) + + mutation.reset() + + expect(mutation).toMatchObject({ + isIdle: { value: true }, + isLoading: { value: false }, + isError: { value: false }, + isSuccess: { value: false }, + data: { value: undefined }, + error: { value: null }, + }) + }) + + describe('side effects', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + test('should call onMutate when passed as an option', async () => { + const onMutate = jest.fn() + const mutation = useMutation((params: string) => successMutator(params), { + onMutate, + }) + + mutation.mutate('') + + await flushPromises(10) + + expect(onMutate).toHaveBeenCalledTimes(1) + }) + + test('should call onError when passed as an option', async () => { + const onError = jest.fn() + const mutation = useMutation((params: string) => errorMutator(params), { + onError, + }) + + mutation.mutate('') + + await flushPromises(10) + + expect(onError).toHaveBeenCalledTimes(1) + }) + + test('should call onSuccess when passed as an option', async () => { + const onSuccess = jest.fn() + const mutation = useMutation((params: string) => successMutator(params), { + onSuccess, + }) + + mutation.mutate('') + + await flushPromises(10) + + expect(onSuccess).toHaveBeenCalledTimes(1) + }) + + test('should call onSettled when passed as an option', async () => { + const onSettled = jest.fn() + const mutation = useMutation((params: string) => successMutator(params), { + onSettled, + }) + + mutation.mutate('') + + await flushPromises(10) + + expect(onSettled).toHaveBeenCalledTimes(1) + }) + + test('should call onError when passed as an argument of mutate function', async () => { + const onError = jest.fn() + const mutation = useMutation((params: string) => errorMutator(params)) + + mutation.mutate('', { onError }) + + await flushPromises(10) + + expect(onError).toHaveBeenCalledTimes(1) + }) + + test('should call onSuccess when passed as an argument of mutate function', async () => { + const onSuccess = jest.fn() + const mutation = useMutation((params: string) => successMutator(params)) + + mutation.mutate('', { onSuccess }) + + await flushPromises(10) + + expect(onSuccess).toHaveBeenCalledTimes(1) + }) + + test('should call onSettled when passed as an argument of mutate function', async () => { + const onSettled = jest.fn() + const mutation = useMutation((params: string) => successMutator(params)) + + mutation.mutate('', { onSettled }) + + await flushPromises(10) + + expect(onSettled).toHaveBeenCalledTimes(1) + }) + + test('should fire both onSettled functions', async () => { + const onSettled = jest.fn() + const onSettledOnFunction = jest.fn() + const mutation = useMutation((params: string) => successMutator(params), { + onSettled, + }) + + mutation.mutate('', { onSettled: onSettledOnFunction }) + + await flushPromises(10) + + expect(onSettled).toHaveBeenCalledTimes(1) + expect(onSettledOnFunction).toHaveBeenCalledTimes(1) + }) + }) + + describe('async', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + test('should resolve properly', async () => { + const result = 'Mock data' + const mutation = useMutation((params: string) => successMutator(params)) + + await expect(mutation.mutateAsync(result)).resolves.toBe(result) + + expect(mutation).toMatchObject({ + isIdle: { value: false }, + isLoading: { value: false }, + isError: { value: false }, + isSuccess: { value: true }, + data: { value: 'Mock data' }, + error: { value: null }, + }) + }) + + test('should throw on error', async () => { + const mutation = useMutation(errorMutator) + + await expect(mutation.mutateAsync()).rejects.toThrowError('Some error') + + expect(mutation).toMatchObject({ + isIdle: { value: false }, + isLoading: { value: false }, + isError: { value: true }, + isSuccess: { value: false }, + data: { value: undefined }, + error: { value: Error('Some error') }, + }) + }) + }) + + describe('parseMutationArgs', () => { + test('should return the same instance of options', () => { + const options = { retry: false } + const result = parseMutationArgs(options) + + expect(result).toEqual(options) + }) + + test('should merge query key with options', () => { + const options = { retry: false } + const result = parseMutationArgs(['key'], options) + const expected = { ...options, mutationKey: ['key'] } + + expect(result).toEqual(expected) + }) + + test('should merge query fn with options', () => { + const options = { retry: false } + const result = parseMutationArgs(successMutator, options) + const expected = { ...options, mutationFn: successMutator } + + expect(result).toEqual(expected) + }) + + test('should merge query key and fn with options', () => { + const options = { retry: false } + const result = parseMutationArgs(['key'], successMutator, options) + const expected = { + ...options, + mutationKey: ['key'], + mutationFn: successMutator, + } + + expect(result).toEqual(expected) + }) + }) +}) diff --git a/packages/vue-query/src/__tests__/useQueries.test.ts b/packages/vue-query/src/__tests__/useQueries.test.ts new file mode 100644 index 00000000000..67f28869c11 --- /dev/null +++ b/packages/vue-query/src/__tests__/useQueries.test.ts @@ -0,0 +1,193 @@ +import { onScopeDispose, reactive } from 'vue-demi' + +import { + flushPromises, + rejectFetcher, + simpleFetcher, + getSimpleFetcherWithReturnData, +} from './test-utils' +import { useQueries } from '../useQueries' + +jest.mock('../useQueryClient') + +describe('useQueries', () => { + test('should return result for each query', () => { + const queries = [ + { + queryKey: ['key1'], + queryFn: simpleFetcher, + }, + { + queryKey: ['key2'], + queryFn: simpleFetcher, + }, + ] + const queriesState = useQueries({ queries }) + + expect(queriesState).toMatchObject([ + { + status: 'loading', + isLoading: true, + isFetching: true, + isStale: true, + }, + { + status: 'loading', + isLoading: true, + isFetching: true, + isStale: true, + }, + ]) + }) + + test('should resolve to success and update reactive state', async () => { + const queries = [ + { + queryKey: ['key11'], + queryFn: simpleFetcher, + }, + { + queryKey: ['key12'], + queryFn: simpleFetcher, + }, + ] + const queriesState = useQueries({ queries }) + + await flushPromises() + + expect(queriesState).toMatchObject([ + { + status: 'success', + isLoading: false, + isFetching: false, + isStale: true, + }, + { + status: 'success', + isLoading: false, + isFetching: false, + isStale: true, + }, + ]) + }) + + test('should reject one of the queries and update reactive state', async () => { + const queries = [ + { + queryKey: ['key21'], + queryFn: rejectFetcher, + }, + { + queryKey: ['key22'], + queryFn: simpleFetcher, + }, + ] + const queriesState = useQueries({ queries }) + + await flushPromises() + + expect(queriesState).toMatchObject([ + { + status: 'error', + isLoading: false, + isFetching: false, + isStale: true, + }, + { + status: 'success', + isLoading: false, + isFetching: false, + isStale: true, + }, + ]) + }) + + test('should return state for new queries', async () => { + const queries = reactive([ + { + queryKey: ['key31'], + queryFn: getSimpleFetcherWithReturnData('value31'), + }, + { + queryKey: ['key32'], + queryFn: getSimpleFetcherWithReturnData('value32'), + }, + { + queryKey: ['key33'], + queryFn: getSimpleFetcherWithReturnData('value33'), + }, + ]) + const queriesState = useQueries({ queries }) + + await flushPromises() + + queries.splice( + 0, + queries.length, + { + queryKey: ['key31'], + queryFn: getSimpleFetcherWithReturnData('value31'), + }, + { + queryKey: ['key34'], + queryFn: getSimpleFetcherWithReturnData('value34'), + }, + ) + + await flushPromises() + await flushPromises() + + expect(queriesState.length).toEqual(2) + expect(queriesState).toMatchObject([ + { + data: 'value31', + status: 'success', + isLoading: false, + isFetching: false, + isStale: true, + }, + { + data: 'value34', + status: 'success', + isLoading: false, + isFetching: false, + isStale: true, + }, + ]) + }) + + test('should stop listening to changes on onScopeDispose', async () => { + const onScopeDisposeMock = onScopeDispose as jest.MockedFunction< + typeof onScopeDispose + > + onScopeDisposeMock.mockImplementationOnce((fn) => fn()) + + const queries = [ + { + queryKey: ['key41'], + queryFn: simpleFetcher, + }, + { + queryKey: ['key42'], + queryFn: simpleFetcher, + }, + ] + const queriesState = useQueries({ queries }) + await flushPromises() + + expect(queriesState).toMatchObject([ + { + status: 'loading', + isLoading: true, + isFetching: true, + isStale: true, + }, + { + status: 'loading', + isLoading: true, + isFetching: true, + isStale: true, + }, + ]) + }) +}) diff --git a/packages/vue-query/src/__tests__/useQuery.test.ts b/packages/vue-query/src/__tests__/useQuery.test.ts new file mode 100644 index 00000000000..456eeb8cf24 --- /dev/null +++ b/packages/vue-query/src/__tests__/useQuery.test.ts @@ -0,0 +1,283 @@ +import { + computed, + reactive, + ref, + onScopeDispose, + getCurrentInstance, +} from 'vue-demi' +import { QueryObserver } from '@tanstack/query-core' + +import { + flushPromises, + rejectFetcher, + simpleFetcher, + getSimpleFetcherWithReturnData, +} from './test-utils' +import { useQuery } from '../useQuery' +import { useBaseQuery } from '../useBaseQuery' + +jest.mock('../useQueryClient') +jest.mock('../useBaseQuery') + +describe('useQuery', () => { + test('should properly execute query', () => { + useQuery(['key0'], simpleFetcher, { staleTime: 1000 }) + + expect(useBaseQuery).toBeCalledWith( + QueryObserver, + ['key0'], + simpleFetcher, + { + staleTime: 1000, + }, + ) + }) + + test('should return loading status initially', () => { + const query = useQuery(['key1'], simpleFetcher) + + expect(query).toMatchObject({ + status: { value: 'loading' }, + isLoading: { value: true }, + isFetching: { value: true }, + isStale: { value: true }, + }) + }) + + test('should resolve to success and update reactive state: useQuery(key, dataFn)', async () => { + const query = useQuery(['key2'], getSimpleFetcherWithReturnData('result2')) + + await flushPromises() + + expect(query).toMatchObject({ + status: { value: 'success' }, + data: { value: 'result2' }, + isLoading: { value: false }, + isFetching: { value: false }, + isFetched: { value: true }, + isSuccess: { value: true }, + }) + }) + + test('should resolve to success and update reactive state: useQuery(optionsObj)', async () => { + const query = useQuery({ + queryKey: ['key31'], + queryFn: getSimpleFetcherWithReturnData('result31'), + enabled: true, + }) + + await flushPromises() + + expect(query).toMatchObject({ + status: { value: 'success' }, + data: { value: 'result31' }, + isLoading: { value: false }, + isFetching: { value: false }, + isFetched: { value: true }, + isSuccess: { value: true }, + }) + }) + + test('should resolve to success and update reactive state: useQuery(key, optionsObj)', async () => { + const query = useQuery(['key32'], { + queryFn: getSimpleFetcherWithReturnData('result32'), + enabled: true, + }) + + await flushPromises() + + expect(query).toMatchObject({ + status: { value: 'success' }, + data: { value: 'result32' }, + isLoading: { value: false }, + isFetching: { value: false }, + isFetched: { value: true }, + isSuccess: { value: true }, + }) + }) + + test('should reject and update reactive state', async () => { + const query = useQuery(['key3'], rejectFetcher) + + await flushPromises() + + expect(query).toMatchObject({ + status: { value: 'error' }, + data: { value: undefined }, + error: { value: { message: 'Some error' } }, + isLoading: { value: false }, + isFetching: { value: false }, + isFetched: { value: true }, + isError: { value: true }, + failureCount: { value: 1 }, + }) + }) + + test('should update query on reactive options object change', async () => { + const spy = jest.fn() + const onSuccess = ref(() => { + // Noop + }) + useQuery( + ['key6'], + simpleFetcher, + reactive({ + onSuccess, + staleTime: 1000, + }), + ) + + onSuccess.value = spy + + await flushPromises() + + expect(spy).toBeCalledTimes(1) + }) + + test('should update query on reactive (Ref) key change', async () => { + const secondKeyRef = ref('key7') + const query = useQuery(['key6', secondKeyRef], simpleFetcher) + + await flushPromises() + + expect(query).toMatchObject({ + status: { value: 'success' }, + }) + + secondKeyRef.value = 'key8' + await flushPromises() + + expect(query).toMatchObject({ + status: { value: 'loading' }, + data: { value: undefined }, + }) + + await flushPromises() + + expect(query).toMatchObject({ + status: { value: 'success' }, + }) + }) + + test("should update query when an option is passed as Ref and it's changed", async () => { + const enabled = ref(false) + const query = useQuery(['key9'], simpleFetcher, { enabled }) + + await flushPromises() + + expect(query).toMatchObject({ + fetchStatus: { value: 'idle' }, + data: { value: undefined }, + }) + + enabled.value = true + + await flushPromises() + + expect(query).toMatchObject({ + fetchStatus: { value: 'fetching' }, + data: { value: undefined }, + }) + + await flushPromises() + + expect(query).toMatchObject({ + status: { value: 'success' }, + }) + }) + + test('should properly execute dependant queries', async () => { + const { data } = useQuery(['dependant1'], simpleFetcher) + + const enabled = computed(() => !!data.value) + + const { fetchStatus, status } = useQuery( + ['dependant2'], + simpleFetcher, + reactive({ + enabled, + }), + ) + + expect(data.value).toStrictEqual(undefined) + expect(fetchStatus.value).toStrictEqual('idle') + + await flushPromises() + + expect(data.value).toStrictEqual('Some data') + expect(fetchStatus.value).toStrictEqual('fetching') + + await flushPromises() + + expect(fetchStatus.value).toStrictEqual('idle') + expect(status.value).toStrictEqual('success') + }) + + test('should stop listening to changes on onScopeDispose', async () => { + const onScopeDisposeMock = onScopeDispose as jest.MockedFunction< + typeof onScopeDispose + > + onScopeDisposeMock.mockImplementationOnce((fn) => fn()) + + const { status } = useQuery(['onScopeDispose'], simpleFetcher) + + expect(status.value).toStrictEqual('loading') + + await flushPromises() + + expect(status.value).toStrictEqual('loading') + + await flushPromises() + + expect(status.value).toStrictEqual('loading') + }) + + describe('suspense', () => { + test('should return a Promise', () => { + const getCurrentInstanceSpy = getCurrentInstance as jest.Mock + getCurrentInstanceSpy.mockImplementation(() => ({ suspense: {} })) + + const query = useQuery(['suspense'], simpleFetcher) + const result = query.suspense() + + expect(result).toBeInstanceOf(Promise) + }) + + test('should resolve after being enabled', () => { + const getCurrentInstanceSpy = getCurrentInstance as jest.Mock + getCurrentInstanceSpy.mockImplementation(() => ({ suspense: {} })) + + let afterTimeout = false + const isEnabeld = ref(false) + const query = useQuery(['suspense'], simpleFetcher, { + enabled: isEnabeld, + }) + + setTimeout(() => { + afterTimeout = true + isEnabeld.value = true + }, 200) + + return query.suspense().then(() => { + expect(afterTimeout).toBe(true) + }) + }) + + test('should resolve immidiately when stale without refetching', () => { + const getCurrentInstanceSpy = getCurrentInstance as jest.Mock + getCurrentInstanceSpy.mockImplementation(() => ({ suspense: {} })) + + const fetcherSpy = jest.fn(() => simpleFetcher()) + + // let afterTimeout = false; + const query = useQuery(['suspense'], simpleFetcher, { + staleTime: 10000, + initialData: 'foo', + }) + + return query.suspense().then(() => { + expect(fetcherSpy).toHaveBeenCalledTimes(0) + }) + }) + }) +}) diff --git a/packages/vue-query/src/__tests__/useQueryClient.test.ts b/packages/vue-query/src/__tests__/useQueryClient.test.ts new file mode 100644 index 00000000000..27861adf0e8 --- /dev/null +++ b/packages/vue-query/src/__tests__/useQueryClient.test.ts @@ -0,0 +1,49 @@ +import { getCurrentInstance, inject } from 'vue-demi' +import { useQueryClient } from '../useQueryClient' +import { VUE_QUERY_CLIENT } from '../utils' + +describe('useQueryClient', () => { + const injectSpy = inject as jest.Mock + const getCurrentInstanceSpy = getCurrentInstance as jest.Mock + + beforeEach(() => { + jest.restoreAllMocks() + }) + + test('should return queryClient when it is provided in the context', () => { + const queryClientMock = { name: 'Mocked client' } + injectSpy.mockReturnValueOnce(queryClientMock) + + const queryClient = useQueryClient() + + expect(queryClient).toStrictEqual(queryClientMock) + expect(injectSpy).toHaveBeenCalledTimes(1) + expect(injectSpy).toHaveBeenCalledWith(VUE_QUERY_CLIENT) + }) + + test('should throw an error when queryClient does not exist in the context', () => { + injectSpy.mockReturnValueOnce(undefined) + + expect(useQueryClient).toThrowError() + expect(injectSpy).toHaveBeenCalledTimes(1) + expect(injectSpy).toHaveBeenCalledWith(VUE_QUERY_CLIENT) + }) + + test('should throw an error when used outside of setup function', () => { + getCurrentInstanceSpy.mockReturnValueOnce(undefined) + + expect(useQueryClient).toThrowError() + expect(getCurrentInstanceSpy).toHaveBeenCalledTimes(1) + }) + + test('should call inject with a custom key as a suffix', () => { + const queryClientKey = 'foo' + const expectedKeyParameter = `${VUE_QUERY_CLIENT}:${queryClientKey}` + const queryClientMock = { name: 'Mocked client' } + injectSpy.mockReturnValueOnce(queryClientMock) + + useQueryClient(queryClientKey) + + expect(injectSpy).toHaveBeenCalledWith(expectedKeyParameter) + }) +}) diff --git a/packages/vue-query/src/__tests__/utils.test.ts b/packages/vue-query/src/__tests__/utils.test.ts new file mode 100644 index 00000000000..0b88356d4bf --- /dev/null +++ b/packages/vue-query/src/__tests__/utils.test.ts @@ -0,0 +1,154 @@ +import { isQueryKey, updateState, cloneDeep, cloneDeepUnref } from '../utils' +import { reactive, ref } from 'vue-demi' + +describe('utils', () => { + describe('isQueryKey', () => { + test('should detect an array as query key', () => { + expect(isQueryKey(['string', 'array'])).toEqual(true) + }) + }) + + describe('updateState', () => { + test('should update first object with values from the second one', () => { + const origin = { option1: 'a', option2: 'b', option3: 'c' } + const update = { option1: 'x', option2: 'y', option3: 'z' } + const expected = { option1: 'x', option2: 'y', option3: 'z' } + + updateState(origin, update) + expect(origin).toEqual(expected) + }) + + test('should update only existing keys', () => { + const origin = { option1: 'a', option2: 'b' } + const update = { option1: 'x', option2: 'y', option3: 'z' } + const expected = { option1: 'x', option2: 'y' } + + updateState(origin, update) + expect(origin).toEqual(expected) + }) + + test('should remove non existing keys', () => { + const origin = { option1: 'a', option2: 'b', option3: 'c' } + const update = { option1: 'x', option2: 'y' } + const expected = { option1: 'x', option2: 'y' } + + updateState(origin, update) + expect(origin).toEqual(expected) + }) + }) + + describe('cloneDeep', () => { + test('should copy primitives and functions AS-IS', () => { + expect(cloneDeep(3456)).toBe(3456) + expect(cloneDeep('theString')).toBe('theString') + expect(cloneDeep(null)).toBe(null) + }) + + test('should copy Maps and Sets AS-IS', () => { + const setVal = new Set([3, 4, 5]) + const setValCopy = cloneDeep(setVal) + expect(setValCopy).toBe(setVal) + expect(setValCopy).toStrictEqual(new Set([3, 4, 5])) + + const mapVal = new Map([ + ['a', 'aVal'], + ['b', 'bVal'], + ]) + const mapValCopy = cloneDeep(mapVal) + expect(mapValCopy).toBe(mapVal) + expect(mapValCopy).toStrictEqual( + new Map([ + ['a', 'aVal'], + ['b', 'bVal'], + ]), + ) + }) + + test('should deeply copy arrays', () => { + const val = [ + 25, + 'str', + null, + new Set([3, 4]), + [5, 6, { a: 1 }], + undefined, + ] + const cp = cloneDeep(val) + expect(cp).toStrictEqual([ + 25, + 'str', + null, + new Set([3, 4]), + [5, 6, { a: 1 }], + undefined, + ]) + expect(cp).not.toBe(val) + expect(cp[3]).toBe(val[3]) // Set([3, 4]) + expect(cp[4]).not.toBe(val[4]) // [5, 6, { a: 1 }] + expect((cp[4] as number[])[2]).not.toBe((val[4] as number[])[2]) // { a : 1 } + }) + + test('should deeply copy object', () => { + const val = reactive({ + a: 25, + b: 'str', + c: null, + d: undefined, + e: new Set([5, 6]), + f: [3, 4], + g: { fa: 26 }, + }) + const cp = cloneDeep(val) + + expect(cp).toStrictEqual({ + a: 25, + b: 'str', + c: null, + d: undefined, + e: new Set([5, 6]), + f: [3, 4], + g: { fa: 26 }, + }) + + expect(cp.e).toBe(val.e) // Set + expect(cp.f).not.toBe(val.f) // [] + expect(cp.g).not.toBe(val.g) // {} + }) + }) + + describe('cloneDeepUnref', () => { + test('should unref primitives', () => { + expect(cloneDeepUnref(ref(34))).toBe(34) + expect(cloneDeepUnref(ref('mystr'))).toBe('mystr') + }) + + test('should deeply unref arrays', () => { + const val = ref([2, 3, ref(4), ref('5'), { a: ref(6) }, [ref(7)]]) + const cp = cloneDeepUnref(val) + expect(cp).toStrictEqual([2, 3, 4, '5', { a: 6 }, [7]]) + }) + + test('should deeply unref objects', () => { + const val = ref({ + a: 1, + b: ref(2), + c: [ref('c1'), ref(['c2'])], + d: { + e: ref('e'), + }, + }) + const cp = cloneDeepUnref(val) + + expect(cp).toEqual({ + a: 1, + b: 2, + c: ['c1', ['c2']], + d: { e: 'e' }, + }) + }) + + test('should unref undefined', () => { + expect(cloneDeepUnref(ref(undefined))).toBe(undefined) + }) + }) +}) diff --git a/packages/vue-query/src/__tests__/vueQueryPlugin.test.ts b/packages/vue-query/src/__tests__/vueQueryPlugin.test.ts new file mode 100644 index 00000000000..1b56e948ae4 --- /dev/null +++ b/packages/vue-query/src/__tests__/vueQueryPlugin.test.ts @@ -0,0 +1,256 @@ +import type { App, ComponentOptions } from 'vue' +import { isVue2, isVue3 } from 'vue-demi' + +import type { QueryClient } from '../queryClient' +import { VueQueryPlugin } from '../vueQueryPlugin' +import { VUE_QUERY_CLIENT } from '../utils' +import { setupDevtools } from '../devtools/devtools' + +jest.mock('../devtools/devtools') + +interface TestApp extends App { + onUnmount: Function + _unmount: Function + _mixin: ComponentOptions + _provided: Record + $root: TestApp +} + +const testIf = (condition: boolean) => (condition ? test : test.skip) + +function getAppMock(withUnmountHook = false): TestApp { + const mock = { + provide: jest.fn(), + unmount: jest.fn(), + onUnmount: withUnmountHook + ? jest.fn((u: Function) => { + mock._unmount = u + }) + : undefined, + mixin: (m: ComponentOptions): any => { + mock._mixin = m + }, + } as unknown as TestApp + + return mock +} + +describe('VueQueryPlugin', () => { + beforeEach(() => { + window.__VUE_QUERY_CONTEXT__ = undefined + }) + + describe('devtools', () => { + test('should NOT setup devtools', () => { + const setupDevtoolsMock = setupDevtools as jest.Mock + const appMock = getAppMock() + VueQueryPlugin.install(appMock) + + expect(setupDevtoolsMock).toHaveBeenCalledTimes(0) + }) + + testIf(isVue2)('should setup devtools', () => { + const envCopy = process.env.NODE_ENV + process.env.NODE_ENV = 'development' + const setupDevtoolsMock = setupDevtools as jest.Mock + const appMock = getAppMock() + VueQueryPlugin.install(appMock) + + appMock.$root = appMock + appMock._mixin.beforeCreate?.call(appMock) + process.env.NODE_ENV = envCopy + + expect(setupDevtoolsMock).toHaveBeenCalledTimes(1) + }) + + testIf(isVue3)('should setup devtools', () => { + const envCopy = process.env.NODE_ENV + process.env.NODE_ENV = 'development' + const setupDevtoolsMock = setupDevtools as jest.Mock + const appMock = getAppMock() + VueQueryPlugin.install(appMock) + process.env.NODE_ENV = envCopy + + expect(setupDevtoolsMock).toHaveBeenCalledTimes(1) + }) + }) + + describe('when app unmounts', () => { + test('should call unmount on each client when onUnmount is missing', () => { + const appMock = getAppMock() + const customClient = { + mount: jest.fn(), + unmount: jest.fn(), + } as unknown as QueryClient + const originalUnmount = appMock.unmount + VueQueryPlugin.install(appMock, { + queryClient: customClient, + }) + + appMock.unmount() + + expect(appMock.unmount).not.toEqual(originalUnmount) + expect(customClient.unmount).toHaveBeenCalledTimes(1) + expect(originalUnmount).toHaveBeenCalledTimes(1) + }) + + test('should call onUnmount if present', () => { + const appMock = getAppMock(true) + const customClient = { + mount: jest.fn(), + unmount: jest.fn(), + } as unknown as QueryClient + const originalUnmount = appMock.unmount + VueQueryPlugin.install(appMock, { queryClient: customClient }) + + appMock._unmount() + + expect(appMock.unmount).toEqual(originalUnmount) + expect(customClient.unmount).toHaveBeenCalledTimes(1) + }) + }) + + describe('when called without additional options', () => { + testIf(isVue2)('should provide a client with default clientKey', () => { + const appMock = getAppMock() + VueQueryPlugin.install(appMock) + + appMock._mixin.beforeCreate?.call(appMock) + + expect(appMock._provided).toMatchObject({ + VUE_QUERY_CLIENT: expect.objectContaining({ defaultOptions: {} }), + }) + }) + + testIf(isVue3)('should provide a client with default clientKey', () => { + const appMock = getAppMock() + VueQueryPlugin.install(appMock) + + expect(appMock.provide).toHaveBeenCalledWith( + VUE_QUERY_CLIENT, + expect.objectContaining({ defaultOptions: {} }), + ) + }) + }) + + describe('when called with custom clientKey', () => { + testIf(isVue2)('should provide a client with customized clientKey', () => { + const appMock = getAppMock() + VueQueryPlugin.install(appMock, { queryClientKey: 'CUSTOM' }) + + appMock._mixin.beforeCreate?.call(appMock) + + expect(appMock._provided).toMatchObject({ + [VUE_QUERY_CLIENT + ':CUSTOM']: expect.objectContaining({ + defaultOptions: {}, + }), + }) + }) + + testIf(isVue3)('should provide a client with customized clientKey', () => { + const appMock = getAppMock() + VueQueryPlugin.install(appMock, { queryClientKey: 'CUSTOM' }) + + expect(appMock.provide).toHaveBeenCalledWith( + VUE_QUERY_CLIENT + ':CUSTOM', + expect.objectContaining({ defaultOptions: {} }), + ) + }) + }) + + describe('when called with custom client', () => { + testIf(isVue2)('should provide that custom client', () => { + const appMock = getAppMock() + const customClient = { mount: jest.fn() } as unknown as QueryClient + VueQueryPlugin.install(appMock, { queryClient: customClient }) + + appMock._mixin.beforeCreate?.call(appMock) + + expect(customClient.mount).toHaveBeenCalled() + expect(appMock._provided).toMatchObject({ + VUE_QUERY_CLIENT: customClient, + }) + }) + + testIf(isVue3)('should provide that custom client', () => { + const appMock = getAppMock() + const customClient = { mount: jest.fn() } as unknown as QueryClient + VueQueryPlugin.install(appMock, { queryClient: customClient }) + + expect(customClient.mount).toHaveBeenCalled() + expect(appMock.provide).toHaveBeenCalledWith( + VUE_QUERY_CLIENT, + customClient, + ) + }) + }) + + describe('when called with custom client config', () => { + testIf(isVue2)( + 'should instantiate a client with the provided config', + () => { + const appMock = getAppMock() + const config = { + defaultOptions: { queries: { enabled: true } }, + } + VueQueryPlugin.install(appMock, { + queryClientConfig: config, + }) + + appMock._mixin.beforeCreate?.call(appMock) + + expect(appMock._provided).toMatchObject({ + VUE_QUERY_CLIENT: expect.objectContaining(config), + }) + }, + ) + + testIf(isVue3)( + 'should instantiate a client with the provided config', + () => { + const appMock = getAppMock() + const config = { + defaultOptions: { queries: { enabled: true } }, + } + VueQueryPlugin.install(appMock, { + queryClientConfig: config, + }) + + expect(appMock.provide).toHaveBeenCalledWith( + VUE_QUERY_CLIENT, + expect.objectContaining(config), + ) + }, + ) + }) + + describe('when context sharing is enabled', () => { + test('should create context if it does not exist', () => { + const appMock = getAppMock() + VueQueryPlugin.install(appMock, { contextSharing: true }) + + expect(window.__VUE_QUERY_CONTEXT__).toBeTruthy() + }) + + test('should create context with options if it does not exist', () => { + const appMock = getAppMock() + VueQueryPlugin.install(appMock, { + contextSharing: true, + queryClientConfig: { defaultOptions: { queries: { staleTime: 5000 } } }, + }) + + expect( + window.__VUE_QUERY_CONTEXT__?.getDefaultOptions().queries?.staleTime, + ).toEqual(5000) + }) + + test('should use existing context', () => { + const customClient = { mount: jest.fn() } as unknown as QueryClient + window.__VUE_QUERY_CONTEXT__ = customClient + const appMock = getAppMock() + VueQueryPlugin.install(appMock, { contextSharing: true }) + + expect(customClient.mount).toHaveBeenCalledTimes(1) + }) + }) +}) diff --git a/packages/vue-query/src/devtools/devtools.ts b/packages/vue-query/src/devtools/devtools.ts new file mode 100644 index 00000000000..d0c91f35daa --- /dev/null +++ b/packages/vue-query/src/devtools/devtools.ts @@ -0,0 +1,199 @@ +/* istanbul ignore file */ + +import { setupDevtoolsPlugin } from '@vue/devtools-api' +import type { CustomInspectorNode } from '@vue/devtools-api' +import { matchSorter } from 'match-sorter' +import type { Query } from '@tanstack/query-core' +import type { QueryClient } from '../queryClient' +import { + getQueryStateLabel, + getQueryStatusBg, + getQueryStatusFg, + sortFns, +} from './utils' + +const pluginId = 'vue-query' +const pluginName = 'Vue Query' + +export function setupDevtools(app: any, queryClient: QueryClient) { + setupDevtoolsPlugin( + { + id: pluginId, + label: pluginName, + packageName: 'vue-query', + homepage: 'https://github.com/DamianOsipiuk/vue-query', + logo: 'https://vue-query.vercel.app/vue-query.svg', + app, + settings: { + baseSort: { + type: 'choice', + component: 'button-group', + label: 'Sort Cache Entries', + options: [ + { + label: 'ASC', + value: 1, + }, + { + label: 'DESC', + value: -1, + }, + ], + defaultValue: 1, + }, + sortFn: { + type: 'choice', + label: 'Sort Function', + options: Object.keys(sortFns).map((key) => ({ + label: key, + value: key, + })), + defaultValue: Object.keys(sortFns)[0]!, + }, + }, + }, + (api) => { + const queryCache = queryClient.getQueryCache() + + api.addInspector({ + id: pluginId, + label: pluginName, + icon: 'api', + nodeActions: [ + { + icon: 'cloud_download', + tooltip: 'Refetch', + action: (queryHash: string) => { + queryCache.get(queryHash)?.fetch() + }, + }, + { + icon: 'alarm', + tooltip: 'Invalidate', + action: (queryHash: string) => { + const query = queryCache.get(queryHash) as Query + queryClient.invalidateQueries(query.queryKey) + }, + }, + { + icon: 'settings_backup_restore', + tooltip: 'Reset', + action: (queryHash: string) => { + queryCache.get(queryHash)?.reset() + }, + }, + { + icon: 'delete', + tooltip: 'Remove', + action: (queryHash: string) => { + const query = queryCache.get(queryHash) as Query + queryCache.remove(query) + }, + }, + ], + }) + + api.addTimelineLayer({ + id: pluginId, + label: pluginName, + color: 0xffd94c, + }) + + queryCache.subscribe((event) => { + api.sendInspectorTree(pluginId) + api.sendInspectorState(pluginId) + + if ( + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + event && + ['queryAdded', 'queryRemoved', 'queryUpdated'].includes(event.type) + ) { + api.addTimelineEvent({ + layerId: pluginId, + event: { + title: event.type, + subtitle: event.query.queryHash, + time: api.now(), + data: { + queryHash: event.query.queryHash, + ...event, + }, + }, + }) + } + }) + + api.on.getInspectorTree((payload) => { + if (payload.inspectorId === pluginId) { + const queries: Query[] = queryCache.getAll() + const settings = api.getSettings() + const filtered = matchSorter(queries, payload.filter, { + keys: ['queryHash'], + baseSort: (a, b) => + sortFns[settings.sortFn]!(a.item, b.item) * settings.baseSort, + }) + + const nodes: CustomInspectorNode[] = filtered.map((query) => { + const stateLabel = getQueryStateLabel(query) + + return { + id: query.queryHash, + label: query.queryHash, + tags: [ + { + label: `${stateLabel} [${query.getObserversCount()}]`, + textColor: getQueryStatusFg(query), + backgroundColor: getQueryStatusBg(query), + }, + ], + } + }) + payload.rootNodes = nodes + } + }) + + api.on.getInspectorState((payload) => { + if (payload.inspectorId === pluginId) { + const query = queryCache.get(payload.nodeId) + + if (!query) { + return + } + + payload.state = { + ' Query Details': [ + { + key: 'Query key', + value: query.queryHash as string, + }, + { + key: 'Query status', + value: getQueryStateLabel(query), + }, + { + key: 'Observers', + value: query.getObserversCount(), + }, + { + key: 'Last Updated', + value: new Date(query.state.dataUpdatedAt).toLocaleTimeString(), + }, + ], + 'Data Explorer': [ + { + key: 'Data', + value: query.state.data, + }, + ], + 'Query Explorer': [ + { + key: 'Query', + value: query, + }, + ], + } + } + }) + }, + ) +} diff --git a/packages/vue-query/src/devtools/utils.ts b/packages/vue-query/src/devtools/utils.ts new file mode 100644 index 00000000000..eb96f1755ce --- /dev/null +++ b/packages/vue-query/src/devtools/utils.ts @@ -0,0 +1,99 @@ +/* istanbul ignore file */ + +import type { Query } from '@tanstack/query-core' + +type SortFn = (a: Query, b: Query) => number + +// eslint-disable-next-line no-shadow +enum QueryState { + Fetching = 0, + Fresh, + Stale, + Inactive, + Paused, +} + +export function getQueryState(query: Query): QueryState { + if (query.state.fetchStatus === 'fetching') { + return QueryState.Fetching + } + if (query.state.fetchStatus === 'paused') { + return QueryState.Paused + } + if (!query.getObserversCount()) { + return QueryState.Inactive + } + if (query.isStale()) { + return QueryState.Stale + } + + return QueryState.Fresh +} + +export function getQueryStateLabel(query: Query): string { + const queryState = getQueryState(query) + + if (queryState === QueryState.Fetching) { + return 'fetching' + } + if (queryState === QueryState.Paused) { + return 'paused' + } + if (queryState === QueryState.Stale) { + return 'stale' + } + if (queryState === QueryState.Inactive) { + return 'inactive' + } + + return 'fresh' +} + +export function getQueryStatusFg(query: Query): number { + const queryState = getQueryState(query) + + if (queryState === QueryState.Stale) { + return 0x000000 + } + + return 0xffffff +} + +export function getQueryStatusBg(query: Query): number { + const queryState = getQueryState(query) + + if (queryState === QueryState.Fetching) { + return 0x006bff + } + if (queryState === QueryState.Paused) { + return 0x8c49eb + } + if (queryState === QueryState.Stale) { + return 0xffb200 + } + if (queryState === QueryState.Inactive) { + return 0x3f4e60 + } + + return 0x008327 +} + +const queryHashSort: SortFn = (a, b) => + String(a.queryHash).localeCompare(b.queryHash) + +const dateSort: SortFn = (a, b) => + a.state.dataUpdatedAt < b.state.dataUpdatedAt ? 1 : -1 + +const statusAndDateSort: SortFn = (a, b) => { + if (getQueryState(a) === getQueryState(b)) { + return dateSort(a, b) + } + + return getQueryState(a) > getQueryState(b) ? 1 : -1 +} + +export const sortFns: Record = { + 'Status > Last Updated': statusAndDateSort, + 'Query Hash': queryHashSort, + 'Last Updated': dateSort, +} diff --git a/packages/vue-query/src/index.ts b/packages/vue-query/src/index.ts new file mode 100644 index 00000000000..5ef0ccfdb7b --- /dev/null +++ b/packages/vue-query/src/index.ts @@ -0,0 +1,26 @@ +/* istanbul ignore file */ + +export * from '@tanstack/query-core' + +export { useQueryClient } from './useQueryClient' +export { VueQueryPlugin } from './vueQueryPlugin' + +export { QueryClient } from './queryClient' +export { QueryCache } from './queryCache' +export { MutationCache } from './mutationCache' +export { useQuery } from './useQuery' +export { useQueries } from './useQueries' +export { useInfiniteQuery } from './useInfiniteQuery' +export { useMutation } from './useMutation' +export { useIsFetching } from './useIsFetching' +export { useIsMutating } from './useIsMutating' +export { VUE_QUERY_CLIENT } from './utils' + +export type { UseQueryReturnType } from './useBaseQuery' +export type { UseQueryOptions } from './useQuery' +export type { UseInfiniteQueryOptions } from './useInfiniteQuery' +export type { UseMutationOptions, UseMutationReturnType } from './useMutation' +export type { UseQueriesOptions, UseQueriesResults } from './useQueries' +export type { MutationFilters } from './useIsMutating' +export type { QueryFilters } from './useIsFetching' +export type { VueQueryPluginOptions } from './vueQueryPlugin' diff --git a/packages/vue-query/src/mutationCache.ts b/packages/vue-query/src/mutationCache.ts new file mode 100644 index 00000000000..d12edd522f8 --- /dev/null +++ b/packages/vue-query/src/mutationCache.ts @@ -0,0 +1,16 @@ +import { MutationCache as MC } from '@tanstack/query-core' +import type { Mutation, MutationFilters } from '@tanstack/query-core' +import type { MaybeRefDeep } from './types' +import { cloneDeepUnref } from './utils' + +export class MutationCache extends MC { + find( + filters: MaybeRefDeep, + ): Mutation | undefined { + return super.find(cloneDeepUnref(filters) as MutationFilters) + } + + findAll(filters: MaybeRefDeep): Mutation[] { + return super.findAll(cloneDeepUnref(filters) as MutationFilters) + } +} diff --git a/packages/vue-query/src/queryCache.ts b/packages/vue-query/src/queryCache.ts new file mode 100644 index 00000000000..04cfaad86c2 --- /dev/null +++ b/packages/vue-query/src/queryCache.ts @@ -0,0 +1,36 @@ +import { QueryCache as QC } from '@tanstack/query-core' +import type { Query, QueryKey, QueryFilters } from '@tanstack/query-core' +import type { MaybeRefDeep } from './types' +import { cloneDeepUnref, isQueryKey } from './utils' + +export class QueryCache extends QC { + find( + arg1: MaybeRefDeep, + arg2?: MaybeRefDeep, + ): Query | undefined { + const arg1Unreffed = cloneDeepUnref(arg1) + const arg2Unreffed = cloneDeepUnref(arg2) as QueryFilters + return super.find(arg1Unreffed, arg2Unreffed) + } + + findAll( + queryKey?: MaybeRefDeep, + filters?: MaybeRefDeep, + ): Query[] + findAll(filters?: MaybeRefDeep): Query[] + findAll( + arg1?: MaybeRefDeep, + arg2?: MaybeRefDeep, + ): Query[] + findAll( + arg1?: MaybeRefDeep | MaybeRefDeep, + arg2?: MaybeRefDeep, + ): Query[] { + const arg1Unreffed = cloneDeepUnref(arg1) as QueryKey | QueryFilters + const arg2Unreffed = cloneDeepUnref(arg2) as QueryFilters + if (isQueryKey(arg1Unreffed)) { + return super.findAll(arg1Unreffed, arg2Unreffed) + } + return super.findAll(arg1Unreffed) + } +} diff --git a/packages/vue-query/src/queryClient.ts b/packages/vue-query/src/queryClient.ts new file mode 100644 index 00000000000..27d1d073f5f --- /dev/null +++ b/packages/vue-query/src/queryClient.ts @@ -0,0 +1,568 @@ +import { QueryClient as QC } from '@tanstack/query-core' +import type { + QueryKey, + QueryClientConfig, + SetDataOptions, + ResetQueryFilters, + ResetOptions, + CancelOptions, + InvalidateQueryFilters, + InvalidateOptions, + RefetchQueryFilters, + RefetchOptions, + FetchQueryOptions, + QueryFunction, + FetchInfiniteQueryOptions, + InfiniteData, + DefaultOptions, + QueryObserverOptions, + MutationKey, + MutationObserverOptions, + QueryFilters, + MutationFilters, + QueryState, + Updater, +} from '@tanstack/query-core' +import type { MaybeRefDeep } from './types' +import { cloneDeepUnref, isQueryKey } from './utils' +import { QueryCache } from './queryCache' +import { MutationCache } from './mutationCache' + +export class QueryClient extends QC { + constructor(config: MaybeRefDeep = {}) { + const unreffedConfig = cloneDeepUnref(config) as QueryClientConfig + const vueQueryConfig: QueryClientConfig = { + logger: cloneDeepUnref(unreffedConfig.logger), + defaultOptions: cloneDeepUnref(unreffedConfig.defaultOptions), + queryCache: unreffedConfig.queryCache || new QueryCache(), + mutationCache: unreffedConfig.mutationCache || new MutationCache(), + } + super(vueQueryConfig) + } + + isFetching(filters?: MaybeRefDeep): number + isFetching( + queryKey?: MaybeRefDeep, + filters?: MaybeRefDeep, + ): number + isFetching( + arg1?: MaybeRefDeep, + arg2?: MaybeRefDeep, + ): number { + const arg1Unreffed = cloneDeepUnref(arg1) + const arg2Unreffed = cloneDeepUnref(arg2) as QueryFilters + if (isQueryKey(arg1Unreffed)) { + return super.isFetching(arg1Unreffed, arg2Unreffed) + } + return super.isFetching(arg1Unreffed as QueryFilters) + } + + isMutating(filters?: MaybeRefDeep): number { + return super.isMutating(cloneDeepUnref(filters) as MutationFilters) + } + + getQueryData( + queryKey: MaybeRefDeep, + filters?: MaybeRefDeep, + ): TData | undefined { + return super.getQueryData( + cloneDeepUnref(queryKey), + cloneDeepUnref(filters) as QueryFilters, + ) + } + + getQueriesData( + queryKey: MaybeRefDeep, + ): [QueryKey, TData | undefined][] + getQueriesData( + filters: MaybeRefDeep, + ): [QueryKey, TData | undefined][] + getQueriesData( + queryKeyOrFilters: MaybeRefDeep | MaybeRefDeep, + ): [QueryKey, TData | undefined][] { + const unreffed = cloneDeepUnref(queryKeyOrFilters) + if (isQueryKey(unreffed)) { + return super.getQueriesData(unreffed) + } + return super.getQueriesData(unreffed as QueryFilters) + } + + setQueryData( + queryKey: MaybeRefDeep, + updater: Updater, + options?: MaybeRefDeep, + ): TData | undefined { + return super.setQueryData( + cloneDeepUnref(queryKey), + updater, + cloneDeepUnref(options) as SetDataOptions, + ) + } + + setQueriesData( + queryKey: MaybeRefDeep, + updater: Updater, + options?: MaybeRefDeep, + ): [QueryKey, TData | undefined][] + setQueriesData( + filters: MaybeRefDeep, + updater: Updater, + options?: MaybeRefDeep, + ): [QueryKey, TData | undefined][] + setQueriesData( + queryKeyOrFilters: MaybeRefDeep, + updater: Updater, + options?: MaybeRefDeep, + ): [QueryKey, TData | undefined][] { + const arg1Unreffed = cloneDeepUnref(queryKeyOrFilters) + const arg3Unreffed = cloneDeepUnref(options) as SetDataOptions + if (isQueryKey(arg1Unreffed)) { + return super.setQueriesData(arg1Unreffed, updater, arg3Unreffed) + } + return super.setQueriesData( + arg1Unreffed as QueryFilters, + updater, + arg3Unreffed, + ) + } + + getQueryState( + queryKey: MaybeRefDeep, + filters?: MaybeRefDeep, + ): QueryState | undefined { + return super.getQueryState( + cloneDeepUnref(queryKey), + cloneDeepUnref(filters) as QueryFilters, + ) + } + + removeQueries(filters?: MaybeRefDeep): void + removeQueries( + queryKey?: MaybeRefDeep, + filters?: MaybeRefDeep, + ): void + removeQueries( + arg1?: MaybeRefDeep, + arg2?: MaybeRefDeep, + ): void { + const arg1Unreffed = cloneDeepUnref(arg1) + if (isQueryKey(arg1Unreffed)) { + return super.removeQueries( + arg1Unreffed, + cloneDeepUnref(arg2) as QueryFilters, + ) + } + return super.removeQueries(arg1Unreffed as QueryFilters) + } + + resetQueries( + filters?: MaybeRefDeep>, + options?: MaybeRefDeep, + ): Promise + resetQueries( + queryKey?: MaybeRefDeep, + filters?: MaybeRefDeep>, + options?: MaybeRefDeep, + ): Promise + resetQueries( + arg1?: MaybeRefDeep>, + arg2?: MaybeRefDeep | ResetOptions>, + arg3?: MaybeRefDeep, + ): Promise { + const arg1Unreffed = cloneDeepUnref(arg1) + const arg2Unreffed = cloneDeepUnref(arg2) + if (isQueryKey(arg1Unreffed)) { + return super.resetQueries( + arg1Unreffed, + arg2Unreffed as ResetQueryFilters | undefined, + cloneDeepUnref(arg3) as ResetOptions, + ) + } + return super.resetQueries( + arg1Unreffed as ResetQueryFilters, + arg2Unreffed as ResetOptions, + ) + } + + cancelQueries( + filters?: MaybeRefDeep, + options?: MaybeRefDeep, + ): Promise + cancelQueries( + queryKey?: MaybeRefDeep, + filters?: MaybeRefDeep, + options?: MaybeRefDeep, + ): Promise + cancelQueries( + arg1?: MaybeRefDeep, + arg2?: MaybeRefDeep, + arg3?: MaybeRefDeep, + ): Promise { + const arg1Unreffed = cloneDeepUnref(arg1) + const arg2Unreffed = cloneDeepUnref(arg2) + if (isQueryKey(arg1Unreffed)) { + return super.cancelQueries( + arg1Unreffed, + arg2Unreffed as QueryFilters | undefined, + cloneDeepUnref(arg3) as CancelOptions, + ) + } + return super.cancelQueries( + arg1Unreffed as QueryFilters, + arg2Unreffed as CancelOptions, + ) + } + + invalidateQueries( + filters?: MaybeRefDeep>, + options?: MaybeRefDeep, + ): Promise + invalidateQueries( + queryKey?: MaybeRefDeep, + filters?: MaybeRefDeep>, + options?: MaybeRefDeep, + ): Promise + invalidateQueries( + arg1?: MaybeRefDeep>, + arg2?: MaybeRefDeep | InvalidateOptions>, + arg3?: MaybeRefDeep, + ): Promise { + const arg1Unreffed = cloneDeepUnref(arg1) + const arg2Unreffed = cloneDeepUnref(arg2) + if (isQueryKey(arg1Unreffed)) { + return super.invalidateQueries( + arg1Unreffed, + arg2Unreffed as InvalidateQueryFilters | undefined, + cloneDeepUnref(arg3) as InvalidateOptions, + ) + } + return super.invalidateQueries( + arg1Unreffed as InvalidateQueryFilters, + arg2Unreffed as InvalidateOptions, + ) + } + + refetchQueries( + filters?: MaybeRefDeep>, + options?: MaybeRefDeep, + ): Promise + refetchQueries( + queryKey?: MaybeRefDeep, + filters?: MaybeRefDeep>, + options?: MaybeRefDeep, + ): Promise + refetchQueries( + arg1?: MaybeRefDeep>, + arg2?: MaybeRefDeep | RefetchOptions>, + arg3?: MaybeRefDeep, + ): Promise { + const arg1Unreffed = cloneDeepUnref(arg1) + const arg2Unreffed = cloneDeepUnref(arg2) + if (isQueryKey(arg1Unreffed)) { + return super.refetchQueries( + arg1Unreffed, + arg2Unreffed as RefetchQueryFilters | undefined, + cloneDeepUnref(arg3) as RefetchOptions, + ) + } + return super.refetchQueries( + arg1Unreffed as RefetchQueryFilters, + arg2Unreffed as RefetchOptions, + ) + } + + fetchQuery< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + >( + options: MaybeRefDeep< + FetchQueryOptions + >, + ): Promise + fetchQuery< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + >( + queryKey: MaybeRefDeep, + options?: MaybeRefDeep< + FetchQueryOptions + >, + ): Promise + fetchQuery< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + >( + queryKey: MaybeRefDeep, + queryFn: QueryFunction, + options?: MaybeRefDeep< + FetchQueryOptions + >, + ): Promise + fetchQuery< + TQueryFnData, + TError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + >( + arg1: + | MaybeRefDeep + | MaybeRefDeep>, + arg2?: + | QueryFunction + | MaybeRefDeep>, + arg3?: MaybeRefDeep< + FetchQueryOptions + >, + ): Promise { + const arg1Unreffed = cloneDeepUnref(arg1) + const arg2Unreffed = cloneDeepUnref(arg2) + if (isQueryKey(arg1Unreffed)) { + return super.fetchQuery( + arg1Unreffed as TQueryKey, + arg2Unreffed as QueryFunction, + cloneDeepUnref(arg3) as FetchQueryOptions< + TQueryFnData, + TError, + TData, + TQueryKey + >, + ) + } + return super.fetchQuery( + arg1Unreffed as FetchQueryOptions, + ) + } + + prefetchQuery< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + >( + options: MaybeRefDeep< + FetchQueryOptions + >, + ): Promise + prefetchQuery< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + >( + queryKey: MaybeRefDeep, + options?: MaybeRefDeep< + FetchQueryOptions + >, + ): Promise + prefetchQuery< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + >( + queryKey: MaybeRefDeep, + queryFn: QueryFunction, + options?: MaybeRefDeep< + FetchQueryOptions + >, + ): Promise + prefetchQuery< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + >( + arg1: MaybeRefDeep< + TQueryKey | FetchQueryOptions + >, + arg2?: + | QueryFunction + | MaybeRefDeep>, + arg3?: MaybeRefDeep< + FetchQueryOptions + >, + ): Promise { + return super.prefetchQuery( + cloneDeepUnref(arg1) as any, + cloneDeepUnref(arg2) as any, + cloneDeepUnref(arg3) as any, + ) + } + + fetchInfiniteQuery< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + >( + options: MaybeRefDeep< + FetchInfiniteQueryOptions + >, + ): Promise> + fetchInfiniteQuery< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + >( + queryKey: MaybeRefDeep, + options?: MaybeRefDeep< + FetchInfiniteQueryOptions + >, + ): Promise> + fetchInfiniteQuery< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + >( + queryKey: MaybeRefDeep, + queryFn: QueryFunction, + options?: MaybeRefDeep< + FetchInfiniteQueryOptions + >, + ): Promise> + fetchInfiniteQuery< + TQueryFnData, + TError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + >( + arg1: MaybeRefDeep< + | TQueryKey + | FetchInfiniteQueryOptions + >, + arg2?: + | QueryFunction + | MaybeRefDeep< + FetchInfiniteQueryOptions + >, + arg3?: MaybeRefDeep< + FetchInfiniteQueryOptions + >, + ): Promise> { + const arg1Unreffed = cloneDeepUnref(arg1) + const arg2Unreffed = cloneDeepUnref(arg2) + if (isQueryKey(arg1Unreffed)) { + return super.fetchInfiniteQuery( + arg1Unreffed as TQueryKey, + arg2Unreffed as QueryFunction, + cloneDeepUnref(arg3) as FetchInfiniteQueryOptions< + TQueryFnData, + TError, + TData, + TQueryKey + >, + ) + } + return super.fetchInfiniteQuery( + arg1Unreffed as FetchInfiniteQueryOptions< + TQueryFnData, + TError, + TData, + TQueryKey + >, + ) + } + + prefetchInfiniteQuery< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + >( + options: MaybeRefDeep< + FetchInfiniteQueryOptions + >, + ): Promise + prefetchInfiniteQuery< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + >( + queryKey: MaybeRefDeep, + options?: MaybeRefDeep< + FetchInfiniteQueryOptions + >, + ): Promise + prefetchInfiniteQuery< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + >( + queryKey: MaybeRefDeep, + queryFn: QueryFunction, + options?: MaybeRefDeep< + FetchInfiniteQueryOptions + >, + ): Promise + prefetchInfiniteQuery< + TQueryFnData, + TError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + >( + arg1: MaybeRefDeep< + | TQueryKey + | FetchInfiniteQueryOptions + >, + arg2?: + | QueryFunction + | MaybeRefDeep< + FetchInfiniteQueryOptions + >, + arg3?: MaybeRefDeep< + FetchInfiniteQueryOptions + >, + ): Promise { + return super.prefetchInfiniteQuery( + cloneDeepUnref(arg1) as any, + cloneDeepUnref(arg2) as any, + cloneDeepUnref(arg3) as any, + ) + } + + setDefaultOptions(options: MaybeRefDeep): void { + super.setDefaultOptions(cloneDeepUnref(options) as DefaultOptions) + } + + setQueryDefaults( + queryKey: MaybeRefDeep, + options: MaybeRefDeep>, + ): void { + super.setQueryDefaults( + cloneDeepUnref(queryKey), + cloneDeepUnref(options) as any, + ) + } + + getQueryDefaults( + queryKey?: MaybeRefDeep, + ): QueryObserverOptions | undefined { + return super.getQueryDefaults(cloneDeepUnref(queryKey)) + } + + setMutationDefaults( + mutationKey: MaybeRefDeep, + options: MaybeRefDeep>, + ): void { + super.setMutationDefaults( + cloneDeepUnref(mutationKey), + cloneDeepUnref(options) as any, + ) + } + + getMutationDefaults( + mutationKey?: MaybeRefDeep, + ): MutationObserverOptions | undefined { + return super.getMutationDefaults(cloneDeepUnref(mutationKey)) + } +} diff --git a/packages/vue-query/src/types.ts b/packages/vue-query/src/types.ts new file mode 100644 index 00000000000..4df6af1c17c --- /dev/null +++ b/packages/vue-query/src/types.ts @@ -0,0 +1,91 @@ +import type { + QueryKey, + QueryObserverOptions, + InfiniteQueryObserverOptions, +} from '@tanstack/query-core' +import type { Ref, UnwrapRef } from 'vue-demi' +import type { QueryClient } from './queryClient' + +export type MaybeRef = Ref | T +export type MaybeRefDeep = T extends Function + ? T + : MaybeRef< + T extends object + ? { + [Property in keyof T]: MaybeRefDeep + } + : T + > + +export type WithQueryClientKey = T & { + queryClientKey?: string + queryClient?: QueryClient +} + +// A Vue version of QueriesObserverOptions from "@tanstack/query-core" +// Accept refs as options +export type VueQueryObserverOptions< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +> = { + [Property in keyof QueryObserverOptions< + TQueryFnData, + TError, + TData, + TQueryData, + TQueryKey + >]: Property extends 'queryFn' + ? QueryObserverOptions< + TQueryFnData, + TError, + TData, + TQueryData, + UnwrapRef + >[Property] + : MaybeRef< + QueryObserverOptions< + TQueryFnData, + TError, + TData, + TQueryData, + TQueryKey + >[Property] + > +} + +// A Vue version of InfiniteQueryObserverOptions from "@tanstack/query-core" +// Accept refs as options +export type VueInfiniteQueryObserverOptions< + TQueryFnData = unknown, + TError = unknown, + TData = unknown, + TQueryData = unknown, + TQueryKey extends QueryKey = QueryKey, +> = { + [Property in keyof InfiniteQueryObserverOptions< + TQueryFnData, + TError, + TData, + TQueryData, + TQueryKey + >]: Property extends 'queryFn' + ? InfiniteQueryObserverOptions< + TQueryFnData, + TError, + TData, + TQueryData, + UnwrapRef + >[Property] + : MaybeRef< + InfiniteQueryObserverOptions< + TQueryFnData, + TError, + TData, + TQueryData, + TQueryKey + >[Property] + > +} diff --git a/packages/vue-query/src/useBaseQuery.ts b/packages/vue-query/src/useBaseQuery.ts new file mode 100644 index 00000000000..951bbb0c3c7 --- /dev/null +++ b/packages/vue-query/src/useBaseQuery.ts @@ -0,0 +1,122 @@ +import { onScopeDispose, toRefs, readonly, reactive, watch } from 'vue-demi' +import type { ToRefs, UnwrapRef } from 'vue-demi' +import type { + QueryObserver, + QueryKey, + QueryObserverOptions, + QueryObserverResult, + QueryFunction, +} from '@tanstack/query-core' +import { useQueryClient } from './useQueryClient' +import { updateState, isQueryKey, cloneDeepUnref } from './utils' +import type { WithQueryClientKey } from './types' +import type { UseQueryOptions } from './useQuery' +import type { UseInfiniteQueryOptions } from './useInfiniteQuery' + +export type UseQueryReturnType< + TData, + TError, + Result = QueryObserverResult, +> = ToRefs> & { + suspense: () => Promise +} + +type UseQueryOptionsGeneric< + TQueryFnData, + TError, + TData, + TQueryKey extends QueryKey = QueryKey, +> = + | UseQueryOptions + | UseInfiniteQueryOptions + +export function useBaseQuery< + TQueryFnData, + TError, + TData, + TQueryData, + TQueryKey extends QueryKey, +>( + Observer: typeof QueryObserver, + arg1: + | TQueryKey + | UseQueryOptionsGeneric, + arg2: + | QueryFunction> + | UseQueryOptionsGeneric = {}, + arg3: UseQueryOptionsGeneric = {}, +): UseQueryReturnType { + const options = getQueryUnreffedOptions() + const queryClient = + options.queryClient ?? useQueryClient(options.queryClientKey) + const defaultedOptions = queryClient.defaultQueryOptions(options) + const observer = new Observer(queryClient, defaultedOptions) + const state = reactive(observer.getCurrentResult()) + const unsubscribe = observer.subscribe((result) => { + updateState(state, result) + }) + + watch( + [() => arg1, () => arg2, () => arg3], + () => { + observer.setOptions( + queryClient.defaultQueryOptions(getQueryUnreffedOptions()), + ) + }, + { deep: true }, + ) + + onScopeDispose(() => { + unsubscribe() + }) + + const suspense = () => { + return new Promise>((resolve) => { + const run = () => { + const newOptions = queryClient.defaultQueryOptions( + getQueryUnreffedOptions(), + ) + if (newOptions.enabled !== false) { + const optimisticResult = observer.getOptimisticResult(newOptions) + if (optimisticResult.isStale) { + resolve(observer.fetchOptimistic(defaultedOptions)) + } else { + resolve(optimisticResult) + } + } + } + + run() + + watch([() => arg1, () => arg2, () => arg3], run, { deep: true }) + }) + } + + return { + ...(toRefs(readonly(state)) as UseQueryReturnType), + suspense, + } + + /** + * Get Query Options object + * All inner refs unwrapped. No Reactivity + */ + function getQueryUnreffedOptions() { + let mergedOptions + + if (!isQueryKey(arg1)) { + // `useQuery(optionsObj)` + mergedOptions = arg1 + } else if (typeof arg2 === 'function') { + // `useQuery(queryKey, queryFn, optionsObj?)` + mergedOptions = { ...arg3, queryKey: arg1, queryFn: arg2 } + } else { + // `useQuery(queryKey, optionsObj?)` + mergedOptions = { ...arg2, queryKey: arg1 } + } + + return cloneDeepUnref(mergedOptions) as WithQueryClientKey< + QueryObserverOptions + > + } +} diff --git a/packages/vue-query/src/useInfiniteQuery.ts b/packages/vue-query/src/useInfiniteQuery.ts new file mode 100644 index 00000000000..b102acac158 --- /dev/null +++ b/packages/vue-query/src/useInfiniteQuery.ts @@ -0,0 +1,114 @@ +import { InfiniteQueryObserver } from '@tanstack/query-core' +import type { UnwrapRef } from 'vue-demi' +import type { + QueryObserver, + QueryFunction, + QueryKey, + InfiniteQueryObserverResult, +} from '@tanstack/query-core' + +import { useBaseQuery } from './useBaseQuery' +import type { UseQueryReturnType } from './useBaseQuery' + +import type { + WithQueryClientKey, + VueInfiniteQueryObserverOptions, +} from './types' + +export type UseInfiniteQueryOptions< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +> = WithQueryClientKey< + VueInfiniteQueryObserverOptions< + TQueryFnData, + TError, + TData, + TQueryFnData, + TQueryKey + > +> + +type InfiniteQueryReturnType = UseQueryReturnType< + TData, + TError, + InfiniteQueryObserverResult +> +type UseInfiniteQueryReturnType = Omit< + InfiniteQueryReturnType, + 'fetchNextPage' | 'fetchPreviousPage' | 'refetch' | 'remove' +> & { + fetchNextPage: InfiniteQueryObserverResult['fetchNextPage'] + fetchPreviousPage: InfiniteQueryObserverResult< + TData, + TError + >['fetchPreviousPage'] + refetch: InfiniteQueryObserverResult['refetch'] + remove: InfiniteQueryObserverResult['remove'] +} + +export function useInfiniteQuery< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>( + options: UseInfiniteQueryOptions, +): UseInfiniteQueryReturnType + +export function useInfiniteQuery< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>( + queryKey: TQueryKey, + options?: Omit< + UseInfiniteQueryOptions, + 'queryKey' + >, +): UseInfiniteQueryReturnType + +export function useInfiniteQuery< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>( + queryKey: TQueryKey, + queryFn: QueryFunction>, + options?: Omit< + UseInfiniteQueryOptions, + 'queryKey' | 'queryFn' + >, +): UseInfiniteQueryReturnType + +export function useInfiniteQuery< + TQueryFnData, + TError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>( + arg1: + | TQueryKey + | UseInfiniteQueryOptions, + arg2?: + | QueryFunction> + | UseInfiniteQueryOptions, + arg3?: UseInfiniteQueryOptions, +): UseInfiniteQueryReturnType { + const result = useBaseQuery( + InfiniteQueryObserver as typeof QueryObserver, + arg1, + arg2, + arg3, + ) as InfiniteQueryReturnType + return { + ...result, + fetchNextPage: result.fetchNextPage.value, + fetchPreviousPage: result.fetchPreviousPage.value, + refetch: result.refetch.value, + remove: result.remove.value, + } +} diff --git a/packages/vue-query/src/useIsFetching.ts b/packages/vue-query/src/useIsFetching.ts new file mode 100644 index 00000000000..43eebbaa61b --- /dev/null +++ b/packages/vue-query/src/useIsFetching.ts @@ -0,0 +1,59 @@ +import { onScopeDispose, ref, watch } from 'vue-demi' +import type { Ref } from 'vue-demi' +import type { QueryKey, QueryFilters as QF } from '@tanstack/query-core' + +import { useQueryClient } from './useQueryClient' +import { cloneDeepUnref, isQueryKey } from './utils' +import type { MaybeRefDeep, WithQueryClientKey } from './types' + +export type QueryFilters = MaybeRefDeep> + +export function useIsFetching(filters?: QueryFilters): Ref +export function useIsFetching( + queryKey?: QueryKey, + filters?: QueryFilters, +): Ref +export function useIsFetching( + arg1?: QueryKey | QueryFilters, + arg2?: QueryFilters, +): Ref { + const filters = ref(parseFilterArgs(arg1, arg2)) + const queryClient = + filters.value.queryClient ?? useQueryClient(filters.value.queryClientKey) + + const isFetching = ref(queryClient.isFetching(filters)) + + const unsubscribe = queryClient.getQueryCache().subscribe(() => { + isFetching.value = queryClient.isFetching(filters) + }) + + watch( + [() => arg1, () => arg2], + () => { + filters.value = parseFilterArgs(arg1, arg2) + isFetching.value = queryClient.isFetching(filters) + }, + { deep: true }, + ) + + onScopeDispose(() => { + unsubscribe() + }) + + return isFetching +} + +export function parseFilterArgs( + arg1?: QueryKey | QueryFilters, + arg2: QueryFilters = {}, +) { + let options: QueryFilters + + if (isQueryKey(arg1)) { + options = { ...arg2, queryKey: arg1 } + } else { + options = arg1 || {} + } + + return cloneDeepUnref(options) as WithQueryClientKey +} diff --git a/packages/vue-query/src/useIsMutating.ts b/packages/vue-query/src/useIsMutating.ts new file mode 100644 index 00000000000..7e4413ffdad --- /dev/null +++ b/packages/vue-query/src/useIsMutating.ts @@ -0,0 +1,59 @@ +import { onScopeDispose, ref, watch } from 'vue-demi' +import type { Ref } from 'vue-demi' +import type { MutationKey, MutationFilters as MF } from '@tanstack/query-core' + +import { useQueryClient } from './useQueryClient' +import { cloneDeepUnref, isQueryKey } from './utils' +import type { MaybeRefDeep, WithQueryClientKey } from './types' + +export type MutationFilters = MaybeRefDeep> + +export function useIsMutating(filters?: MutationFilters): Ref +export function useIsMutating( + mutationKey?: MutationKey, + filters?: Omit, +): Ref +export function useIsMutating( + arg1?: MutationKey | MutationFilters, + arg2?: Omit, +): Ref { + const filters = ref(parseMutationFilterArgs(arg1, arg2)) + const queryClient = + filters.value.queryClient ?? useQueryClient(filters.value.queryClientKey) + + const isMutating = ref(queryClient.isMutating(filters)) + + const unsubscribe = queryClient.getMutationCache().subscribe(() => { + isMutating.value = queryClient.isMutating(filters) + }) + + watch( + [() => arg1, () => arg2], + () => { + filters.value = parseMutationFilterArgs(arg1, arg2) + isMutating.value = queryClient.isMutating(filters) + }, + { deep: true }, + ) + + onScopeDispose(() => { + unsubscribe() + }) + + return isMutating +} + +export function parseMutationFilterArgs( + arg1?: MutationKey | MutationFilters, + arg2: MutationFilters = {}, +) { + let options: MutationFilters + + if (isQueryKey(arg1)) { + options = { ...arg2, mutationKey: arg1 } + } else { + options = arg1 || {} + } + + return cloneDeepUnref(options) as WithQueryClientKey +} diff --git a/packages/vue-query/src/useMutation.ts b/packages/vue-query/src/useMutation.ts new file mode 100644 index 00000000000..07fd73ffda8 --- /dev/null +++ b/packages/vue-query/src/useMutation.ts @@ -0,0 +1,189 @@ +import { onScopeDispose, reactive, readonly, toRefs, watch } from 'vue-demi' +import type { ToRefs } from 'vue-demi' +import { MutationObserver } from '@tanstack/query-core' +import type { + MutateOptions, + MutationFunction, + MutationKey, + MutationObserverOptions, + MutateFunction, + MutationObserverResult, +} from '@tanstack/query-core' +import { cloneDeepUnref, isQueryKey, updateState } from './utils' +import { useQueryClient } from './useQueryClient' +import type { WithQueryClientKey } from './types' + +type MutationResult = Omit< + MutationObserverResult, + 'mutate' | 'reset' +> + +export type UseMutationOptions = + WithQueryClientKey< + MutationObserverOptions + > + +type MutateSyncFunction< + TData = unknown, + TError = unknown, + TVariables = void, + TContext = unknown, +> = ( + ...options: Parameters> +) => void + +export type UseMutationReturnType< + TData, + TError, + TVariables, + TContext, + Result = MutationResult, +> = ToRefs> & { + mutate: MutateSyncFunction + mutateAsync: MutateFunction + reset: MutationObserverResult['reset'] +} + +export function useMutation< + TData = unknown, + TError = unknown, + TVariables = void, + TContext = unknown, +>( + options: UseMutationOptions, +): UseMutationReturnType +export function useMutation< + TData = unknown, + TError = unknown, + TVariables = void, + TContext = unknown, +>( + mutationFn: MutationFunction, + options?: Omit< + UseMutationOptions, + 'mutationFn' + >, +): UseMutationReturnType +export function useMutation< + TData = unknown, + TError = unknown, + TVariables = void, + TContext = unknown, +>( + mutationKey: MutationKey, + options?: Omit< + UseMutationOptions, + 'mutationKey' + >, +): UseMutationReturnType +export function useMutation< + TData = unknown, + TError = unknown, + TVariables = void, + TContext = unknown, +>( + mutationKey: MutationKey, + mutationFn?: MutationFunction, + options?: Omit< + UseMutationOptions, + 'mutationKey' | 'mutationFn' + >, +): UseMutationReturnType +export function useMutation< + TData = unknown, + TError = unknown, + TVariables = void, + TContext = unknown, +>( + arg1: + | MutationKey + | MutationFunction + | UseMutationOptions, + arg2?: + | MutationFunction + | UseMutationOptions, + arg3?: UseMutationOptions, +): UseMutationReturnType { + const options = parseMutationArgs(arg1, arg2, arg3) + const queryClient = + options.queryClient ?? useQueryClient(options.queryClientKey) + const defaultedOptions = queryClient.defaultMutationOptions(options) + const observer = new MutationObserver(queryClient, defaultedOptions) + + const state = reactive(observer.getCurrentResult()) + + const unsubscribe = observer.subscribe((result) => { + updateState(state, result) + }) + + const mutate = ( + variables: TVariables, + mutateOptions?: MutateOptions, + ) => { + observer.mutate(variables, mutateOptions).catch(() => { + // This is intentional + }) + } + + watch( + [() => arg1, () => arg2, () => arg3], + () => { + observer.setOptions( + queryClient.defaultMutationOptions(parseMutationArgs(arg1, arg2, arg3)), + ) + }, + { deep: true }, + ) + + onScopeDispose(() => { + unsubscribe() + }) + + const resultRefs = toRefs(readonly(state)) as unknown as ToRefs< + Readonly> + > + + return { + ...resultRefs, + mutate, + mutateAsync: state.mutate, + reset: state.reset, + } +} + +export function parseMutationArgs< + TData = unknown, + TError = unknown, + TVariables = void, + TContext = unknown, +>( + arg1: + | MutationKey + | MutationFunction + | UseMutationOptions, + arg2?: + | MutationFunction + | UseMutationOptions, + arg3?: UseMutationOptions, +): UseMutationOptions { + let options = arg1 + + if (isQueryKey(arg1)) { + if (typeof arg2 === 'function') { + options = { ...arg3, mutationKey: arg1, mutationFn: arg2 } + } else { + options = { ...arg2, mutationKey: arg1 } + } + } + + if (typeof arg1 === 'function') { + options = { ...arg2, mutationFn: arg1 } + } + + return cloneDeepUnref(options) as UseMutationOptions< + TData, + TError, + TVariables, + TContext + > +} diff --git a/packages/vue-query/src/useQueries.ts b/packages/vue-query/src/useQueries.ts new file mode 100644 index 00000000000..6879bcd2cb1 --- /dev/null +++ b/packages/vue-query/src/useQueries.ts @@ -0,0 +1,167 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { QueriesObserver } from '@tanstack/query-core' +import { onScopeDispose, reactive, readonly, watch } from 'vue-demi' +import type { Ref } from 'vue-demi' + +import type { QueryFunction, QueryObserverResult } from '@tanstack/query-core' + +import { useQueryClient } from './useQueryClient' +import type { UseQueryOptions } from './useQuery' +import { cloneDeepUnref } from './utils' + +// Avoid TS depth-limit error in case of large array literal +type MAXIMUM_DEPTH = 20 + +type GetOptions = + // 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 + } + ? UseQueryOptions + : T extends { queryFnData: infer TQueryFnData; error?: infer TError } + ? UseQueryOptions + : T extends { data: infer TData; error?: infer TError } + ? UseQueryOptions + : // Part 2: responsible for applying explicit type parameter to function arguments, if tuple [TQueryFnData, TError, TData] + T extends [infer TQueryFnData, infer TError, infer TData] + ? UseQueryOptions + : T extends [infer TQueryFnData, infer TError] + ? UseQueryOptions + : T extends [infer TQueryFnData] + ? UseQueryOptions + : // Part 3: responsible for inferring and enforcing type if no explicit parameter was provided + T extends { + queryFn?: QueryFunction + select: (data: any) => infer TData + } + ? UseQueryOptions + : T extends { queryFn?: QueryFunction } + ? UseQueryOptions + : // Fallback + UseQueryOptions + +type GetResults = + // Part 1: responsible for mapping explicit type parameter to function result, if object + T extends { queryFnData: any; error?: infer TError; data: infer TData } + ? QueryObserverResult + : T extends { queryFnData: infer TQueryFnData; error?: infer TError } + ? QueryObserverResult + : T extends { data: infer TData; error?: infer TError } + ? QueryObserverResult + : // Part 2: responsible for mapping explicit type parameter to function result, if tuple + T extends [any, infer TError, infer TData] + ? QueryObserverResult + : T extends [infer TQueryFnData, infer TError] + ? QueryObserverResult + : T extends [infer TQueryFnData] + ? QueryObserverResult + : // Part 3: responsible for mapping inferred type to results, if no explicit parameter was provided + T extends { + queryFn?: QueryFunction + select: (data: any) => infer TData + } + ? QueryObserverResult + : T extends { queryFn?: QueryFunction } + ? QueryObserverResult + : // Fallback + QueryObserverResult + +/** + * UseQueriesOptions reducer recursively unwraps function arguments to infer/enforce type param + */ +export type UseQueriesOptions< + T extends any[], + Result extends any[] = [], + Depth extends ReadonlyArray = [], +> = Depth['length'] extends MAXIMUM_DEPTH + ? UseQueryOptions[] + : T extends [] + ? [] + : T extends [infer Head] + ? [...Result, GetOptions] + : T extends [infer Head, ...infer Tail] + ? UseQueriesOptions<[...Tail], [...Result, GetOptions], [...Depth, 1]> + : unknown[] extends T + ? T + : // If T is *some* array but we couldn't assign unknown[] to it, then it must hold some known/homogenous type! + // use this to infer the param types in the case of Array.map() argument + T extends UseQueryOptions< + infer TQueryFnData, + infer TError, + infer TData, + infer TQueryKey + >[] + ? UseQueryOptions[] + : // Fallback + UseQueryOptions[] + +/** + * UseQueriesResults reducer recursively maps type param to results + */ +export type UseQueriesResults< + T extends any[], + Result extends any[] = [], + Depth extends ReadonlyArray = [], +> = Depth['length'] extends MAXIMUM_DEPTH + ? QueryObserverResult[] + : T extends [] + ? [] + : T extends [infer Head] + ? [...Result, GetResults] + : T extends [infer Head, ...infer Tail] + ? UseQueriesResults<[...Tail], [...Result, GetResults], [...Depth, 1]> + : T extends UseQueryOptions< + infer TQueryFnData, + infer TError, + infer TData, + any + >[] + ? // Dynamic-size (homogenous) UseQueryOptions array: map directly to array of results + QueryObserverResult[] + : // Fallback + QueryObserverResult[] + +type UseQueriesOptionsArg = readonly [...UseQueriesOptions] + +export function useQueries({ + queries, +}: { + queries: Ref> | UseQueriesOptionsArg +}): Readonly> { + const unreffedQueries = cloneDeepUnref(queries) as UseQueriesOptionsArg + + const queryClientKey = unreffedQueries[0].queryClientKey + const optionsQueryClient = unreffedQueries[0].queryClient + const queryClient = optionsQueryClient ?? useQueryClient(queryClientKey) + const defaultedQueries = unreffedQueries.map((options) => { + return queryClient.defaultQueryOptions(options) + }) + + const observer = new QueriesObserver(queryClient, defaultedQueries) + const state = reactive(observer.getCurrentResult()) + + const unsubscribe = observer.subscribe((result) => { + state.splice(0, state.length, ...result) + }) + + watch( + () => queries, + () => { + const defaulted = ( + cloneDeepUnref(queries) as UseQueriesOptionsArg + ).map((options) => { + return queryClient.defaultQueryOptions(options) + }) + observer.setQueries(defaulted) + }, + { deep: true }, + ) + + onScopeDispose(() => { + unsubscribe() + }) + + return readonly(state) as UseQueriesResults +} diff --git a/packages/vue-query/src/useQuery.ts b/packages/vue-query/src/useQuery.ts new file mode 100644 index 00000000000..ef15749ca8c --- /dev/null +++ b/packages/vue-query/src/useQuery.ts @@ -0,0 +1,174 @@ +import type { ToRefs, UnwrapRef } from 'vue-demi' +import { QueryObserver } from '@tanstack/query-core' +import type { + QueryFunction, + QueryKey, + QueryObserverResult, + DefinedQueryObserverResult, +} from '@tanstack/query-core' +import { useBaseQuery } from './useBaseQuery' +import type { UseQueryReturnType as UQRT } from './useBaseQuery' +import type { WithQueryClientKey, VueQueryObserverOptions } from './types' + +type UseQueryReturnType = Omit< + UQRT, + 'refetch' | 'remove' +> & { + refetch: QueryObserverResult['refetch'] + remove: QueryObserverResult['remove'] +} + +type UseQueryDefinedReturnType = Omit< + ToRefs>>, + 'refetch' | 'remove' +> & { + suspense: () => Promise> + refetch: QueryObserverResult['refetch'] + remove: QueryObserverResult['remove'] +} + +export type UseQueryOptions< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +> = WithQueryClientKey< + VueQueryObserverOptions +> + +export function useQuery< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>( + options: Omit< + UseQueryOptions, + 'initialData' + > & { initialData?: () => undefined }, +): UseQueryReturnType + +export function useQuery< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>( + options: Omit< + UseQueryOptions, + 'initialData' + > & { initialData: TQueryFnData | (() => TQueryFnData) }, +): UseQueryDefinedReturnType + +export function useQuery< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>( + options: UseQueryOptions, +): UseQueryReturnType + +export function useQuery< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>( + queryKey: TQueryKey, + options?: Omit< + UseQueryOptions, + 'queryKey' | 'initialData' + > & { initialData?: () => undefined }, +): UseQueryReturnType + +export function useQuery< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>( + queryKey: TQueryKey, + options?: Omit< + UseQueryOptions, + 'queryKey' | 'initialData' + > & { initialData: TQueryFnData | (() => TQueryFnData) }, +): UseQueryDefinedReturnType + +export function useQuery< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>( + queryKey: TQueryKey, + options?: Omit< + UseQueryOptions, + 'queryKey' + >, +): UseQueryReturnType + +export function useQuery< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>( + queryKey: TQueryKey, + queryFn: QueryFunction, + options?: Omit< + UseQueryOptions, + 'queryKey' | 'queryFn' | 'initialData' + > & { initialData?: () => undefined }, +): UseQueryReturnType + +export function useQuery< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>( + queryKey: TQueryKey, + queryFn: QueryFunction, + options?: Omit< + UseQueryOptions, + 'queryKey' | 'queryFn' | 'initialData' + > & { initialData: TQueryFnData | (() => TQueryFnData) }, +): UseQueryDefinedReturnType + +export function useQuery< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>( + queryKey: TQueryKey, + queryFn: QueryFunction, + options?: Omit< + UseQueryOptions, + 'queryKey' | 'queryFn' + >, +): UseQueryReturnType + +export function useQuery< + TQueryFnData, + TError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>( + arg1: TQueryKey | UseQueryOptions, + arg2?: + | QueryFunction> + | UseQueryOptions, + arg3?: UseQueryOptions, +): + | UseQueryReturnType + | UseQueryDefinedReturnType { + const result = useBaseQuery(QueryObserver, arg1, arg2, arg3) + + return { + ...result, + refetch: result.refetch.value, + remove: result.remove.value, + } +} diff --git a/packages/vue-query/src/useQueryClient.ts b/packages/vue-query/src/useQueryClient.ts new file mode 100644 index 00000000000..4662d1b12d3 --- /dev/null +++ b/packages/vue-query/src/useQueryClient.ts @@ -0,0 +1,23 @@ +import { getCurrentInstance, inject } from 'vue-demi' + +import type { QueryClient } from './queryClient' +import { getClientKey } from './utils' + +export function useQueryClient(id = ''): QueryClient { + const vm = getCurrentInstance()?.proxy + + if (!vm) { + throw new Error('vue-query hooks can only be used inside setup() function.') + } + + const key = getClientKey(id) + const queryClient = inject(key) + + if (!queryClient) { + throw new Error( + "No 'queryClient' found in Vue context, use 'VueQueryPlugin' to properly initialize the library.", + ) + } + + return queryClient +} diff --git a/packages/vue-query/src/utils.ts b/packages/vue-query/src/utils.ts new file mode 100644 index 00000000000..7c4e1ab7968 --- /dev/null +++ b/packages/vue-query/src/utils.ts @@ -0,0 +1,67 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import type { QueryKey } from '@tanstack/query-core' +import { isRef, unref } from 'vue-demi' +import type { UnwrapRef } from 'vue-demi' + +export const VUE_QUERY_CLIENT = 'VUE_QUERY_CLIENT' + +export function getClientKey(key?: string) { + const suffix = key ? `:${key}` : '' + return `${VUE_QUERY_CLIENT}${suffix}` +} + +export function isQueryKey(value: unknown): value is QueryKey { + return Array.isArray(value) +} + +export function updateState( + state: Record, + update: Record, +): void { + Object.keys(state).forEach((key) => { + state[key] = update[key] + }) +} + +export function cloneDeep( + value: T, + customizer?: (val: unknown) => unknown | void, +): T { + if (customizer) { + const result = customizer(value) + if (result !== undefined || isRef(value)) { + return result as typeof value + } + } + + if (Array.isArray(value)) { + return value.map((val) => cloneDeep(val, customizer)) as typeof value + } + + if (typeof value === 'object' && isPlainObject(value)) { + const entries = Object.entries(value).map(([key, val]) => [ + key, + cloneDeep(val, customizer), + ]) + return Object.fromEntries(entries) + } + + return value +} + +export function cloneDeepUnref(obj: T): UnwrapRef { + return cloneDeep(obj, (val) => { + if (isRef(val)) { + return cloneDeepUnref(unref(val)) + } + }) as UnwrapRef +} + +function isPlainObject(value: unknown): value is Object { + if (Object.prototype.toString.call(value) !== '[object Object]') { + return false + } + + const prototype = Object.getPrototypeOf(value) + return prototype === null || prototype === Object.prototype +} diff --git a/packages/vue-query/src/vueQueryPlugin.ts b/packages/vue-query/src/vueQueryPlugin.ts new file mode 100644 index 00000000000..4bfa47e77ad --- /dev/null +++ b/packages/vue-query/src/vueQueryPlugin.ts @@ -0,0 +1,106 @@ +import { isVue2 } from 'vue-demi' +import type { QueryClientConfig } from '@tanstack/query-core' + +import { QueryClient } from './queryClient' +import { getClientKey } from './utils' +import { setupDevtools } from './devtools/devtools' +import type { MaybeRefDeep } from './types' + +declare global { + interface Window { + __VUE_QUERY_CONTEXT__?: QueryClient + } +} + +export interface AdditionalClient { + queryClient: QueryClient + queryClientKey: string +} + +interface ConfigOptions { + queryClientConfig?: MaybeRefDeep + queryClientKey?: string + contextSharing?: boolean +} + +interface ClientOptions { + queryClient?: QueryClient + queryClientKey?: string + contextSharing?: boolean +} + +export type VueQueryPluginOptions = ConfigOptions | ClientOptions + +export const VueQueryPlugin = { + install: (app: any, options: VueQueryPluginOptions = {}) => { + const clientKey = getClientKey(options.queryClientKey) + let client: QueryClient + + if ('queryClient' in options && options.queryClient) { + client = options.queryClient + } else { + if (options.contextSharing && typeof window !== 'undefined') { + if (!window.__VUE_QUERY_CONTEXT__) { + const clientConfig = + 'queryClientConfig' in options + ? options.queryClientConfig + : undefined + client = new QueryClient(clientConfig) + window.__VUE_QUERY_CONTEXT__ = client + } else { + client = window.__VUE_QUERY_CONTEXT__ + } + } else { + const clientConfig = + 'queryClientConfig' in options ? options.queryClientConfig : undefined + client = new QueryClient(clientConfig) + } + } + + client.mount() + + const cleanup = () => { + client.unmount() + } + + if (app.onUnmount) { + app.onUnmount(cleanup) + } else { + const originalUnmount = app.unmount + app.unmount = function vueQueryUnmount() { + cleanup() + originalUnmount() + } + } + + /* istanbul ignore next */ + if (isVue2) { + app.mixin({ + beforeCreate() { + // HACK: taken from provide(): https://github.com/vuejs/composition-api/blob/master/src/apis/inject.ts#L30 + if (!this._provided) { + const provideCache = {} + Object.defineProperty(this, '_provided', { + get: () => provideCache, + set: (v) => Object.assign(provideCache, v), + }) + } + + this._provided[clientKey] = client + + if (process.env.NODE_ENV === 'development') { + if (this === this.$root) { + setupDevtools(this, client) + } + } + }, + }) + } else { + app.provide(clientKey, client) + + if (process.env.NODE_ENV === 'development') { + setupDevtools(app, client) + } + } + }, +} diff --git a/packages/vue-query/tsconfig.json b/packages/vue-query/tsconfig.json new file mode 100644 index 00000000000..48a8c7efce5 --- /dev/null +++ b/packages/vue-query/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "rootDir": "./src", + "outDir": "./build/lib", + "tsBuildInfoFile": "./build/.tsbuildinfo" + }, + "include": ["src"], + "exclude": ["src/__tests__"], + "references": [ + { "path": "../query-core" } + ] +} diff --git a/packages/vue-query/tsconfig.lint.json b/packages/vue-query/tsconfig.lint.json new file mode 100644 index 00000000000..85281afb6c3 --- /dev/null +++ b/packages/vue-query/tsconfig.lint.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "rootDir": "./src", + "outDir": "./build/lib", + "tsBuildInfoFile": "./build/.tsbuildinfo" + }, + "include": ["src"], + "references": [ + { "path": "../query-core" } + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 215b930199c..f8de10e64a3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -727,6 +727,25 @@ importers: '@types/jscodeshift': 0.11.5 jscodeshift: 0.13.1 + packages/vue-query: + specifiers: + '@tanstack/query-core': workspace:* + '@vue/composition-api': 1.7.1 + '@vue/devtools-api': ^6.4.2 + match-sorter: ^6.3.1 + vue: ^3.2.40 + vue-demi: ^0.13.11 + vue2: npm:vue@2 + dependencies: + '@tanstack/query-core': link:../query-core + '@vue/devtools-api': 6.4.2 + match-sorter: 6.3.1 + vue-demi: 0.13.11_zv57lmwz3lpne326jxcwg2uc6q + devDependencies: + '@vue/composition-api': 1.7.1_vue@3.2.40 + vue: 3.2.40 + vue2: /vue/2.7.10 + packages: /@ampproject/remapping/2.2.0: @@ -5836,6 +5855,14 @@ packages: source-map: 0.6.1 dev: true + /@vue/compiler-core/3.2.40: + resolution: {integrity: sha512-2Dc3Stk0J/VyQ4OUr2yEC53kU28614lZS+bnrCbFSAIftBJ40g/2yQzf4mPBiFuqguMB7hyHaujdgZAQ67kZYA==} + dependencies: + '@babel/parser': 7.19.1 + '@vue/shared': 3.2.40 + estree-walker: 2.0.2 + source-map: 0.6.1 + /@vue/compiler-dom/3.2.37: resolution: {integrity: sha512-yxJLH167fucHKxaqXpYk7x8z7mMEnXOw3G2q62FTkmsvNxu4FQSu5+3UMb+L7fjKa26DEzhrmCxAgFLLIzVfqQ==} dependencies: @@ -5843,6 +5870,20 @@ packages: '@vue/shared': 3.2.37 dev: true + /@vue/compiler-dom/3.2.40: + resolution: {integrity: sha512-OZCNyYVC2LQJy4H7h0o28rtk+4v+HMQygRTpmibGoG9wZyomQiS5otU7qo3Wlq5UfHDw2RFwxb9BJgKjVpjrQw==} + dependencies: + '@vue/compiler-core': 3.2.40 + '@vue/shared': 3.2.40 + + /@vue/compiler-sfc/2.7.10: + resolution: {integrity: sha512-55Shns6WPxlYsz4WX7q9ZJBL77sKE1ZAYNYStLs6GbhIOMrNtjMvzcob6gu3cGlfpCR4bT7NXgyJ3tly2+Hx8Q==} + dependencies: + '@babel/parser': 7.19.1 + postcss: 8.4.16 + source-map: 0.6.1 + dev: true + /@vue/compiler-sfc/3.2.37: resolution: {integrity: sha512-+7i/2+9LYlpqDv+KTtWhOZH+pa8/HnX/905MdVmAcI/mPQOBwkHHIzrsEsucyOIZQYMkXUiTkmZq5am/NyXKkg==} dependencies: @@ -5858,6 +5899,20 @@ packages: source-map: 0.6.1 dev: true + /@vue/compiler-sfc/3.2.40: + resolution: {integrity: sha512-tzqwniIN1fu1PDHC3CpqY/dPCfN/RN1thpBC+g69kJcrl7mbGiHKNwbA6kJ3XKKy8R6JLKqcpVugqN4HkeBFFg==} + dependencies: + '@babel/parser': 7.19.1 + '@vue/compiler-core': 3.2.40 + '@vue/compiler-dom': 3.2.40 + '@vue/compiler-ssr': 3.2.40 + '@vue/reactivity-transform': 3.2.40 + '@vue/shared': 3.2.40 + estree-walker: 2.0.2 + magic-string: 0.25.9 + postcss: 8.4.16 + source-map: 0.6.1 + /@vue/compiler-ssr/3.2.37: resolution: {integrity: sha512-7mQJD7HdXxQjktmsWp/J67lThEIcxLemz1Vb5I6rYJHR5vI+lON3nPGOH3ubmbvYGt8xEUaAr1j7/tIFWiEOqw==} dependencies: @@ -5865,6 +5920,23 @@ packages: '@vue/shared': 3.2.37 dev: true + /@vue/compiler-ssr/3.2.40: + resolution: {integrity: sha512-80cQcgasKjrPPuKcxwuCx7feq+wC6oFl5YaKSee9pV3DNq+6fmCVwEEC3vvkf/E2aI76rIJSOYHsWSEIxK74oQ==} + dependencies: + '@vue/compiler-dom': 3.2.40 + '@vue/shared': 3.2.40 + + /@vue/composition-api/1.7.1_vue@3.2.40: + resolution: {integrity: sha512-xDWoEtxGXhH9Ku3ROYX/rzhcpt4v31hpPU5zF3UeVC/qxA3dChmqU8zvTUYoKh3j7rzpNsoFOwqsWG7XPMlaFA==} + peerDependencies: + vue: '>= 2.5 < 2.7' + dependencies: + vue: 3.2.40 + + /@vue/devtools-api/6.4.2: + resolution: {integrity: sha512-6hNZ23h1M2Llky+SIAmVhL7s6BjLtZBCzjIz9iRSBUsysjE7kC39ulW0dH4o/eZtycmSt4qEr6RDVGTIuWu+ow==} + dev: false + /@vue/reactivity-transform/3.2.37: resolution: {integrity: sha512-IWopkKEb+8qpu/1eMKVeXrK0NLw9HicGviJzhJDEyfxTR9e1WtpnnbYkJWurX6WwoFP0sz10xQg8yL8lgskAZg==} dependencies: @@ -5875,12 +5947,26 @@ packages: magic-string: 0.25.9 dev: true + /@vue/reactivity-transform/3.2.40: + resolution: {integrity: sha512-HQUCVwEaacq6fGEsg2NUuGKIhUveMCjOk8jGHqLXPI2w6zFoPrlQhwWEaINTv5kkZDXKEnCijAp+4gNEHG03yw==} + dependencies: + '@babel/parser': 7.19.1 + '@vue/compiler-core': 3.2.40 + '@vue/shared': 3.2.40 + estree-walker: 2.0.2 + magic-string: 0.25.9 + /@vue/reactivity/3.2.37: resolution: {integrity: sha512-/7WRafBOshOc6m3F7plwzPeCu/RCVv9uMpOwa/5PiY1Zz+WLVRWiy0MYKwmg19KBdGtFWsmZ4cD+LOdVPcs52A==} dependencies: '@vue/shared': 3.2.37 dev: true + /@vue/reactivity/3.2.40: + resolution: {integrity: sha512-N9qgGLlZmtUBMHF9xDT4EkD9RdXde1Xbveb+niWMXuHVWQP5BzgRmE3SFyUBBcyayG4y1lhoz+lphGRRxxK4RA==} + dependencies: + '@vue/shared': 3.2.40 + /@vue/runtime-core/3.2.37: resolution: {integrity: sha512-JPcd9kFyEdXLl/i0ClS7lwgcs0QpUAWj+SKX2ZC3ANKi1U4DOtiEr6cRqFXsPwY5u1L9fAjkinIdB8Rz3FoYNQ==} dependencies: @@ -5888,6 +5974,12 @@ packages: '@vue/shared': 3.2.37 dev: true + /@vue/runtime-core/3.2.40: + resolution: {integrity: sha512-U1+rWf0H8xK8aBUZhnrN97yoZfHbjgw/bGUzfgKPJl69/mXDuSg8CbdBYBn6VVQdR947vWneQBFzdhasyzMUKg==} + dependencies: + '@vue/reactivity': 3.2.40 + '@vue/shared': 3.2.40 + /@vue/runtime-dom/3.2.37: resolution: {integrity: sha512-HimKdh9BepShW6YozwRKAYjYQWg9mQn63RGEiSswMbW+ssIht1MILYlVGkAGGQbkhSh31PCdoUcfiu4apXJoPw==} dependencies: @@ -5896,6 +5988,13 @@ packages: csstype: 2.6.20 dev: true + /@vue/runtime-dom/3.2.40: + resolution: {integrity: sha512-AO2HMQ+0s2+MCec8hXAhxMgWhFhOPJ/CyRXnmTJ6XIOnJFLrH5Iq3TNwvVcODGR295jy77I6dWPj+wvFoSYaww==} + dependencies: + '@vue/runtime-core': 3.2.40 + '@vue/shared': 3.2.40 + csstype: 2.6.20 + /@vue/server-renderer/3.2.37_vue@3.2.37: resolution: {integrity: sha512-kLITEJvaYgZQ2h47hIzPh2K3jG8c1zCVbp/o/bzQOyvzaKiCquKS7AaioPI28GNxIsE/zSx+EwWYsNxDCX95MA==} peerDependencies: @@ -5906,10 +6005,22 @@ packages: vue: 3.2.37 dev: true + /@vue/server-renderer/3.2.40_vue@3.2.40: + resolution: {integrity: sha512-gtUcpRwrXOJPJ4qyBpU3EyxQa4EkV8I4f8VrDePcGCPe4O/hd0BPS7v9OgjIQob6Ap8VDz9G+mGTKazE45/95w==} + peerDependencies: + vue: 3.2.40 + dependencies: + '@vue/compiler-ssr': 3.2.40 + '@vue/shared': 3.2.40 + vue: 3.2.40 + /@vue/shared/3.2.37: resolution: {integrity: sha512-4rSJemR2NQIo9Klm1vabqWjD8rs/ZaJSzMxkMNeJS6lHiUjjUeYFbooN19NgFjztubEKh3WlZUeOLVdbbUWHsw==} dev: true + /@vue/shared/3.2.40: + resolution: {integrity: sha512-0PLQ6RUtZM0vO3teRfzGi4ltLUO5aO+kLgwh4Um3THSR03rpQWLTuRCkuO5A41ITzwdWeKdPHtSARuPkoo5pCQ==} + /@webassemblyjs/ast/1.8.5: resolution: {integrity: sha512-aJMfngIZ65+t71C3y2nBBg5FFG0Okt9m0XEgWZ7Ywgn1oMAT8cNwx00Uv1cQyHtidq0Xn94R4TAywO+LCQ+ZAQ==} dependencies: @@ -6130,6 +6241,12 @@ packages: resolution: {integrity: sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==} engines: {node: '>=0.4.0'} hasBin: true + dev: true + + /acorn/8.8.0: + resolution: {integrity: sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==} + engines: {node: '>=0.4.0'} + hasBin: true /address/1.0.3: resolution: {integrity: sha512-z55ocwKBRLryBs394Sm3ushTtBeg6VAeuku7utSoSnsJKvKcnXFIyC6vh27n3rXyxSgkJBBCAvyOn7gSUcTYjg==} @@ -6522,7 +6639,7 @@ packages: /axios/0.21.4: resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==} dependencies: - follow-redirects: 1.15.1_debug@4.3.4 + follow-redirects: 1.15.1 transitivePeerDependencies: - debug dev: false @@ -6530,7 +6647,7 @@ packages: /axios/0.24.0: resolution: {integrity: sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==} dependencies: - follow-redirects: 1.15.1_debug@4.3.4 + follow-redirects: 1.15.1 transitivePeerDependencies: - debug dev: true @@ -6538,7 +6655,7 @@ packages: /axios/0.26.1: resolution: {integrity: sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==} dependencies: - follow-redirects: 1.15.1_debug@4.3.4 + follow-redirects: 1.15.1 transitivePeerDependencies: - debug @@ -7028,6 +7145,7 @@ packages: /bindings/1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + requiresBuild: true dependencies: file-uri-to-path: 1.0.0 optional: true @@ -10019,7 +10137,6 @@ packages: /estree-walker/2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} - dev: true /esutils/2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} @@ -10520,6 +10637,7 @@ packages: /file-uri-to-path/1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + requiresBuild: true optional: true /filesize/3.6.1: @@ -10668,7 +10786,7 @@ packages: inherits: 2.0.4 readable-stream: 2.3.7 - /follow-redirects/1.15.1_debug@4.3.4: + /follow-redirects/1.15.1: resolution: {integrity: sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==} engines: {node: '>=4.0'} peerDependencies: @@ -10676,8 +10794,6 @@ packages: peerDependenciesMeta: debug: optional: true - dependencies: - debug: 4.3.4_supports-color@6.1.0 /follow-redirects/1.5.10: resolution: {integrity: sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==} @@ -11424,7 +11540,7 @@ packages: engines: {node: '>=8.0.0'} dependencies: eventemitter3: 4.0.7 - follow-redirects: 1.15.1_debug@4.3.4 + follow-redirects: 1.15.1 requires-port: 1.0.0 transitivePeerDependencies: - debug @@ -13368,7 +13484,7 @@ packages: optional: true dependencies: abab: 2.0.6 - acorn: 8.7.1 + acorn: 8.8.0 acorn-globals: 6.0.0 cssom: 0.4.4 cssstyle: 2.3.0 @@ -13922,7 +14038,6 @@ packages: resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} dependencies: sourcemap-codec: 1.4.8 - dev: true /magic-string/0.26.4: resolution: {integrity: sha512-e5uXtVJ22aEpK9u1+eQf0fSxHeqwyV19K+uGnlROCxUhzwRip9tBsaMViK/0vC3viyPd5Gtucp3UmEp/Q2cPTQ==} @@ -14756,6 +14871,7 @@ packages: /nan/2.16.0: resolution: {integrity: sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA==} + requiresBuild: true optional: true /nano-time/1.0.0: @@ -16267,7 +16383,6 @@ packages: nanoid: 3.3.4 picocolors: 1.0.0 source-map-js: 1.0.2 - dev: true /postcss/8.4.5: resolution: {integrity: sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg==} @@ -18136,7 +18251,6 @@ packages: /sourcemap-codec/1.4.8: resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} - dev: true /spawn-command/0.0.2-1: resolution: {integrity: sha512-n98l9E2RMSJ9ON1AKisHzz7V42VDiBQGY6PB1BwRglz99wpVsSuGzQ+jOi6lFXBGVTCrRpltvjm+/XA+tpeJrg==} @@ -18750,7 +18864,7 @@ packages: engines: {node: '>=6.0.0'} hasBin: true dependencies: - acorn: 8.7.1 + acorn: 8.8.0 commander: 2.20.3 source-map: 0.6.1 source-map-support: 0.5.21 @@ -18761,7 +18875,7 @@ packages: hasBin: true dependencies: '@jridgewell/source-map': 0.3.2 - acorn: 8.7.1 + acorn: 8.8.0 commander: 2.20.3 source-map-support: 0.5.21 dev: true @@ -19469,6 +19583,29 @@ packages: /vm-browserify/1.1.2: resolution: {integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==} + /vue-demi/0.13.11_zv57lmwz3lpne326jxcwg2uc6q: + resolution: {integrity: sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + peerDependencies: + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + dependencies: + '@vue/composition-api': 1.7.1_vue@3.2.40 + vue: 3.2.40 + dev: false + + /vue/2.7.10: + resolution: {integrity: sha512-HmFC70qarSHPXcKtW8U8fgIkF6JGvjEmDiVInTkKZP0gIlEPhlVlcJJLkdGIDiNkIeA2zJPQTWJUI4iWe+AVfg==} + dependencies: + '@vue/compiler-sfc': 2.7.10 + csstype: 3.1.0 + dev: true + /vue/3.2.37: resolution: {integrity: sha512-bOKEZxrm8Eh+fveCqS1/NkG/n6aMidsI6hahas7pa0w/l7jkbssJVsRhVDs07IdDq7h9KHswZOgItnwJAgtVtQ==} dependencies: @@ -19479,6 +19616,15 @@ packages: '@vue/shared': 3.2.37 dev: true + /vue/3.2.40: + resolution: {integrity: sha512-1mGHulzUbl2Nk3pfvI5aXYYyJUs1nm4kyvuz38u4xlQkLUn1i2R7nDbI4TufECmY8v1qNBHYy62bCaM+3cHP2A==} + dependencies: + '@vue/compiler-dom': 3.2.40 + '@vue/compiler-sfc': 3.2.40 + '@vue/runtime-dom': 3.2.40 + '@vue/server-renderer': 3.2.40_vue@3.2.40 + '@vue/shared': 3.2.40 + /w3c-hr-time/1.0.2: resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==} dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b91ef79552b..17e81edac70 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,3 +2,4 @@ packages: - 'packages/**' - 'examples/react/**' - 'examples/solid/**' + - 'examples/vue/**' diff --git a/rollup.config.ts b/rollup.config.ts index 95a397f6c52..ad6a120de4a 100644 --- a/rollup.config.ts +++ b/rollup.config.ts @@ -7,7 +7,6 @@ import replace from '@rollup/plugin-replace' import nodeResolve from '@rollup/plugin-node-resolve' import commonJS from '@rollup/plugin-commonjs' import path from 'path' -import svelte from 'rollup-plugin-svelte' type Options = { input: string | string[] @@ -170,6 +169,26 @@ export default function rollup(options: RollupOptions): RollupOptions[] { }, bundleUMDGlobals: ['@tanstack/query-core'], }), + ...buildConfigs({ + name: 'vue-query', + packageDir: 'packages/vue-query', + jsName: 'VueQuery', + outputFile: 'index', + entryFile: 'src/index.ts', + globals: { + '@tanstack/query-core': 'QueryCore', + vue: 'Vue', + 'vue-demi': 'VueDemi', + 'match-sorter': 'MatchSorter', + '@vue/devtools-api': 'DevtoolsApi', + }, + bundleUMDGlobals: [ + '@tanstack/query-core', + 'vue-demi', + 'match-sorter', + '@vue/devtools-api', + ], + }), ] } @@ -260,7 +279,6 @@ function mjs({ input, output: forceBundle ? bundleOutput : normalOutput, plugins: [ - svelte(), babelPlugin, commonJS(), nodeResolve({ extensions: ['.ts', '.tsx', '.native.ts'] }), @@ -300,7 +318,6 @@ function esm({ input, output: forceBundle ? bundleOutput : normalOutput, plugins: [ - svelte(), babelPlugin, commonJS(), nodeResolve({ extensions: ['.ts', '.tsx', '.native.ts'] }), @@ -342,7 +359,6 @@ function cjs({ input, output: forceBundle ? bundleOutput : normalOutput, plugins: [ - svelte(), babelPlugin, commonJS(), nodeResolve({ extensions: ['.ts', '.tsx', '.native.ts'] }), @@ -383,7 +399,6 @@ function umdDev({ banner, }, plugins: [ - svelte(), commonJS(), babelPlugin, nodeResolve({ extensions: ['.ts', '.tsx', '.native.ts'] }), @@ -414,7 +429,6 @@ function umdProd({ banner, }, plugins: [ - svelte(), commonJS(), babelPlugin, nodeResolve({ extensions: ['.ts', '.tsx', '.native.ts'] }), diff --git a/scripts/config.ts b/scripts/config.ts index 68d879e679a..610d8ef100f 100644 --- a/scripts/config.ts +++ b/scripts/config.ts @@ -40,6 +40,11 @@ export const packages: Package[] = [ packageDir: 'solid-query', srcDir: 'src', }, + { + name: '@tanstack/vue-query', + packageDir: 'vue-query', + srcDir: 'src', + }, ] export const latestBranch = 'main' diff --git a/tsconfig.base.json b/tsconfig.base.json index 0360ca34daa..bb51f7a7b89 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -35,6 +35,7 @@ "packages/react-query-persist-client/src" ], "@tanstack/solid-query": ["packages/solid-query/src"], + "@tanstack/vue-query": ["packages/vue-query/src"], } } } diff --git a/tsconfig.json b/tsconfig.json index 1f0f5c83ec3..9e3853018ec 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,5 +10,6 @@ { "path": "packages/react-query-devtools" }, { "path": "packages/react-query-persist-client" }, { "path": "packages/solid-query" }, + { "path": "packages/vue-query" }, ] } From 8450a91ba8d4b62670cdf79bb5c4e5cf13dffc94 Mon Sep 17 00:00:00 2001 From: Damian Osipiuk Date: Sun, 2 Oct 2022 13:38:05 +0200 Subject: [PATCH 2/8] fix: umd build --- rollup.config.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rollup.config.ts b/rollup.config.ts index ad6a120de4a..97d1ec31563 100644 --- a/rollup.config.ts +++ b/rollup.config.ts @@ -178,13 +178,12 @@ export default function rollup(options: RollupOptions): RollupOptions[] { globals: { '@tanstack/query-core': 'QueryCore', vue: 'Vue', - 'vue-demi': 'VueDemi', + 'vue-demi': 'Vue', 'match-sorter': 'MatchSorter', '@vue/devtools-api': 'DevtoolsApi', }, bundleUMDGlobals: [ '@tanstack/query-core', - 'vue-demi', 'match-sorter', '@vue/devtools-api', ], From 90131f9710b27c24a6b24690fb02fdc7943af3ad Mon Sep 17 00:00:00 2001 From: Damian Osipiuk Date: Sun, 2 Oct 2022 13:43:01 +0200 Subject: [PATCH 3/8] chore: add basic example --- .codesandbox/ci.json | 2 +- examples/vue/basic/.gitignore | 6 +++ examples/vue/basic/README.md | 6 +++ examples/vue/basic/index.html | 12 ++++++ examples/vue/basic/package.json | 19 +++++++++ examples/vue/basic/src/App.vue | 43 +++++++++++++++++++++ examples/vue/basic/src/Post.vue | 51 +++++++++++++++++++++++++ examples/vue/basic/src/Posts.vue | 55 +++++++++++++++++++++++++++ examples/vue/basic/src/main.ts | 6 +++ examples/vue/basic/src/shims-vue.d.ts | 5 +++ examples/vue/basic/src/types.d.ts | 6 +++ examples/vue/basic/tsconfig.json | 15 ++++++++ examples/vue/basic/vite.config.ts | 11 ++++++ pnpm-workspace.yaml | 2 +- 14 files changed, 237 insertions(+), 2 deletions(-) create mode 100644 examples/vue/basic/.gitignore create mode 100644 examples/vue/basic/README.md create mode 100644 examples/vue/basic/index.html create mode 100644 examples/vue/basic/package.json create mode 100644 examples/vue/basic/src/App.vue create mode 100644 examples/vue/basic/src/Post.vue create mode 100644 examples/vue/basic/src/Posts.vue create mode 100644 examples/vue/basic/src/main.ts create mode 100644 examples/vue/basic/src/shims-vue.d.ts create mode 100644 examples/vue/basic/src/types.d.ts create mode 100644 examples/vue/basic/tsconfig.json create mode 100644 examples/vue/basic/vite.config.ts diff --git a/.codesandbox/ci.json b/.codesandbox/ci.json index eb804d66fa5..c6358167dea 100644 --- a/.codesandbox/ci.json +++ b/.codesandbox/ci.json @@ -1,6 +1,6 @@ { "installCommand": "install:csb", - "sandboxes": ["/examples/react/basic", "/examples/react/basic-typescript", "/examples/solid/basic-typescript"], + "sandboxes": ["/examples/react/basic", "/examples/react/basic-typescript", "/examples/solid/basic-typescript", "/examples/vue/basic"], "packages": ["packages/**"], "node": "16" } diff --git a/examples/vue/basic/.gitignore b/examples/vue/basic/.gitignore new file mode 100644 index 00000000000..d424f6a89a8 --- /dev/null +++ b/examples/vue/basic/.gitignore @@ -0,0 +1,6 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local +package-lock.json diff --git a/examples/vue/basic/README.md b/examples/vue/basic/README.md new file mode 100644 index 00000000000..7573cb91da8 --- /dev/null +++ b/examples/vue/basic/README.md @@ -0,0 +1,6 @@ +# Basic example + +To run this example: + +- `npm install` or `yarn` +- `npm run dev` or `yarn dev` diff --git a/examples/vue/basic/index.html b/examples/vue/basic/index.html new file mode 100644 index 00000000000..547b3c19e69 --- /dev/null +++ b/examples/vue/basic/index.html @@ -0,0 +1,12 @@ + + + + + + Vue Query Example + + +
+ + + diff --git a/examples/vue/basic/package.json b/examples/vue/basic/package.json new file mode 100644 index 00000000000..faddca4d24a --- /dev/null +++ b/examples/vue/basic/package.json @@ -0,0 +1,19 @@ +{ + "name": "@tanstack/query-example-vue-basic", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "build:dev": "vite build -m development", + "serve": "vite preview" + }, + "dependencies": { + "vue": "3.2.39", + "@tanstack/vue-query": "^4.9.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "3.1.0", + "typescript": "4.8.4", + "vite": "3.1.4" + } +} diff --git a/examples/vue/basic/src/App.vue b/examples/vue/basic/src/App.vue new file mode 100644 index 00000000000..52028d9dcdb --- /dev/null +++ b/examples/vue/basic/src/App.vue @@ -0,0 +1,43 @@ + + + diff --git a/examples/vue/basic/src/Post.vue b/examples/vue/basic/src/Post.vue new file mode 100644 index 00000000000..bb9f36e0895 --- /dev/null +++ b/examples/vue/basic/src/Post.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/examples/vue/basic/src/Posts.vue b/examples/vue/basic/src/Posts.vue new file mode 100644 index 00000000000..12ee8ef15e1 --- /dev/null +++ b/examples/vue/basic/src/Posts.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/examples/vue/basic/src/main.ts b/examples/vue/basic/src/main.ts new file mode 100644 index 00000000000..7c6ec1d6b81 --- /dev/null +++ b/examples/vue/basic/src/main.ts @@ -0,0 +1,6 @@ +import { createApp } from "vue"; +import { VueQueryPlugin } from "@tanstack/vue-query"; + +import App from "./App.vue"; + +createApp(App).use(VueQueryPlugin).mount("#app"); diff --git a/examples/vue/basic/src/shims-vue.d.ts b/examples/vue/basic/src/shims-vue.d.ts new file mode 100644 index 00000000000..daba9b9ec05 --- /dev/null +++ b/examples/vue/basic/src/shims-vue.d.ts @@ -0,0 +1,5 @@ +declare module "*.vue" { + import { DefineComponent } from "vue"; + const component: DefineComponent<{}, {}, any>; + export default component; +} diff --git a/examples/vue/basic/src/types.d.ts b/examples/vue/basic/src/types.d.ts new file mode 100644 index 00000000000..0ff5fbb96df --- /dev/null +++ b/examples/vue/basic/src/types.d.ts @@ -0,0 +1,6 @@ +export interface Post { + userId: number; + id: number; + title: string; + body: string; +} diff --git a/examples/vue/basic/tsconfig.json b/examples/vue/basic/tsconfig.json new file mode 100644 index 00000000000..e754e65292f --- /dev/null +++ b/examples/vue/basic/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "moduleResolution": "node", + "strict": true, + "jsx": "preserve", + "sourceMap": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "lib": ["esnext", "dom"], + "types": ["vite/client"] + }, + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"] +} diff --git a/examples/vue/basic/vite.config.ts b/examples/vue/basic/vite.config.ts new file mode 100644 index 00000000000..499a4250d64 --- /dev/null +++ b/examples/vue/basic/vite.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "vite"; +import vue from "@vitejs/plugin-vue"; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [vue()], + optimizeDeps: { + include: ["remove-accents"], + exclude: ["vue-query", "vue-demi"], + }, +}); diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 17e81edac70..0ef5cb73458 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,4 +2,4 @@ packages: - 'packages/**' - 'examples/react/**' - 'examples/solid/**' - - 'examples/vue/**' + # - 'examples/vue/**' From 2b76226365903385b58e8554ea9a0f0c375049c8 Mon Sep 17 00:00:00 2001 From: Damian Osipiuk Date: Sun, 2 Oct 2022 14:19:20 +0200 Subject: [PATCH 4/8] docs: add vue adapter page --- docs/adapters/vue-query.md | 51 +++++++++++++++++++++++++++++++++++--- docs/config.json | 4 +-- 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/docs/adapters/vue-query.md b/docs/adapters/vue-query.md index 4bebc13b675..e2dfb6c9130 100644 --- a/docs/adapters/vue-query.md +++ b/docs/adapters/vue-query.md @@ -1,7 +1,52 @@ --- -title: Vue Query (Coming Soon) +title: Vue Query --- -> ⚠️ This module has not yet been developed. It requires an adapter similar to `react-query` to work. We estimate the amount of code to do this is low-to-moderate, but does require familiarity with the Vue framework. If you would like to contribute this adapter, please open a PR! +The `vue-query` package offers a 1st-class API for using TanStack Query via Vue. However, all of the primitives you receive from these hooks are core APIs that are shared across all of the TanStack Adapters including the Query Client, query results, query subscriptions, etc. -The `@tanstack/vue-query` package offers a 1st-class API for using TanStack Query via Vue. However, all of the primitives you receive from this API are core APIs that are shared across all of the TanStack Adapters including the Query Client, query results, query subscriptions, etc. +## Example + +This example very briefly illustrates the 3 core concepts of Vue Query: + +- [Queries](guides/queries) +- [Mutations](guides/mutations) +- [Query Invalidation](guides/query-invalidation) + +```vue + + + +``` + +These three concepts make up most of the core functionality of Vue Query. The next sections of the documentation will go over each of these core concepts in great detail. \ No newline at end of file diff --git a/docs/config.json b/docs/config.json index 670ce3c920c..76c3c74fce1 100644 --- a/docs/config.json +++ b/docs/config.json @@ -58,8 +58,8 @@ "to": "adapters/solid-query" }, { - "label": "Vue Query (Coming Soon)", - "to": "#" + "label": "Vue Query", + "to": "adapters/vue-query" }, { "label": "Svelte Query (Coming Soon)", From 84d8542db5ea850bb2b1d9ffe6d6cd84325a8d08 Mon Sep 17 00:00:00 2001 From: Damian Osipiuk Date: Sun, 2 Oct 2022 14:33:31 +0200 Subject: [PATCH 5/8] docs: add readme --- packages/vue-query/README.md | 81 +++++++++++++++++++++++++ packages/vue-query/media/vue-query.png | Bin 0 -> 12056 bytes packages/vue-query/media/vue-query.svg | 4 ++ 3 files changed, 85 insertions(+) create mode 100644 packages/vue-query/README.md create mode 100644 packages/vue-query/media/vue-query.png create mode 100644 packages/vue-query/media/vue-query.svg diff --git a/packages/vue-query/README.md b/packages/vue-query/README.md new file mode 100644 index 00000000000..24ed954da34 --- /dev/null +++ b/packages/vue-query/README.md @@ -0,0 +1,81 @@ +[![Vue Query logo](./media/vue-query.png)](https://damianosipiuk.github.io/vue-query/) + +[![npm version](https://img.shields.io/npm/v/vue-query)](https://www.npmjs.com/package/vue-query) +[![npm license](https://img.shields.io/npm/l/vue-query)](https://github.com/DamianOsipiuk/vue-query/blob/main/LICENSE) +[![bundle size](https://img.shields.io/bundlephobia/minzip/vue-query)](https://bundlephobia.com/result?p=vue-query) +[![npm](https://img.shields.io/npm/dm/vue-query)](https://www.npmjs.com/package/vue-query) + +# Vue Query + +Hooks for fetching, caching and updating asynchronous data in Vue. + +Support for Vue 2.x via [vue-demi](https://github.com/vueuse/vue-demi) + +# Documentation + +Visit https://tanstack.com/query/v4/docs/adapters/vue-query + +# Quick Features + +- Transport/protocol/backend agnostic data fetching (REST, GraphQL, promises, whatever!) +- Auto Caching + Refetching (stale-while-revalidate, Window Refocus, Polling/Realtime) +- Parallel + Dependent Queries +- Mutations + Reactive Query Refetching +- Multi-layer Cache + Automatic Garbage Collection +- Paginated + Cursor-based Queries +- Load-More + Infinite Scroll Queries w/ Scroll Recovery +- Request Cancellation +- (experimental) [Suspense](https://v3.vuejs.org/guide/migration/suspense.html#introduction) + Fetch-As-You-Render Query Prefetching +- (experimental) SSR support +- Dedicated Devtools +- [![npm bundle size](https://img.shields.io/bundlephobia/minzip/vue-query)](https://bundlephobia.com/result?p=vue-query) (depending on features imported) + +# Quick Start + +1. Install `vue-query` + + ```bash + npm install vue-query + # or + yarn add vue-query + ``` + + > If you are using Vue 2.x, make sure to also setup [@vue/composition-api](https://github.com/vuejs/composition-api) + +2. Initialize **Vue Query** via **VueQueryPlugin** + + ```ts + import { createApp } from "vue"; + import { VueQueryPlugin } from "vue-query"; + + import App from "./App.vue"; + + createApp(App).use(VueQueryPlugin).mount("#app"); + ``` + +3. Use query + + ```ts + import { defineComponent } from "vue"; + import { useQuery } from "vue-query"; + + export default defineComponent({ + name: "MyComponent", + setup() { + const query = useQuery("todos", getTodos); + + return { + query, + }; + }, + }); + ``` + +4. If you need to update options on your query dynamically, make sure to pass them as reactive variables + + ```ts + const id = ref(1); + const enabled = ref(false); + + const query = useQuery(["todos", id], () => getTodos(id), { enabled }); + ``` diff --git a/packages/vue-query/media/vue-query.png b/packages/vue-query/media/vue-query.png new file mode 100644 index 0000000000000000000000000000000000000000..9d9b0183c0add6db6d781b77f1c75903732c882b GIT binary patch literal 12056 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 + + + From 091c49a331f4db993f0047ef7036ec7f0a19681e Mon Sep 17 00:00:00 2001 From: Damian Osipiuk Date: Sun, 2 Oct 2022 21:51:51 +0200 Subject: [PATCH 6/8] fix: shields in readme, package ver, ts config --- packages/vue-query/README.md | 20 +++++++++++--------- packages/vue-query/package.json | 2 +- packages/vue-query/tsconfig.json | 1 - 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/vue-query/README.md b/packages/vue-query/README.md index 24ed954da34..2b2fae6f7c5 100644 --- a/packages/vue-query/README.md +++ b/packages/vue-query/README.md @@ -1,9 +1,9 @@ -[![Vue Query logo](./media/vue-query.png)](https://damianosipiuk.github.io/vue-query/) +[![Vue Query logo](./media/vue-query.png)](https://github.com/TanStack/query/tree/main/packages/vue-query) -[![npm version](https://img.shields.io/npm/v/vue-query)](https://www.npmjs.com/package/vue-query) -[![npm license](https://img.shields.io/npm/l/vue-query)](https://github.com/DamianOsipiuk/vue-query/blob/main/LICENSE) -[![bundle size](https://img.shields.io/bundlephobia/minzip/vue-query)](https://bundlephobia.com/result?p=vue-query) -[![npm](https://img.shields.io/npm/dm/vue-query)](https://www.npmjs.com/package/vue-query) +[![npm version](https://img.shields.io/npm/v/@tanstack/vue-query)](https://www.npmjs.com/package/@tanstack/vue-query) +[![npm license](https://img.shields.io/npm/l/@tanstack/vue-query)](https://github.com/TanStack/query/blob/main/LICENSE) +[![bundle size](https://img.shields.io/bundlephobia/minzip/@tanstack/vue-query)](https://bundlephobia.com/package/@tanstack/vue-query) +[![npm](https://img.shields.io/npm/dm/@tanstack/vue-query)](https://www.npmjs.com/package/@tanstack/vue-query) # Vue Query @@ -35,12 +35,14 @@ Visit https://tanstack.com/query/v4/docs/adapters/vue-query 1. Install `vue-query` ```bash - npm install vue-query + $ npm i @tanstack/vue-query # or - yarn add vue-query + $ pnpm add @tanstack/vue-query + # or + $ yarn add @tanstack/vue-query ``` - > If you are using Vue 2.x, make sure to also setup [@vue/composition-api](https://github.com/vuejs/composition-api) + > If you are using Vue 2.6, make sure to also setup [@vue/composition-api](https://github.com/vuejs/composition-api) 2. Initialize **Vue Query** via **VueQueryPlugin** @@ -62,7 +64,7 @@ Visit https://tanstack.com/query/v4/docs/adapters/vue-query export default defineComponent({ name: "MyComponent", setup() { - const query = useQuery("todos", getTodos); + const query = useQuery(["todos"], getTodos); return { query, diff --git a/packages/vue-query/package.json b/packages/vue-query/package.json index 670fce4dad9..54e3b31df58 100644 --- a/packages/vue-query/package.json +++ b/packages/vue-query/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/vue-query", - "version": "4.7.2", + "version": "4.8.0", "description": "Hooks for managing, caching and syncing asynchronous and remote data in Vue", "author": "Damian Osipiuk", "license": "MIT", diff --git a/packages/vue-query/tsconfig.json b/packages/vue-query/tsconfig.json index 48a8c7efce5..85281afb6c3 100644 --- a/packages/vue-query/tsconfig.json +++ b/packages/vue-query/tsconfig.json @@ -7,7 +7,6 @@ "tsBuildInfoFile": "./build/.tsbuildinfo" }, "include": ["src"], - "exclude": ["src/__tests__"], "references": [ { "path": "../query-core" } ] From 769b6c11607d5d850b6fa6df9c4331993d13ca69 Mon Sep 17 00:00:00 2001 From: Damian Osipiuk Date: Sun, 2 Oct 2022 21:53:08 +0200 Subject: [PATCH 7/8] docs: fix one more link --- packages/vue-query/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vue-query/README.md b/packages/vue-query/README.md index 2b2fae6f7c5..f8f40096775 100644 --- a/packages/vue-query/README.md +++ b/packages/vue-query/README.md @@ -28,7 +28,7 @@ Visit https://tanstack.com/query/v4/docs/adapters/vue-query - (experimental) [Suspense](https://v3.vuejs.org/guide/migration/suspense.html#introduction) + Fetch-As-You-Render Query Prefetching - (experimental) SSR support - Dedicated Devtools -- [![npm bundle size](https://img.shields.io/bundlephobia/minzip/vue-query)](https://bundlephobia.com/result?p=vue-query) (depending on features imported) +- [![npm bundle size](https://img.shields.io/bundlephobia/minzip/@tanstack/vue-query)](https://bundlephobia.com/package/@tanstack/vue-query) (depending on features imported) # Quick Start From 3c82c83ebe8facb5f76bab12fb7e29f7c5b13ebf Mon Sep 17 00:00:00 2001 From: Damian Osipiuk Date: Sun, 2 Oct 2022 21:57:10 +0200 Subject: [PATCH 8/8] fix: removed unused params --- packages/vue-query/src/__tests__/test-utils.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/vue-query/src/__tests__/test-utils.ts b/packages/vue-query/src/__tests__/test-utils.ts index 7844abdf340..fa1efb7e750 100644 --- a/packages/vue-query/src/__tests__/test-utils.ts +++ b/packages/vue-query/src/__tests__/test-utils.ts @@ -32,7 +32,7 @@ export function infiniteFetcher({ } export function rejectFetcher(): Promise { - return new Promise((resolve, reject) => { + return new Promise((_, reject) => { setTimeout(() => { return reject(new Error('Some error')) }, 0) @@ -47,7 +47,6 @@ export function successMutator(param: T): Promise { }) } -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export function errorMutator(param: T): Promise { +export function errorMutator(_: T): Promise { return rejectFetcher() }