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/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)", 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/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/README.md b/packages/vue-query/README.md new file mode 100644 index 00000000000..f8f40096775 --- /dev/null +++ b/packages/vue-query/README.md @@ -0,0 +1,83 @@ +[![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/@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 + +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/@tanstack/vue-query)](https://bundlephobia.com/package/@tanstack/vue-query) (depending on features imported) + +# Quick Start + +1. Install `vue-query` + + ```bash + $ npm i @tanstack/vue-query + # or + $ pnpm add @tanstack/vue-query + # or + $ yarn add @tanstack/vue-query + ``` + + > 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** + + ```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/__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/media/vue-query.png b/packages/vue-query/media/vue-query.png new file mode 100644 index 00000000000..9d9b0183c0a Binary files /dev/null and b/packages/vue-query/media/vue-query.png differ diff --git a/packages/vue-query/media/vue-query.svg b/packages/vue-query/media/vue-query.svg new file mode 100644 index 00000000000..46684b66579 --- /dev/null +++ b/packages/vue-query/media/vue-query.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/vue-query/package.json b/packages/vue-query/package.json new file mode 100644 index 00000000000..54e3b31df58 --- /dev/null +++ b/packages/vue-query/package.json @@ -0,0 +1,55 @@ +{ + "name": "@tanstack/vue-query", + "version": "4.8.0", + "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..fa1efb7e750 --- /dev/null +++ b/packages/vue-query/src/__tests__/test-utils.ts @@ -0,0 +1,52 @@ +/* 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((_, reject) => { + setTimeout(() => { + return reject(new Error('Some error')) + }, 0) + }) +} + +export function successMutator(param: T): Promise { + return new Promise((resolve) => { + setTimeout(() => { + return resolve(param) + }, 0) + }) +} + +export function errorMutator(_: 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..85281afb6c3 --- /dev/null +++ b/packages/vue-query/tsconfig.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/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..0ef5cb73458 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..97d1ec31563 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,25 @@ 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': 'Vue', + 'match-sorter': 'MatchSorter', + '@vue/devtools-api': 'DevtoolsApi', + }, + bundleUMDGlobals: [ + '@tanstack/query-core', + 'match-sorter', + '@vue/devtools-api', + ], + }), ] } @@ -260,7 +278,6 @@ function mjs({ input, output: forceBundle ? bundleOutput : normalOutput, plugins: [ - svelte(), babelPlugin, commonJS(), nodeResolve({ extensions: ['.ts', '.tsx', '.native.ts'] }), @@ -300,7 +317,6 @@ function esm({ input, output: forceBundle ? bundleOutput : normalOutput, plugins: [ - svelte(), babelPlugin, commonJS(), nodeResolve({ extensions: ['.ts', '.tsx', '.native.ts'] }), @@ -342,7 +358,6 @@ function cjs({ input, output: forceBundle ? bundleOutput : normalOutput, plugins: [ - svelte(), babelPlugin, commonJS(), nodeResolve({ extensions: ['.ts', '.tsx', '.native.ts'] }), @@ -383,7 +398,6 @@ function umdDev({ banner, }, plugins: [ - svelte(), commonJS(), babelPlugin, nodeResolve({ extensions: ['.ts', '.tsx', '.native.ts'] }), @@ -414,7 +428,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" }, ] }