From 224e8794a4386345989a98546dc11ae1ff2aedf6 Mon Sep 17 00:00:00 2001 From: DaniAkash Date: Sat, 18 Feb 2023 23:10:07 +0530 Subject: [PATCH 01/17] feat: added Readme for svelte-query --- packages/svelte-query/README.md | 111 ++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 packages/svelte-query/README.md diff --git a/packages/svelte-query/README.md b/packages/svelte-query/README.md new file mode 100644 index 000000000..ed893d747 --- /dev/null +++ b/packages/svelte-query/README.md @@ -0,0 +1,111 @@ +# WunderGraph Svelte Query Integration + +![wunderctl](https://img.shields.io/npm/v/@wundergraph/svelte-query.svg) + +This package provides a type-safe integration of [@tanstack/svelte-query](https://tanstack.com/query/latest/docs/svelte/overview) with WunderGraph. +Svelte Query is a data fetching library for Svelte apps. With simple utilities, you can significantly simplify the data fetching logic in your project. And it also covered in all aspects of speed, correctness, and stability to help you build better experiences. + +> **Warning**: Only works with WunderGraph. + +## Getting Started + +```shell +npm install @wundergraph/svelte-query @tanstack/svelte-query +``` + +Before you can use the utilities, you need to modify your code generation to include the base typescript client. + +```typescript +// wundergraph.config.ts +configureWunderGraphApplication({ + // ... omitted for brevity + codeGenerators: [ + { + templates: [templates.typescript.client], + // the location where you want to generate the client + path: '../src/components/generated', + }, + ], +}); +``` + +Second, run `wunderctl generate` to generate the code. + +Now you can use the utility functions. + +```ts +import { createQueryUtils } from '@wundergraph/svelte-query'; +import { createClient } from '../generated/client'; +import type { Operations } from '../generated/client'; + +const client = createClient(); // Typesafe WunderGraph client + +// These utility functions needs to be imported into your app +export const { createQuery, createFileUpload, createMutation, createSubscription, getAuth, getUser, queryKey } = + createQueryUtils(client); +``` + +Now, in your svelte layout setup Svelte Query Provider such that it is always wrapping above the rest of the app. + +```svelte + + +
+ + + +
+``` + +Now you can use svelte-query to call your wundergraph operations! + +```svelte + + +
+

Simple Query

+
+ {#if $query.isLoading} + Loading... + {/if} + {#if $query.error} + An error has occurred: + {$query.error.message} + {/if} + {#if $query.isSuccess} +
+
{JSON.stringify($query.data.starwars_allPeople)}
+
+ {/if} +
+
+``` + +## Options + +You can use all available options from [Svelte Query](https://tanstack.com/query/latest/docs/svelte/overview) with the generated functions. +Due to the fact that we use the operationName + variables as **key**, you can't use the `key` option as usual. +In order to use conditional-fetching you can use the `enabled` option. + +## Global Configuration + +You can configure the utilities globally by using the Svelte Query's [QueryClient](https://tanstack.com/query/v4/docs/react/reference/QueryClient) config. From cbbfd5def5a4836ca6389292f8c6b204303b9438 Mon Sep 17 00:00:00 2001 From: DaniAkash Date: Sat, 18 Feb 2023 23:20:04 +0530 Subject: [PATCH 02/17] chore: setup base files needed for the project --- packages/svelte-query/.npmignore | 6 +++ packages/svelte-query/CHANGELOG.md | 4 ++ packages/svelte-query/jest-setup.ts | 1 + packages/svelte-query/jest.config.js | 11 +++++ packages/svelte-query/package.json | 53 +++++++++++++++++++++++ packages/svelte-query/tsconfig.build.json | 4 ++ packages/svelte-query/tsconfig.json | 14 ++++++ 7 files changed, 93 insertions(+) create mode 100644 packages/svelte-query/.npmignore create mode 100644 packages/svelte-query/CHANGELOG.md create mode 100644 packages/svelte-query/jest-setup.ts create mode 100644 packages/svelte-query/jest.config.js create mode 100644 packages/svelte-query/package.json create mode 100644 packages/svelte-query/tsconfig.build.json create mode 100644 packages/svelte-query/tsconfig.json diff --git a/packages/svelte-query/.npmignore b/packages/svelte-query/.npmignore new file mode 100644 index 000000000..53d6ad162 --- /dev/null +++ b/packages/svelte-query/.npmignore @@ -0,0 +1,6 @@ +node_modules +src +tsconfig.json +.gitignore +.npmrc +dist/tsconfig.tsbuildinfo diff --git a/packages/svelte-query/CHANGELOG.md b/packages/svelte-query/CHANGELOG.md new file mode 100644 index 000000000..e4d87c4d4 --- /dev/null +++ b/packages/svelte-query/CHANGELOG.md @@ -0,0 +1,4 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. diff --git a/packages/svelte-query/jest-setup.ts b/packages/svelte-query/jest-setup.ts new file mode 100644 index 000000000..7b0828bfa --- /dev/null +++ b/packages/svelte-query/jest-setup.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom'; diff --git a/packages/svelte-query/jest.config.js b/packages/svelte-query/jest.config.js new file mode 100644 index 000000000..558e4b2c8 --- /dev/null +++ b/packages/svelte-query/jest.config.js @@ -0,0 +1,11 @@ +module.exports = { + testEnvironment: 'jsdom', + testRegex: '/tests/.*\\.test\\.tsx?$', + setupFilesAfterEnv: ['/jest-setup.ts'], + transform: { + '^.+\\.(t|j)sx?$': '@swc/jest', + }, + coveragePathIgnorePatterns: ['/node_modules/', '/dist/'], + coverageReporters: ['text', 'html'], + reporters: ['default', 'github-actions'], +}; diff --git a/packages/svelte-query/package.json b/packages/svelte-query/package.json new file mode 100644 index 000000000..36b702bde --- /dev/null +++ b/packages/svelte-query/package.json @@ -0,0 +1,53 @@ +{ + "name": "@wundergraph/svelte-query", + "version": "0.0.1", + "license": "Apache-2.0", + "description": "WunderGraph Svelte Query Integration", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "scripts": { + "build": "tsc -p tsconfig.build.json", + "test": "jest && tsd" + }, + "tsd": { + "directory": "tests" + }, + "publishConfig": { + "registry": "https://registry.npmjs.org", + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/wundergraph/wundergraph.git" + }, + "peerDependencies": { + "@wundergraph/sdk": ">=0.110.0", + "@tanstack/svelte-query": "^4.24.6" + }, + "devDependencies": { + "@swc/core": "^1.3.14", + "@swc/jest": "^0.2.23", + "@testing-library/jest-dom": "^5.16.5", + "@testing-library/svelte": "^3.2.2", + "@types/jest": "^28.1.1", + "@types/node-fetch": "2.6.2", + "@wundergraph/sdk": "workspace:*", + "jest": "^29.0.3", + "jest-environment-jsdom": "^29.3.0", + "nock": "^13.2.9", + "node-fetch": "2.6.7", + "@tanstack/svelte-query": "^4.24.6", + "tsd": "^0.24.1", + "typescript": "^4.8.2" + }, + "homepage": "https://wundergraph.com", + "author": { + "name": "WunderGraph Maintainers", + "email": "info@wundergraph.com" + }, + "keywords": [ + "svelte-query", + "wundergraph", + "svelte" + ] +} diff --git a/packages/svelte-query/tsconfig.build.json b/packages/svelte-query/tsconfig.build.json new file mode 100644 index 000000000..bdc63200b --- /dev/null +++ b/packages/svelte-query/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*"] +} diff --git a/packages/svelte-query/tsconfig.json b/packages/svelte-query/tsconfig.json new file mode 100644 index 000000000..17190b706 --- /dev/null +++ b/packages/svelte-query/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "target": "ES2019", + "module": "commonjs", + "moduleResolution": "node", + "declarationDir": "./dist", + "jsx": "react", + "outDir": "./dist", + "typeRoots": ["./node_modules/@types", "./src/typedefinitions"] + }, + "include": ["src/**/*", "tests"], + "exclude": ["node_modules", "dist"] +} From 0119434fe640fc46408c685d9f2383019d28b436 Mon Sep 17 00:00:00 2001 From: DaniAkash Date: Sat, 18 Feb 2023 23:23:40 +0530 Subject: [PATCH 03/17] feat: added svelte-query utilities --- packages/svelte-query/src/createQueryUtils.ts | 263 ++++++++++++++++++ packages/svelte-query/src/index.ts | 3 + packages/svelte-query/src/types.ts | 201 +++++++++++++ 3 files changed, 467 insertions(+) create mode 100644 packages/svelte-query/src/createQueryUtils.ts create mode 100644 packages/svelte-query/src/index.ts create mode 100644 packages/svelte-query/src/types.ts diff --git a/packages/svelte-query/src/createQueryUtils.ts b/packages/svelte-query/src/createQueryUtils.ts new file mode 100644 index 000000000..052d3e100 --- /dev/null +++ b/packages/svelte-query/src/createQueryUtils.ts @@ -0,0 +1,263 @@ +import { + createQuery as tanstackCreateQuery, + createMutation as tanstackCreateMutation, + useQueryClient, +} from '@tanstack/svelte-query'; +import type { QueryFunctionContext } from '@tanstack/svelte-query'; +import type { OperationsDefinition, LogoutOptions, Client } from '@wundergraph/sdk/client'; +// import { serialize } from '@wundergraph/sdk/internal'; +import type { + CreateFileUpload, + CreateMutation, + CreateQuery, + CreateSubscribeToProps, + CreateSubscription, + GetUser, + MutationFetcher, + QueryFetcher, + QueryKey, + SubscribeToOptions, +} from './types'; + +export const userQueryKey = 'wg_user'; + +export function createQueryUtils(client: Client) { + const queryFetcher: QueryFetcher = async (query) => { + const result = await client.query(query); + + if (result.error) { + throw result.error; + } + + return result.data; + }; + + const queryKey: QueryKey = ({ operationName, input }) => { + return [operationName, input]; + }; + + const mutationFetcher: MutationFetcher = async (mutation) => { + const result = await client.mutate(mutation); + + if (result.error) { + throw result.error; + } + + return result.data; + }; + + /** + * Execute a WunderGraph query. + * + * @usage + * ```ts + * const { data, error, isLoading } = createQuery({ + * operationName: 'Weather', + * }) + * ``` + * + * All queries support liveQuery by default, enabling this will set up a realtime subscription. + * ```ts + * const { data, error, isLoading, isSubscribed } = useQuery({ + * operationName: 'Weather', + * liveQuery: true, + * }) + * ``` + */ + const createQuery: CreateQuery = (options) => { + const { operationName, liveQuery, input, enabled, refetchOnWindowFocus, ...queryOptions } = options; + + // TODO: will be needed for subscriptions + // const queryHash = serialize([operationName, input]); + + const result = tanstackCreateQuery({ + queryKey: queryKey({ operationName, input }), + queryFn: ({ signal }: QueryFunctionContext) => queryFetcher({ operationName, input, abortSignal: signal }), + ...queryOptions, + enabled: liveQuery ? false : enabled, + refetchOnWindowFocus: liveQuery ? false : refetchOnWindowFocus, + }); + + // TODO: Learn about how to build subscription utility in svelte + // const { isSubscribed } = useSubscribeTo({ + // queryHash, + // operationName, + // input, + // liveQuery, + // enabled: options.enabled !== false && liveQuery, + // onSuccess: options.onSuccess, + // onError: options.onError + // }); + + if (liveQuery) { + return { + ...result, + // TODO: Subscription not ready yet + // isSubscribed + }; + } + return result; + }; + + /** + * Execute a WunderGraph mutation. + * + * @usage + * ```ts + * const { mutate, data, error, isLoading } = createMutation({ + * operationName: 'SetName' + * }) + * + * mutate({ + * name: 'John Doe' + * }) + * ``` + */ + const createMutation: CreateMutation = (options) => { + const { operationName, ...mutationOptions } = options; + + return tanstackCreateMutation({ + mutationKey: [operationName], + mutationFn: (input) => mutationFetcher({ operationName, input }), + ...mutationOptions, + }); + }; + + const getAuth = () => { + const queryClient = useQueryClient(); + + return { + login: (authProviderID: Operations['authProvider'], redirectURI?: string | undefined) => + client.login(authProviderID, redirectURI), + logout: async (options?: LogoutOptions | undefined) => { + const result = await client.logout(options); + // reset user in the cache and don't trigger a refetch + queryClient.setQueryData([userQueryKey], null); + return result; + }, + }; + }; + + /** + * Return the logged in user. + * + * @usage + * ```ts + * const { user, error, isLoading } = getUser() + * ``` + */ + const getUser: GetUser = (options) => { + const { revalidate, ...queryOptions } = options || {}; + return tanstackCreateQuery( + [userQueryKey], + ({ signal }) => + client.fetchUser({ + revalidate, + abortSignal: signal, + }), + queryOptions + ); + }; + + /** + * Upload a file to S3 compatible storage. + * + * @usage + * ```ts + * const { upload, data, error } = createFileUpload() + * + * const uploadFile = (file: File) => { + * upload(file) + * } + * ``` + */ + const createFileUpload: CreateFileUpload = (options) => { + const { mutate, mutateAsync, ...mutation } = tanstackCreateMutation( + ['uploadFiles'], + async (input) => { + const resp = await client.uploadFiles(input); + return resp.fileKeys; + }, + options + ) as any; + + return { + upload: mutate, + uploadAsync: mutateAsync, + ...mutation, + }; + }; + + // Set up a subscription that can be aborted. + const subscribeTo = (options: SubscribeToOptions) => { + const abort = new AbortController(); + + const { onSuccess, onError, onResult, onAbort, ...subscription } = options; + + subscription.abortSignal = abort.signal; + + client.subscribe(subscription, onResult).catch(onError); + + return () => { + onAbort?.(); + abort.abort(); + }; + }; + + // Helper function used in createQuery and createSubscription + const createSubscribeTo = (props: CreateSubscribeToProps) => { + const client = useQueryClient(); + const { queryHash, operationName, input, enabled, liveQuery, subscribeOnce, resetOnMount, onSuccess, onError } = + props; + + //TODO: Svelte style utility to setup subscriptions + }; + + /** + * createSubscription + * + * Subscribe to subscription operations. + * + * @usage + * ```ts + * const { data, error, isLoading, isSubscribed } = useSubscription({ + * operationName: 'Countdown', + * }) + */ + const createSubscription: CreateSubscription = (options) => { + const { enabled = true, operationName, input, subscribeOnce, onSuccess, onError } = options; + // const queryHash = serialize([operationName, input]); + + const subscription = tanstackCreateQuery({ + queryKey: [operationName, input], + enabled: false, // we update the cache async + }); + + // TODO: Learn about how to build subscription utility in svelte + // const { isSubscribed } = useSubscribeTo({ + // queryHash, + // operationName, + // input, + // subscribeOnce, + // enabled, + // onSuccess, + // onError + // }); + + return { + ...subscription, + // TODO: set actual value + isSubscribed: false, + }; + }; + + return { + createQuery, + createMutation, + getAuth, + getUser, + createFileUpload, + queryKey, + createSubscription, + }; +} diff --git a/packages/svelte-query/src/index.ts b/packages/svelte-query/src/index.ts new file mode 100644 index 000000000..8e56fa248 --- /dev/null +++ b/packages/svelte-query/src/index.ts @@ -0,0 +1,3 @@ +export { createQueryUtils, userQueryKey } from './createQueryUtils'; + +export * from './types'; diff --git a/packages/svelte-query/src/types.ts b/packages/svelte-query/src/types.ts new file mode 100644 index 000000000..ec659ac73 --- /dev/null +++ b/packages/svelte-query/src/types.ts @@ -0,0 +1,201 @@ +import type { + ClientResponse, + SubscriptionRequestOptions, + OperationRequestOptions, + FetchUserRequestOptions, + OperationsDefinition, + WithInput, + ClientResponseError, + UploadRequestOptions, + UploadRequestOptionsWithProfile, +} from '@wundergraph/sdk/client'; + +import type { + CreateQueryOptions as TanstackCreateQueryOptions, + CreateQueryResult, + CreateMutationOptions as TanstackCreateMutationOptions, + CreateMutationResult, +} from '@tanstack/svelte-query'; + +export type QueryFetcher = { + < + OperationName extends Extract, + Data extends Operations['queries'][OperationName]['data'] = Operations['queries'][OperationName]['data'], + RequestOptions extends OperationRequestOptions< + Extract, + Operations['queries'][OperationName]['input'] + > = OperationRequestOptions< + Extract, + Operations['queries'][OperationName]['input'] + > + >( + query: RequestOptions + ): Promise; +}; + +export type MutationFetcher = { + < + OperationName extends Extract, + Data extends Operations['mutations'][OperationName]['data'] = Operations['mutations'][OperationName]['data'], + RequestOptions extends OperationRequestOptions< + Extract, + Operations['mutations'][OperationName]['input'] + > = OperationRequestOptions< + Extract, + Operations['mutations'][OperationName]['input'] + > + >( + mutation: RequestOptions + ): Promise; +}; + +export type QueryKey = { + < + OperationName extends Extract, + Input extends Operations['queries'][OperationName]['input'] = Operations['queries'][OperationName]['input'] + >(query: { + operationName: OperationName; + input?: Input; + }): (OperationName | Input | undefined)[]; +}; + +export type CreateQueryOptions< + Data, + Error, + Input extends object | undefined, + OperationName extends string, + LiveQuery +> = Omit, 'queryKey' | 'queryFn'> & + WithInput< + Input, + { + operationName: OperationName; + liveQuery?: LiveQuery; + input?: Input; + } + >; + +export type CreateQuery = { + < + OperationName extends Extract, + Input extends Operations['queries'][OperationName]['input'] = Operations['queries'][OperationName]['input'], + Data extends Operations['queries'][OperationName]['data'] = Operations['queries'][OperationName]['data'], + LiveQuery extends Operations['queries'][OperationName]['liveQuery'] = Operations['queries'][OperationName]['liveQuery'] + >( + options: CreateQueryOptions & ExtraOptions + ): CreateQueryResult & { isSubscribed?: boolean }; +}; + +export type UseSubscriptionOptions< + Data, + Error, + Input extends object | undefined, + OperationName extends string +> = WithInput< + Input, + { + operationName: OperationName; + subscribeOnce?: boolean; + resetOnMount?: boolean; + enabled?: boolean; + input?: Input; + onSuccess?(response: ClientResponse): void; + onError?(error: Error): void; + } +>; + +export type CreateSubscription = { + < + OperationName extends Extract, + Input extends Operations['subscriptions'][OperationName]['input'] = Operations['subscriptions'][OperationName]['input'], + Data extends Operations['subscriptions'][OperationName]['data'] = Operations['subscriptions'][OperationName]['data'] + >( + options: UseSubscriptionOptions & ExtraOptions + ): CreateSubscriptionResult; +}; + +export type CreateSubscriptionResult = CreateQueryResult & { + isSubscribed: boolean; +}; + +export type UseMutationOptions = Omit< + TanstackCreateMutationOptions, + 'mutationKey' | 'mutationFn' +> & { + operationName: OperationName; +}; + +export type CreateMutation = { + < + OperationName extends Extract, + Input extends Operations['mutations'][OperationName]['input'] = Operations['mutations'][OperationName]['input'], + Data extends Operations['mutations'][OperationName]['data'] = Operations['mutations'][OperationName]['data'] + >( + options: UseMutationOptions & ExtraOptions + ): CreateMutationResult; +}; + +export interface UseUserOptions + extends FetchUserRequestOptions, + TanstackCreateQueryOptions { + enabled?: boolean; +} + +export type GetUser = { + (options?: UseUserOptions): CreateQueryResult; +}; + +export type UseUploadOptions = Omit< + TanstackCreateMutationOptions, + 'fetcher' +>; + +export type CreateFileUpload = { + (options?: UseUploadOptions): Omit< + TanstackCreateMutationOptions, + 'mutate' + > & { + upload: < + ProviderName extends Extract, + ProfileName extends Extract = Extract< + keyof Operations['s3Provider'][ProviderName]['profiles'], + string + >, + Meta extends Operations['s3Provider'][ProviderName]['profiles'][ProfileName] = Operations['s3Provider'][ProviderName]['profiles'][ProfileName] + >( + options: ProfileName extends string + ? UploadRequestOptionsWithProfile + : UploadRequestOptions, + config?: UseUploadOptions + ) => Promise; + + uploadAsync: < + ProviderName extends Extract, + ProfileName extends Extract = Extract< + keyof Operations['s3Provider'][ProviderName]['profiles'], + string + >, + Meta extends Operations['s3Provider'][ProviderName]['profiles'][ProfileName] = Operations['s3Provider'][ProviderName]['profiles'][ProfileName] + >( + options: Operations['s3Provider'][ProviderName]['hasProfiles'] extends true + ? UploadRequestOptionsWithProfile + : UploadRequestOptions, + config?: UseUploadOptions + ) => Promise; + }; +}; + +export interface SubscribeToOptions extends SubscriptionRequestOptions { + onResult(response: ClientResponse): void; + onSuccess?(response: ClientResponse): void; + onError?(error: ClientResponseError): void; + onAbort?(): void; +} + +export interface CreateSubscribeToProps extends SubscriptionRequestOptions { + queryHash: string; + enabled?: boolean; + resetOnMount?: boolean; + onSuccess?(response: ClientResponse): void; + onError?(error: ClientResponseError): void; +} From 48d454502ac0961559d869f23fd6c72f2e21932c Mon Sep 17 00:00:00 2001 From: DaniAkash Date: Sun, 19 Feb 2023 15:09:50 +0530 Subject: [PATCH 04/17] feat: added subscriptions --- packages/svelte-query/src/createQueryUtils.ts | 118 +++++++++++++----- packages/svelte-query/src/types.ts | 13 +- 2 files changed, 95 insertions(+), 36 deletions(-) diff --git a/packages/svelte-query/src/createQueryUtils.ts b/packages/svelte-query/src/createQueryUtils.ts index 052d3e100..6f9678c0f 100644 --- a/packages/svelte-query/src/createQueryUtils.ts +++ b/packages/svelte-query/src/createQueryUtils.ts @@ -3,9 +3,11 @@ import { createMutation as tanstackCreateMutation, useQueryClient, } from '@tanstack/svelte-query'; +import { readable } from 'svelte/store'; +import { onMount } from 'svelte'; import type { QueryFunctionContext } from '@tanstack/svelte-query'; import type { OperationsDefinition, LogoutOptions, Client } from '@wundergraph/sdk/client'; -// import { serialize } from '@wundergraph/sdk/internal'; +import { serialize } from '@wundergraph/sdk/internal'; import type { CreateFileUpload, CreateMutation, @@ -21,7 +23,7 @@ import type { export const userQueryKey = 'wg_user'; -export function createQueryUtils(client: Client) { +export default function createQueryUtils(client: Client) { const queryFetcher: QueryFetcher = async (query) => { const result = await client.query(query); @@ -58,7 +60,7 @@ export function createQueryUtils(client * * All queries support liveQuery by default, enabling this will set up a realtime subscription. * ```ts - * const { data, error, isLoading, isSubscribed } = useQuery({ + * const { data, error, isLoading, subscriptionState } = useQuery({ * operationName: 'Weather', * liveQuery: true, * }) @@ -67,8 +69,7 @@ export function createQueryUtils(client const createQuery: CreateQuery = (options) => { const { operationName, liveQuery, input, enabled, refetchOnWindowFocus, ...queryOptions } = options; - // TODO: will be needed for subscriptions - // const queryHash = serialize([operationName, input]); + const queryHash = serialize([operationName, input]); const result = tanstackCreateQuery({ queryKey: queryKey({ operationName, input }), @@ -78,22 +79,20 @@ export function createQueryUtils(client refetchOnWindowFocus: liveQuery ? false : refetchOnWindowFocus, }); - // TODO: Learn about how to build subscription utility in svelte - // const { isSubscribed } = useSubscribeTo({ - // queryHash, - // operationName, - // input, - // liveQuery, - // enabled: options.enabled !== false && liveQuery, - // onSuccess: options.onSuccess, - // onError: options.onError - // }); + const subscriptionState = createSubscribeTo({ + queryHash, + operationName, + input, + liveQuery, + enabled: options.enabled !== false && liveQuery, + onSuccess: options.onSuccess, + onError: options.onError, + }); if (liveQuery) { return { ...result, - // TODO: Subscription not ready yet - // isSubscribed + subscriptionState, }; } return result; @@ -207,10 +206,63 @@ export function createQueryUtils(client // Helper function used in createQuery and createSubscription const createSubscribeTo = (props: CreateSubscribeToProps) => { const client = useQueryClient(); - const { queryHash, operationName, input, enabled, liveQuery, subscribeOnce, resetOnMount, onSuccess, onError } = - props; + const { operationName, input, enabled, liveQuery, subscribeOnce, resetOnMount, onSuccess, onError } = props; + + let startedAtRef: number | null = null; + let unsubscribe: ReturnType; + + onMount(() => { + if (!startedAtRef && resetOnMount) { + client.removeQueries([operationName, input]); + } + }); - //TODO: Svelte style utility to setup subscriptions + const subscriptionState = readable( + { + isLoading: false, + isSubscribed: false, + }, + function start(set) { + set({ isLoading: true, isSubscribed: false }); + unsubscribe = subscribeTo({ + operationName, + input, + liveQuery, + subscribeOnce, + onError(error) { + set({ isLoading: false, isSubscribed: false }); + onError?.(error); + startedAtRef = null; + }, + onResult(result) { + if (!startedAtRef) { + set({ isLoading: false, isSubscribed: true }); + onSuccess?.(result); + startedAtRef = new Date().getTime(); + } + + // Promise is not handled because we are not interested in the result + // Errors are handled by React Query internally + client.setQueryData([operationName, input], () => { + if (result.error) { + throw result.error; + } + + return result.data; + }); + }, + onAbort() { + set({ isLoading: false, isSubscribed: false }); + startedAtRef = null; + }, + }); + return function stop() { + unsubscribe?.(); + }; + } + ); + + return subscriptionState; }; /** @@ -220,34 +272,32 @@ export function createQueryUtils(client * * @usage * ```ts - * const { data, error, isLoading, isSubscribed } = useSubscription({ + * const { data, error, isLoading, subscriptionState } = createSubscription({ * operationName: 'Countdown', * }) */ const createSubscription: CreateSubscription = (options) => { const { enabled = true, operationName, input, subscribeOnce, onSuccess, onError } = options; - // const queryHash = serialize([operationName, input]); + const queryHash = serialize([operationName, input]); const subscription = tanstackCreateQuery({ queryKey: [operationName, input], enabled: false, // we update the cache async }); - // TODO: Learn about how to build subscription utility in svelte - // const { isSubscribed } = useSubscribeTo({ - // queryHash, - // operationName, - // input, - // subscribeOnce, - // enabled, - // onSuccess, - // onError - // }); + const subscriptionState = createSubscribeTo({ + queryHash, + operationName, + input, + subscribeOnce, + enabled, + onSuccess, + onError, + }); return { ...subscription, - // TODO: set actual value - isSubscribed: false, + subscriptionState, }; }; diff --git a/packages/svelte-query/src/types.ts b/packages/svelte-query/src/types.ts index ec659ac73..9c8c30915 100644 --- a/packages/svelte-query/src/types.ts +++ b/packages/svelte-query/src/types.ts @@ -16,6 +16,7 @@ import type { CreateMutationOptions as TanstackCreateMutationOptions, CreateMutationResult, } from '@tanstack/svelte-query'; +import type { Readable } from 'svelte/store'; export type QueryFetcher = { < @@ -83,7 +84,12 @@ export type CreateQuery( options: CreateQueryOptions & ExtraOptions - ): CreateQueryResult & { isSubscribed?: boolean }; + ): CreateQueryResult & { + subscriptionState?: Readable<{ + isLoading: boolean; + isSubscribed: boolean; + }>; + }; }; export type UseSubscriptionOptions< @@ -115,7 +121,10 @@ export type CreateSubscription = CreateQueryResult & { - isSubscribed: boolean; + subscriptionState: Readable<{ + isLoading: boolean; + isSubscribed: boolean; + }>; }; export type UseMutationOptions = Omit< From 8de7983a0371a945ce77feac26b13cb86034976d Mon Sep 17 00:00:00 2001 From: DaniAkash Date: Sun, 19 Feb 2023 21:03:42 +0530 Subject: [PATCH 05/17] feat: added tsd for svelte-query --- packages/svelte-query/package.json | 6 +- packages/svelte-query/src/createQueryUtils.ts | 2 +- .../svelte-query/tests/svelte-query.test-d.ts | 103 ++++++++++ pnpm-lock.yaml | 180 ++++++++++-------- 4 files changed, 209 insertions(+), 82 deletions(-) create mode 100644 packages/svelte-query/tests/svelte-query.test-d.ts diff --git a/packages/svelte-query/package.json b/packages/svelte-query/package.json index 36b702bde..bcbb14aca 100644 --- a/packages/svelte-query/package.json +++ b/packages/svelte-query/package.json @@ -7,7 +7,7 @@ "types": "./dist/index.d.ts", "scripts": { "build": "tsc -p tsconfig.build.json", - "test": "jest && tsd" + "test": "tsd" }, "tsd": { "directory": "tests" @@ -22,7 +22,8 @@ }, "peerDependencies": { "@wundergraph/sdk": ">=0.110.0", - "@tanstack/svelte-query": "^4.24.6" + "@tanstack/svelte-query": "^4.24.6", + "svelte": "^3.54.0" }, "devDependencies": { "@swc/core": "^1.3.14", @@ -38,6 +39,7 @@ "node-fetch": "2.6.7", "@tanstack/svelte-query": "^4.24.6", "tsd": "^0.24.1", + "svelte": "^3.54.0", "typescript": "^4.8.2" }, "homepage": "https://wundergraph.com", diff --git a/packages/svelte-query/src/createQueryUtils.ts b/packages/svelte-query/src/createQueryUtils.ts index 6f9678c0f..bda110c3d 100644 --- a/packages/svelte-query/src/createQueryUtils.ts +++ b/packages/svelte-query/src/createQueryUtils.ts @@ -23,7 +23,7 @@ import type { export const userQueryKey = 'wg_user'; -export default function createQueryUtils(client: Client) { +export function createQueryUtils(client: Client) { const queryFetcher: QueryFetcher = async (query) => { const result = await client.query(query); diff --git a/packages/svelte-query/tests/svelte-query.test-d.ts b/packages/svelte-query/tests/svelte-query.test-d.ts new file mode 100644 index 000000000..7b62fe552 --- /dev/null +++ b/packages/svelte-query/tests/svelte-query.test-d.ts @@ -0,0 +1,103 @@ +import { createQueryUtils } from '../src'; +import { Client } from '@wundergraph/sdk/client'; +import type { ClientResponseError, OperationsDefinition, User } from '@wundergraph/sdk/client'; +import { expectType } from 'tsd'; +import { get } from 'svelte/store'; +import type { CreateQueryResult } from '@tanstack/svelte-query'; + +interface Operations extends OperationsDefinition { + queries: { + Weather: { + input: { + city: string; + }; + data: any; + requiresAuthentication: boolean; + }; + }; + subscriptions: { + Weather: { + input: { + forCity: string; + }; + data: any; + requiresAuthentication: boolean; + }; + }; + mutations: { + CreateUser: { + input: { + name: string; + }; + data: any; + requiresAuthentication: boolean; + }; + }; +} + +const { createSubscription, createQuery, createMutation, getUser, queryKey } = createQueryUtils( + new Client({ + baseURL: 'http://localhost:8080', + applicationHash: 'my-application-hash', + sdkVersion: '0.0.0', + }) +); + +const query = createQuery({ + enabled: true, + operationName: 'Weather', + input: { + city: 'Berlin', + }, +}); + +const { data: queryData, error: queryError } = get(query); + +expectType(queryData); +expectType(queryError); + +const subscription = createSubscription({ + enabled: true, + subscribeOnce: true, + operationName: 'Weather', + input: { + forCity: 'Berlin', + }, +}); + +const { data: subData, error: subError } = get(subscription); +expectType(subData); +expectType(subError); + +const mutation = createMutation({ + operationName: 'CreateUser', +}); + +const { data: mutData, error: mutError, mutate, mutateAsync } = get(mutation); + +expectType(mutData); +expectType(mutError); + +expectType( + mutate({ + name: 'John Doe', + }) +); + +expectType>( + mutateAsync({ + name: 'John Doe', + }) +); + +expectType, ClientResponseError>>(getUser()); +expectType, ClientResponseError>>( + getUser({ + revalidate: true, + abortSignal: new AbortController().signal, + }) +); + +expectType<('Weather' | { city: string } | undefined)[]>( + queryKey({ operationName: 'Weather', input: { city: 'Berlin' } }) +); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c089c4afb..2add27b6c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -449,6 +449,40 @@ importers: tsup: 6.5.0_typescript@4.9.4 typescript: 4.9.4 + packages/svelte-query: + specifiers: + '@swc/core': ^1.3.14 + '@swc/jest': ^0.2.23 + '@tanstack/svelte-query': ^4.24.6 + '@testing-library/jest-dom': ^5.16.5 + '@testing-library/svelte': ^3.2.2 + '@types/jest': ^28.1.1 + '@types/node-fetch': 2.6.2 + '@wundergraph/sdk': workspace:* + jest: ^29.0.3 + jest-environment-jsdom: ^29.3.0 + nock: ^13.2.9 + node-fetch: 2.6.7 + svelte: ^3.54.0 + tsd: ^0.24.1 + typescript: ^4.8.2 + devDependencies: + '@swc/core': 1.3.14 + '@swc/jest': 0.2.23_@swc+core@1.3.14 + '@tanstack/svelte-query': 4.24.9_svelte@3.55.1 + '@testing-library/jest-dom': 5.16.5 + '@testing-library/svelte': 3.2.2_svelte@3.55.1 + '@types/jest': 28.1.8 + '@types/node-fetch': 2.6.2 + '@wundergraph/sdk': link:../sdk + jest: 29.4.2 + jest-environment-jsdom: 29.3.0 + nock: 13.2.9 + node-fetch: 2.6.7 + svelte: 3.55.1 + tsd: 0.24.1 + typescript: 4.9.4 + packages/swr: specifiers: '@swc/core': ^1.3.14 @@ -3009,7 +3043,7 @@ packages: jest-validate: 29.2.2 jest-watcher: 29.2.2 micromatch: 4.0.5 - pretty-format: 29.3.1 + pretty-format: 29.4.2 slash: 3.0.0 strip-ansi: 6.0.1 transitivePeerDependencies: @@ -3230,7 +3264,7 @@ packages: resolution: {integrity: sha512-wlrznINZI5sMjwvUoLVk617ll/UYfGIZNxmbU+Pa7wmkL4vYzhV9R2pwVqUh4NWWuLQWkI8+8mOkxs//prKQ3g==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - jest-get-type: 29.2.0 + jest-get-type: 29.4.2 dev: true /@jest/expect-utils/29.4.2: @@ -3244,7 +3278,7 @@ packages: resolution: {integrity: sha512-QivM7GlSHSsIAWzgfyP8dgeExPRZ9BIe2LsdPyEhCGkZkoyA+kGsoIzbKAfZCvvRzfZioKwPtCZIt5SaoxYCvg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - expect: 29.3.1 + expect: 29.4.2 jest-snapshot: 29.3.1 transitivePeerDependencies: - supports-color @@ -5121,6 +5155,10 @@ packages: resolution: {integrity: sha512-Tfru6YTDTCpX7dKVwHp/sosw/dNjEdzrncduUjIkQxn7n7u+74HT2ZrGtwwrU6Orws4x7zp3FKRqBPWVVhpx9w==} dev: true + /@tanstack/query-core/4.24.9: + resolution: {integrity: sha512-pZQ2NpdaHzx8gPPkAPh06d6zRkjfonUzILSYBXrdHDapP2eaBbGsx5L4/dMF+fyAglFzQZdDDzZgAykbM20QVw==} + dev: true + /@tanstack/react-query/4.16.1_biqbaboplfbrettd7655fr4n2y: resolution: {integrity: sha512-PDE9u49wSDykPazlCoLFevUpceLjQ0Mm8i6038HgtTEKb/aoVnUZdlUP7C392ds3Cd75+EGlHU7qpEX06R7d9Q==} peerDependencies: @@ -5166,6 +5204,15 @@ packages: solid-js: 1.6.11 dev: true + /@tanstack/svelte-query/4.24.9_svelte@3.55.1: + resolution: {integrity: sha512-sKZgyrHbCVWxbGPNOwRG3py7b/si2lXCMP0jCLHIK6ncHrrxhZXhYDQgbIy75D44wKRDV7jjP2GJ7CIktWxNrA==} + peerDependencies: + svelte: ^3.54.0 + dependencies: + '@tanstack/query-core': 4.24.9 + svelte: 3.55.1 + dev: true + /@testing-library/dom/8.19.0: resolution: {integrity: sha512-6YWYPPpxG3e/xOo6HIWwB/58HukkwIVTOaZ0VwdMVjhRUX/01E4FtQbck9GazOOj7MXHc5RBzMrU86iBJHbI+A==} engines: {node: '>=12'} @@ -5223,6 +5270,16 @@ packages: react-dom: 18.2.0_react@18.2.0 dev: true + /@testing-library/svelte/3.2.2_svelte@3.55.1: + resolution: {integrity: sha512-IKwZgqbekC3LpoRhSwhd0JswRGxKdAGkf39UiDXTywK61YyLXbCYoR831e/UUC6EeNW4hiHPY+2WuovxOgI5sw==} + engines: {node: '>= 10'} + peerDependencies: + svelte: 3.x + dependencies: + '@testing-library/dom': 8.20.0 + svelte: 3.55.1 + dev: true + /@tokenizer/token/0.3.0: resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} dev: true @@ -6263,24 +6320,6 @@ packages: - supports-color dev: true - /babel-jest/29.4.2_@babel+core@7.19.6: - resolution: {integrity: sha512-vcghSqhtowXPG84posYkkkzcZsdayFkubUgbE3/1tuGbX7AQtwCkkNA/wIbB0BMjuCPoqTkiDyKN7Ty7d3uwNQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@babel/core': ^7.8.0 - dependencies: - '@babel/core': 7.19.6 - '@jest/transform': 29.4.2 - '@types/babel__core': 7.1.19 - babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.4.2_@babel+core@7.19.6 - chalk: 4.1.2 - graceful-fs: 4.2.10 - slash: 3.0.0 - transitivePeerDependencies: - - supports-color - dev: true - /babel-jest/29.4.2_@babel+core@7.20.12: resolution: {integrity: sha512-vcghSqhtowXPG84posYkkkzcZsdayFkubUgbE3/1tuGbX7AQtwCkkNA/wIbB0BMjuCPoqTkiDyKN7Ty7d3uwNQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -6432,17 +6471,6 @@ packages: babel-preset-current-node-syntax: 1.0.1_@babel+core@7.19.6 dev: true - /babel-preset-jest/29.4.2_@babel+core@7.19.6: - resolution: {integrity: sha512-ecWdaLY/8JyfUDr0oELBMpj3R5I1L6ZqG+kRJmwqfHtLWuPrJStR0LUkvUhfykJWTsXXMnohsayN/twltBbDrQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.19.6 - babel-plugin-jest-hoist: 29.4.2 - babel-preset-current-node-syntax: 1.0.1_@babel+core@7.19.6 - dev: true - /babel-preset-jest/29.4.2_@babel+core@7.20.12: resolution: {integrity: sha512-ecWdaLY/8JyfUDr0oELBMpj3R5I1L6ZqG+kRJmwqfHtLWuPrJStR0LUkvUhfykJWTsXXMnohsayN/twltBbDrQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -8644,17 +8672,6 @@ packages: jest-util: 29.4.2 dev: true - /expect/29.3.1: - resolution: {integrity: sha512-gGb1yTgU30Q0O/tQq+z30KBWv24ApkMgFUpvKBkyLUBL68Wv8dHdJxTBZFl/iT8K/bqDHvUYRH6IIN3rToopPA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@jest/expect-utils': 29.3.1 - jest-get-type: 29.2.0 - jest-matcher-utils: 29.3.1 - jest-message-util: 29.4.2 - jest-util: 29.4.2 - dev: true - /expect/29.4.2: resolution: {integrity: sha512-+JHYg9O3hd3RlICG90OPVjRkPBoiUH7PxvDVMnRiaq1g6JUgZStX514erMl0v2Dc5SkfVbm7ztqbd6qHHPn+mQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -10319,13 +10336,13 @@ packages: dedent: 0.7.0 is-generator-fn: 2.1.0 jest-each: 29.3.1 - jest-matcher-utils: 29.3.1 + jest-matcher-utils: 29.4.2 jest-message-util: 29.4.2 jest-runtime: 29.3.1 jest-snapshot: 29.4.2 jest-util: 29.4.2 p-limit: 3.1.0 - pretty-format: 29.3.1 + pretty-format: 29.4.2 slash: 3.0.0 stack-utils: 2.0.5 transitivePeerDependencies: @@ -10606,7 +10623,7 @@ packages: graceful-fs: 4.2.10 jest-circus: 29.3.1 jest-environment-node: 29.3.1 - jest-get-type: 29.2.0 + jest-get-type: 29.4.2 jest-regex-util: 29.2.0 jest-resolve: 29.3.1 jest-runner: 29.3.1 @@ -10614,7 +10631,7 @@ packages: jest-validate: 29.3.1 micromatch: 4.0.5 parse-json: 5.2.0 - pretty-format: 29.3.1 + pretty-format: 29.4.2 slash: 3.0.0 strip-json-comments: 3.1.1 transitivePeerDependencies: @@ -10645,7 +10662,7 @@ packages: graceful-fs: 4.2.10 jest-circus: 29.3.1 jest-environment-node: 29.3.1 - jest-get-type: 29.2.0 + jest-get-type: 29.4.2 jest-regex-util: 29.2.0 jest-resolve: 29.3.1 jest-runner: 29.3.1 @@ -10653,7 +10670,7 @@ packages: jest-validate: 29.3.1 micromatch: 4.0.5 parse-json: 5.2.0 - pretty-format: 29.3.1 + pretty-format: 29.4.2 slash: 3.0.0 strip-json-comments: 3.1.1 transitivePeerDependencies: @@ -10684,7 +10701,7 @@ packages: graceful-fs: 4.2.10 jest-circus: 29.3.1 jest-environment-node: 29.3.1 - jest-get-type: 29.2.0 + jest-get-type: 29.4.2 jest-regex-util: 29.2.0 jest-resolve: 29.3.1 jest-runner: 29.3.1 @@ -10692,7 +10709,7 @@ packages: jest-validate: 29.3.1 micromatch: 4.0.5 parse-json: 5.2.0 - pretty-format: 29.3.1 + pretty-format: 29.4.2 slash: 3.0.0 strip-json-comments: 3.1.1 transitivePeerDependencies: @@ -10730,7 +10747,7 @@ packages: jest-validate: 29.3.1 micromatch: 4.0.5 parse-json: 5.2.0 - pretty-format: 29.3.1 + pretty-format: 29.4.2 slash: 3.0.0 strip-json-comments: 3.1.1 transitivePeerDependencies: @@ -10769,7 +10786,7 @@ packages: jest-validate: 29.3.1 micromatch: 4.0.5 parse-json: 5.2.0 - pretty-format: 29.3.1 + pretty-format: 29.4.2 slash: 3.0.0 strip-json-comments: 3.1.1 transitivePeerDependencies: @@ -10808,7 +10825,7 @@ packages: jest-validate: 29.3.1 micromatch: 4.0.5 parse-json: 5.2.0 - pretty-format: 29.3.1 + pretty-format: 29.4.2 slash: 3.0.0 strip-json-comments: 3.1.1 transitivePeerDependencies: @@ -10847,7 +10864,7 @@ packages: jest-validate: 29.3.1 micromatch: 4.0.5 parse-json: 5.2.0 - pretty-format: 29.3.1 + pretty-format: 29.4.2 slash: 3.0.0 strip-json-comments: 3.1.1 transitivePeerDependencies: @@ -10886,7 +10903,7 @@ packages: jest-validate: 29.3.1 micromatch: 4.0.5 parse-json: 5.2.0 - pretty-format: 29.3.1 + pretty-format: 29.4.2 slash: 3.0.0 strip-json-comments: 3.1.1 ts-node: 10.9.1_awa2wsr5thmg3i7jqycphctjfq @@ -10944,11 +10961,11 @@ packages: ts-node: optional: true dependencies: - '@babel/core': 7.19.6 + '@babel/core': 7.20.12 '@jest/test-sequencer': 29.4.2 '@jest/types': 29.4.2 '@types/node': 14.18.33 - babel-jest: 29.4.2_@babel+core@7.19.6 + babel-jest: 29.4.2_@babel+core@7.20.12 chalk: 4.1.2 ci-info: 3.5.0 deepmerge: 4.2.2 @@ -11022,11 +11039,11 @@ packages: ts-node: optional: true dependencies: - '@babel/core': 7.19.6 + '@babel/core': 7.20.12 '@jest/test-sequencer': 29.4.2 '@jest/types': 29.4.2 '@types/node': 18.11.18 - babel-jest: 29.4.2_@babel+core@7.19.6 + babel-jest: 29.4.2_@babel+core@7.20.12 chalk: 4.1.2 ci-info: 3.5.0 deepmerge: 4.2.2 @@ -11066,8 +11083,8 @@ packages: dependencies: chalk: 4.1.2 diff-sequences: 29.3.1 - jest-get-type: 29.2.0 - pretty-format: 29.3.1 + jest-get-type: 29.4.2 + pretty-format: 29.4.2 dev: true /jest-diff/29.4.2: @@ -11100,9 +11117,9 @@ packages: dependencies: '@jest/types': 29.4.2 chalk: 4.1.2 - jest-get-type: 29.2.0 + jest-get-type: 29.4.2 jest-util: 29.4.2 - pretty-format: 29.3.1 + pretty-format: 29.4.2 dev: true /jest-each/29.4.2: @@ -11239,8 +11256,8 @@ packages: resolution: {integrity: sha512-3DA/VVXj4zFOPagGkuqHnSQf1GZBmmlagpguxEERO6Pla2g84Q1MaVIB3YMxgUaFIaYag8ZnTyQgiZ35YEqAQA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - jest-get-type: 29.2.0 - pretty-format: 29.3.1 + jest-get-type: 29.4.2 + pretty-format: 29.4.2 dev: true /jest-leak-detector/29.4.2: @@ -11268,7 +11285,7 @@ packages: chalk: 4.1.2 jest-diff: 29.3.1 jest-get-type: 29.2.0 - pretty-format: 29.3.1 + pretty-format: 29.4.2 dev: true /jest-matcher-utils/29.3.1: @@ -11277,8 +11294,8 @@ packages: dependencies: chalk: 4.1.2 jest-diff: 29.3.1 - jest-get-type: 29.2.0 - pretty-format: 29.3.1 + jest-get-type: 29.4.2 + pretty-format: 29.4.2 dev: true /jest-matcher-utils/29.4.2: @@ -11316,7 +11333,7 @@ packages: chalk: 4.1.2 graceful-fs: 4.2.10 micromatch: 4.0.5 - pretty-format: 29.3.1 + pretty-format: 29.4.2 slash: 3.0.0 stack-utils: 2.0.5 dev: true @@ -11331,7 +11348,7 @@ packages: chalk: 4.1.2 graceful-fs: 4.2.10 micromatch: 4.0.5 - pretty-format: 29.3.1 + pretty-format: 29.4.2 slash: 3.0.0 stack-utils: 2.0.5 dev: true @@ -11678,7 +11695,7 @@ packages: '@babel/plugin-syntax-typescript': 7.20.0_@babel+core@7.19.6 '@babel/traverse': 7.20.1 '@babel/types': 7.20.7 - '@jest/expect-utils': 29.3.1 + '@jest/expect-utils': 29.4.2 '@jest/transform': 29.3.1 '@jest/types': 29.4.2 '@types/babel__traverse': 7.18.2 @@ -11688,13 +11705,13 @@ packages: expect: 29.4.2 graceful-fs: 4.2.10 jest-diff: 29.3.1 - jest-get-type: 29.2.0 + jest-get-type: 29.4.2 jest-haste-map: 29.3.1 - jest-matcher-utils: 29.3.1 + jest-matcher-utils: 29.4.2 jest-message-util: 29.4.2 jest-util: 29.4.2 natural-compare: 1.4.0 - pretty-format: 29.3.1 + pretty-format: 29.4.2 semver: 7.3.8 transitivePeerDependencies: - supports-color @@ -11726,7 +11743,7 @@ packages: jest-message-util: 29.3.1 jest-util: 29.4.2 natural-compare: 1.4.0 - pretty-format: 29.3.1 + pretty-format: 29.4.2 semver: 7.3.8 transitivePeerDependencies: - supports-color @@ -11807,9 +11824,9 @@ packages: '@jest/types': 29.4.2 camelcase: 6.3.0 chalk: 4.1.2 - jest-get-type: 29.2.0 + jest-get-type: 29.4.2 leven: 3.1.0 - pretty-format: 29.3.1 + pretty-format: 29.4.2 dev: true /jest-validate/29.3.1: @@ -11821,7 +11838,7 @@ packages: chalk: 4.1.2 jest-get-type: 29.2.0 leven: 3.1.0 - pretty-format: 29.3.1 + pretty-format: 29.4.2 dev: true /jest-validate/29.4.2: @@ -16150,6 +16167,11 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + /svelte/3.55.1: + resolution: {integrity: sha512-S+87/P0Ve67HxKkEV23iCdAh/SX1xiSfjF1HOglno/YTbSTW7RniICMCofWGdJJbdjw3S+0PfFb1JtGfTXE0oQ==} + engines: {node: '>= 8'} + dev: true + /swagger2openapi/7.0.8: resolution: {integrity: sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g==} hasBin: true From 68bd57e8c229596ad6dae375e039332bf4f45249 Mon Sep 17 00:00:00 2001 From: DaniAkash Date: Mon, 20 Feb 2023 00:11:35 +0530 Subject: [PATCH 06/17] test: queries and mutations --- packages/svelte-query/jest.config.js | 7 +- packages/svelte-query/package.json | 9 +- .../FetchDisabledComponent.svelte | 10 + .../FetchDisabledWrapper.svelte | 9 + .../MutationWithAuthComponent.svelte | 12 + .../MutationWithAuthWrapper.svelte | 9 + .../MutationWithInvalidationComponent.svelte | 15 + .../MutationWithInvalidationWrapper.svelte | 9 + .../TestComponents/WeatherComponent.svelte | 7 + .../TestComponents/WeatherWrapper.svelte | 9 + .../svelte-query/tests/queryUtils.test.ts | 288 ++++++++++++++++++ pnpm-lock.yaml | 13 + 12 files changed, 392 insertions(+), 5 deletions(-) create mode 100644 packages/svelte-query/tests/TestComponents/FetchDisabledComponent.svelte create mode 100644 packages/svelte-query/tests/TestComponents/FetchDisabledWrapper.svelte create mode 100644 packages/svelte-query/tests/TestComponents/MutationWithAuthComponent.svelte create mode 100644 packages/svelte-query/tests/TestComponents/MutationWithAuthWrapper.svelte create mode 100644 packages/svelte-query/tests/TestComponents/MutationWithInvalidationComponent.svelte create mode 100644 packages/svelte-query/tests/TestComponents/MutationWithInvalidationWrapper.svelte create mode 100644 packages/svelte-query/tests/TestComponents/WeatherComponent.svelte create mode 100644 packages/svelte-query/tests/TestComponents/WeatherWrapper.svelte create mode 100644 packages/svelte-query/tests/queryUtils.test.ts diff --git a/packages/svelte-query/jest.config.js b/packages/svelte-query/jest.config.js index 558e4b2c8..08e1c813f 100644 --- a/packages/svelte-query/jest.config.js +++ b/packages/svelte-query/jest.config.js @@ -1,10 +1,15 @@ module.exports = { testEnvironment: 'jsdom', - testRegex: '/tests/.*\\.test\\.tsx?$', + testRegex: '/tests/.*\\.test\\.ts?$', setupFilesAfterEnv: ['/jest-setup.ts'], transform: { '^.+\\.(t|j)sx?$': '@swc/jest', + '^.+\\.svelte$': 'svelte-jester', }, + transformIgnorePatterns: [ + '/node_modules/.pnpm/@tanstack+svelte-query@4.24.9_svelte@3.55.1/node_modules/@tanstack/svelte-query/', + ], + moduleFileExtensions: ['js', 'ts', 'svelte'], coveragePathIgnorePatterns: ['/node_modules/', '/dist/'], coverageReporters: ['text', 'html'], reporters: ['default', 'github-actions'], diff --git a/packages/svelte-query/package.json b/packages/svelte-query/package.json index bcbb14aca..a839f7be9 100644 --- a/packages/svelte-query/package.json +++ b/packages/svelte-query/package.json @@ -7,7 +7,7 @@ "types": "./dist/index.d.ts", "scripts": { "build": "tsc -p tsconfig.build.json", - "test": "tsd" + "test": "jest && tsd" }, "tsd": { "directory": "tests" @@ -21,13 +21,14 @@ "url": "git+https://github.com/wundergraph/wundergraph.git" }, "peerDependencies": { - "@wundergraph/sdk": ">=0.110.0", "@tanstack/svelte-query": "^4.24.6", + "@wundergraph/sdk": ">=0.110.0", "svelte": "^3.54.0" }, "devDependencies": { "@swc/core": "^1.3.14", "@swc/jest": "^0.2.23", + "@tanstack/svelte-query": "^4.24.6", "@testing-library/jest-dom": "^5.16.5", "@testing-library/svelte": "^3.2.2", "@types/jest": "^28.1.1", @@ -37,9 +38,9 @@ "jest-environment-jsdom": "^29.3.0", "nock": "^13.2.9", "node-fetch": "2.6.7", - "@tanstack/svelte-query": "^4.24.6", - "tsd": "^0.24.1", "svelte": "^3.54.0", + "svelte-jester": "^2.3.2", + "tsd": "^0.24.1", "typescript": "^4.8.2" }, "homepage": "https://wundergraph.com", diff --git a/packages/svelte-query/tests/TestComponents/FetchDisabledComponent.svelte b/packages/svelte-query/tests/TestComponents/FetchDisabledComponent.svelte new file mode 100644 index 000000000..94672005c --- /dev/null +++ b/packages/svelte-query/tests/TestComponents/FetchDisabledComponent.svelte @@ -0,0 +1,10 @@ + + +
+
Fetched: {$query.isFetched ? 'true' : 'false'}
+
+ \ No newline at end of file diff --git a/packages/svelte-query/tests/TestComponents/FetchDisabledWrapper.svelte b/packages/svelte-query/tests/TestComponents/FetchDisabledWrapper.svelte new file mode 100644 index 000000000..74e828102 --- /dev/null +++ b/packages/svelte-query/tests/TestComponents/FetchDisabledWrapper.svelte @@ -0,0 +1,9 @@ + + + + + diff --git a/packages/svelte-query/tests/TestComponents/MutationWithAuthComponent.svelte b/packages/svelte-query/tests/TestComponents/MutationWithAuthComponent.svelte new file mode 100644 index 000000000..18401d199 --- /dev/null +++ b/packages/svelte-query/tests/TestComponents/MutationWithAuthComponent.svelte @@ -0,0 +1,12 @@ + + +
{$mutation.data?.id}
diff --git a/packages/svelte-query/tests/TestComponents/MutationWithAuthWrapper.svelte b/packages/svelte-query/tests/TestComponents/MutationWithAuthWrapper.svelte new file mode 100644 index 000000000..165e84540 --- /dev/null +++ b/packages/svelte-query/tests/TestComponents/MutationWithAuthWrapper.svelte @@ -0,0 +1,9 @@ + + + + + diff --git a/packages/svelte-query/tests/TestComponents/MutationWithInvalidationComponent.svelte b/packages/svelte-query/tests/TestComponents/MutationWithInvalidationComponent.svelte new file mode 100644 index 000000000..5b49fe061 --- /dev/null +++ b/packages/svelte-query/tests/TestComponents/MutationWithInvalidationComponent.svelte @@ -0,0 +1,15 @@ + + +
+
{$query.data?.name}
+ +
diff --git a/packages/svelte-query/tests/TestComponents/MutationWithInvalidationWrapper.svelte b/packages/svelte-query/tests/TestComponents/MutationWithInvalidationWrapper.svelte new file mode 100644 index 000000000..a4223cd8a --- /dev/null +++ b/packages/svelte-query/tests/TestComponents/MutationWithInvalidationWrapper.svelte @@ -0,0 +1,9 @@ + + + + + diff --git a/packages/svelte-query/tests/TestComponents/WeatherComponent.svelte b/packages/svelte-query/tests/TestComponents/WeatherComponent.svelte new file mode 100644 index 000000000..a616ffd4c --- /dev/null +++ b/packages/svelte-query/tests/TestComponents/WeatherComponent.svelte @@ -0,0 +1,7 @@ + + +
Response: {$query.data?.id}
diff --git a/packages/svelte-query/tests/TestComponents/WeatherWrapper.svelte b/packages/svelte-query/tests/TestComponents/WeatherWrapper.svelte new file mode 100644 index 000000000..1b3a50a84 --- /dev/null +++ b/packages/svelte-query/tests/TestComponents/WeatherWrapper.svelte @@ -0,0 +1,9 @@ + + + + + diff --git a/packages/svelte-query/tests/queryUtils.test.ts b/packages/svelte-query/tests/queryUtils.test.ts new file mode 100644 index 000000000..24b633cfa --- /dev/null +++ b/packages/svelte-query/tests/queryUtils.test.ts @@ -0,0 +1,288 @@ +import { QueryCache, QueryClient, QueryClientProvider, useQueryClient } from '@tanstack/svelte-query'; +import { Client, ClientConfig, OperationsDefinition } from '@wundergraph/sdk/client'; +import nock from 'nock'; +import fetch from 'node-fetch'; +import { render, fireEvent, screen, waitFor, act } from '@testing-library/svelte'; +import Weather from './TestComponents/WeatherWrapper.svelte'; +import { createQueryUtils } from '../src'; +import FetchDisabledWrapper from './TestComponents/FetchDisabledWrapper.svelte'; +import MutationWithAuthWrapper from './TestComponents/MutationWithAuthWrapper.svelte'; +import MutationWithInvalidationWrapper from './TestComponents/MutationWithInvalidationWrapper.svelte'; + +export type Queries = { + Weather: { + data: any; + requiresAuthentication: false; + liveQuery: boolean; + }; +}; + +export type Mutations = { + SetNameWithoutAuth: { + input: { name: string }; + data: { id: string }; + requiresAuthentication: false; + }; + SetName: { + input: { name: string }; + data: { id: string }; + requiresAuthentication: true; + }; +}; + +export type Subscriptions = { + Countdown: { + input: { from: number }; + data: { count: number }; + requiresAuthentication: false; + }; +}; + +export interface Operations extends OperationsDefinition {} + +export function sleep(time: number) { + return new Promise((resolve) => setTimeout(resolve, time)); +} + +const createClient = (overrides?: Partial) => { + return new Client({ + sdkVersion: '1.0.0', + baseURL: 'https://api.com', + applicationHash: '123', + customFetch: fetch as any, + operationMetadata: { + Weather: { + requiresAuthentication: false, + }, + SetName: { + requiresAuthentication: true, + }, + SetNameWithoutAuth: { + requiresAuthentication: false, + }, + Countdown: { + requiresAuthentication: false, + }, + }, + ...overrides, + }); +}; + +const nockQuery = (operationName = 'Weather', wgParams = {}) => { + return nock('https://api.com') + .matchHeader('accept', 'application/json') + .matchHeader('content-type', 'application/json') + .matchHeader('WG-SDK-Version', '1.0.0') + .get('/operations/' + operationName) + .query({ wg_api_hash: '123', wg_variables: '{}', ...wgParams }); +}; + +const nockMutation = (operationName = 'SetName', wgParams = {}, authenticated = false) => { + const csrfScope = nock('https://api.com') + .matchHeader('accept', 'text/plain') + .matchHeader('WG-SDK-Version', '1.0.0') + .get('/auth/cookie/csrf') + .reply(200, 'csrf'); + const mutation = nock('https://api.com') + .matchHeader('accept', 'application/json') + .matchHeader('content-type', 'application/json') + .matchHeader('WG-SDK-Version', '1.0.0') + .post('/operations/' + operationName, wgParams) + .query({ wg_api_hash: '123' }); + + if (authenticated) { + mutation.matchHeader('x-csrf-token', 'csrf'); + } + + return { + csrfScope, + mutation, + }; +}; + +describe('Svelte Query - createQueryUtils', () => { + const client = createClient(); + + const queryCache = new QueryCache(); + const queryClient = new QueryClient({ queryCache }); + + beforeEach(() => { + queryClient.clear(); + nock.cleanAll(); + }); + + const utils = createQueryUtils(client); + + it('should be able to init utility functions', async () => { + expect(utils).toBeTruthy(); + }); + + it('should return data', async () => { + const scope = nockQuery() + .once() + .reply(200, { + data: { + id: '1', + }, + }); + + const queryCreator = () => + utils.createQuery({ + operationName: 'Weather', + }); + + render(Weather, { queryClient, queryCreator }); + await waitFor(() => { + screen.getByText('Response: 1'); + }); + scope.done(); + }); + + it('should be disabled', async () => { + const scope = nockQuery().reply(200, { + data: { + id: '2', + }, + }); + + const queryCreator = () => + utils.createQuery({ + operationName: 'Weather', + input: { + forCity: 'berlin', + }, + enabled: false, + }); + + render(FetchDisabledWrapper, { queryClient, queryCreator }); + + screen.getByText('Fetched: false'); + + await act(() => sleep(150)); + + screen.getByText('Fetched: false'); + + expect(() => scope.done()).toThrow(); + }); +}); + +describe('Svelte Query - createMutation', () => { + const client = createClient(); + + const queryCache = new QueryCache(); + const queryClient = new QueryClient({ queryCache }); + + beforeEach(() => { + queryClient.clear(); + nock.cleanAll(); + }); + + const { createMutation, createQuery, queryKey } = createQueryUtils(client); + + it('should trigger mutation with auth', async () => { + const { mutation, csrfScope } = nockMutation('SetName', { name: 'Rick Astley' }, true); + + const scope = mutation.once().reply(200, { + data: { + id: 'Never gonna give you up', + }, + }); + + const mutationCreator = () => + createMutation({ + operationName: 'SetName', + }); + + render(MutationWithAuthWrapper, { + queryClient, + mutationCreator, + }); + + await waitFor(() => { + screen.getByText('Never gonna give you up'); + }); + + csrfScope.done(); + scope.done(); + }); + + it('should trigger mutation', async () => { + const { mutation, csrfScope } = nockMutation('SetNameWithoutAuth', { name: 'Rick Astley' }); + + const scope = mutation.once().reply(200, { + data: { + id: '1', + }, + }); + + const mutationCreator = () => + createMutation({ + operationName: 'SetNameWithoutAuth', + }); + + render(MutationWithAuthWrapper, { + queryClient, + mutationCreator, + }); + + await waitFor(() => { + screen.getByText('1'); + }); + + expect(() => csrfScope.done()).toThrow(); // should not be called + + scope.done(); + }); + + it('should invalidate query', async () => { + const scope = nockQuery() + .reply(200, { + data: { + id: '1', + name: 'Test', + }, + }) + .matchHeader('accept', 'application/json') + .matchHeader('content-type', 'application/json') + .matchHeader('WG-SDK-Version', '1.0.0') + .post('/operations/SetNameWithoutAuth', { name: 'Rick Astley' }) + .query({ wg_api_hash: '123' }) + .reply(200, { data: { id: '1', name: 'Rick Astley' } }) + .matchHeader('accept', 'application/json') + .matchHeader('content-type', 'application/json') + .matchHeader('WG-SDK-Version', '1.0.0') + .get('/operations/Weather') + .query({ wg_api_hash: '123', wg_variables: '{}' }) + .reply(200, { data: { id: '1', name: 'Rick Astley' } }); + + const queryCache = new QueryCache(); + const queryClient = new QueryClient({ queryCache }); + + const queryCreator = () => + createQuery({ + operationName: 'Weather', + }); + + const mutationCreator = () => + createMutation({ + operationName: 'SetNameWithoutAuth', + onSuccess: () => { + queryClient.invalidateQueries(queryKey({ operationName: 'Weather' })); + }, + }); + + render(MutationWithInvalidationWrapper, { queryClient, queryCreator, mutationCreator }); + + await waitFor(() => { + screen.getByText('Test'); + }); + + fireEvent.click(screen.getByText('submit')); + + await waitFor(() => { + screen.getByText('Rick Astley'); + }); + + scope.done(); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2add27b6c..e01bdb4ec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -464,6 +464,7 @@ importers: nock: ^13.2.9 node-fetch: 2.6.7 svelte: ^3.54.0 + svelte-jester: ^2.3.2 tsd: ^0.24.1 typescript: ^4.8.2 devDependencies: @@ -480,6 +481,7 @@ importers: nock: 13.2.9 node-fetch: 2.6.7 svelte: 3.55.1 + svelte-jester: 2.3.2_jest@29.4.2+svelte@3.55.1 tsd: 0.24.1 typescript: 4.9.4 @@ -16167,6 +16169,17 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + /svelte-jester/2.3.2_jest@29.4.2+svelte@3.55.1: + resolution: {integrity: sha512-JtxSz4FWAaCRBXbPsh4LcDs4Ua7zdXgLC0TZvT1R56hRV0dymmNP+abw67DTPF7sQPyNxWsOKd0Sl7Q8SnP8kg==} + engines: {node: '>=14'} + peerDependencies: + jest: '>= 27' + svelte: '>= 3' + dependencies: + jest: 29.4.2 + svelte: 3.55.1 + dev: true + /svelte/3.55.1: resolution: {integrity: sha512-S+87/P0Ve67HxKkEV23iCdAh/SX1xiSfjF1HOglno/YTbSTW7RniICMCofWGdJJbdjw3S+0PfFb1JtGfTXE0oQ==} engines: {node: '>= 8'} From 626bd2d36a30febbe31a538ebd080750cef53659 Mon Sep 17 00:00:00 2001 From: DaniAkash Date: Mon, 20 Feb 2023 00:27:30 +0530 Subject: [PATCH 07/17] test: subscriptions & user --- .../SubscriptionComponent.svelte | 9 ++ .../TestComponents/SubscriptionWrapper.svelte | 9 ++ .../tests/TestComponents/UserComponent.svelte | 8 ++ .../tests/TestComponents/UserWrapper.svelte | 9 ++ .../svelte-query/tests/queryUtils.test.ts | 86 +++++++++++++++++++ 5 files changed, 121 insertions(+) create mode 100644 packages/svelte-query/tests/TestComponents/SubscriptionComponent.svelte create mode 100644 packages/svelte-query/tests/TestComponents/SubscriptionWrapper.svelte create mode 100644 packages/svelte-query/tests/TestComponents/UserComponent.svelte create mode 100644 packages/svelte-query/tests/TestComponents/UserWrapper.svelte diff --git a/packages/svelte-query/tests/TestComponents/SubscriptionComponent.svelte b/packages/svelte-query/tests/TestComponents/SubscriptionComponent.svelte new file mode 100644 index 000000000..dc5175828 --- /dev/null +++ b/packages/svelte-query/tests/TestComponents/SubscriptionComponent.svelte @@ -0,0 +1,9 @@ + + +
+ {$subscription.data?.count ? $subscription.data.count : 'loading'} +
diff --git a/packages/svelte-query/tests/TestComponents/SubscriptionWrapper.svelte b/packages/svelte-query/tests/TestComponents/SubscriptionWrapper.svelte new file mode 100644 index 000000000..53ee35a13 --- /dev/null +++ b/packages/svelte-query/tests/TestComponents/SubscriptionWrapper.svelte @@ -0,0 +1,9 @@ + + + + + diff --git a/packages/svelte-query/tests/TestComponents/UserComponent.svelte b/packages/svelte-query/tests/TestComponents/UserComponent.svelte new file mode 100644 index 000000000..1e3e5258b --- /dev/null +++ b/packages/svelte-query/tests/TestComponents/UserComponent.svelte @@ -0,0 +1,8 @@ + + +
{$query.data?.email}
diff --git a/packages/svelte-query/tests/TestComponents/UserWrapper.svelte b/packages/svelte-query/tests/TestComponents/UserWrapper.svelte new file mode 100644 index 000000000..e73da2840 --- /dev/null +++ b/packages/svelte-query/tests/TestComponents/UserWrapper.svelte @@ -0,0 +1,9 @@ + + + + + diff --git a/packages/svelte-query/tests/queryUtils.test.ts b/packages/svelte-query/tests/queryUtils.test.ts index 24b633cfa..966675e29 100644 --- a/packages/svelte-query/tests/queryUtils.test.ts +++ b/packages/svelte-query/tests/queryUtils.test.ts @@ -8,6 +8,8 @@ import { createQueryUtils } from '../src'; import FetchDisabledWrapper from './TestComponents/FetchDisabledWrapper.svelte'; import MutationWithAuthWrapper from './TestComponents/MutationWithAuthWrapper.svelte'; import MutationWithInvalidationWrapper from './TestComponents/MutationWithInvalidationWrapper.svelte'; +import SubscriptionWrapper from './TestComponents/SubscriptionWrapper.svelte'; +import UserWrapper from './TestComponents/UserWrapper.svelte'; export type Queries = { Weather: { @@ -286,3 +288,87 @@ describe('Svelte Query - createMutation', () => { scope.done(); }); }); + +describe('Svelte Query - createSubscription', () => { + const client = createClient(); + + const queryCache = new QueryCache(); + const queryClient = new QueryClient({ queryCache }); + + beforeEach(() => { + queryCache.clear(); + }); + + afterAll(() => { + queryCache.clear(); + }); + + const { createSubscription } = createQueryUtils(client); + + it('should subscribe', async () => { + // web streams not supported in node-fetch, we use subscribeOnce to test + const scope = nock('https://api.com') + .matchHeader('WG-SDK-Version', '1.0.0') + .matchHeader('accept', 'application/json') + .matchHeader('content-type', 'application/json') + .get('/operations/Countdown') + .query({ wg_api_hash: '123', wg_variables: JSON.stringify({ from: 100 }), wg_subscribe_once: 'true' }) + .reply(200, { data: { count: 100 } }); + + const subscriptionCreator = () => + createSubscription({ + operationName: 'Countdown', + subscribeOnce: true, + input: { + from: 100, + }, + }); + + render(SubscriptionWrapper, { queryClient, subscriptionCreator }); + + screen.getByText('loading'); + + await waitFor( + () => { + screen.getByText('100'); + }, + { + timeout: 10000, + } + ); + + scope.done(); + }); +}); + +describe('Svelte Query - getUser', () => { + const client = createClient(); + + const queryCache = new QueryCache(); + const queryClient = new QueryClient({ queryCache }); + + beforeEach(() => { + queryCache.clear(); + }); + + const { getUser } = createQueryUtils(client); + + it('should return user', async () => { + const scope = nock('https://api.com') + .matchHeader('accept', 'application/json') + .matchHeader('content-type', 'application/json') + .matchHeader('WG-SDK-Version', '1.0.0') + .get('/auth/user') + .reply(200, { email: 'info@wundergraph.com' }); + + const userGetter = () => getUser(); + + render(UserWrapper, { queryClient, userGetter }); + + await waitFor(() => { + screen.getByText('info@wundergraph.com'); + }); + + scope.done(); + }); +}); From 7229306423b0bec4c657dfa432be2f50ba08ec52 Mon Sep 17 00:00:00 2001 From: DaniAkash Date: Wed, 22 Feb 2023 17:06:02 +0530 Subject: [PATCH 08/17] fix: subscription tests --- packages/svelte-query/src/createQueryUtils.ts | 87 +++++++++---------- packages/svelte-query/src/types.ts | 6 +- 2 files changed, 45 insertions(+), 48 deletions(-) diff --git a/packages/svelte-query/src/createQueryUtils.ts b/packages/svelte-query/src/createQueryUtils.ts index bda110c3d..7afb9a5e3 100644 --- a/packages/svelte-query/src/createQueryUtils.ts +++ b/packages/svelte-query/src/createQueryUtils.ts @@ -3,8 +3,8 @@ import { createMutation as tanstackCreateMutation, useQueryClient, } from '@tanstack/svelte-query'; -import { readable } from 'svelte/store'; -import { onMount } from 'svelte'; +import { writable } from 'svelte/store'; +import { onDestroy, onMount } from 'svelte'; import type { QueryFunctionContext } from '@tanstack/svelte-query'; import type { OperationsDefinition, LogoutOptions, Client } from '@wundergraph/sdk/client'; import { serialize } from '@wundergraph/sdk/internal'; @@ -210,57 +210,54 @@ export function createQueryUtils(client let startedAtRef: number | null = null; let unsubscribe: ReturnType; + const subscriptionState = writable({ + isLoading: false, + isSubscribed: false, + }); onMount(() => { if (!startedAtRef && resetOnMount) { client.removeQueries([operationName, input]); } - }); - const subscriptionState = readable( - { - isLoading: false, - isSubscribed: false, - }, - function start(set) { - set({ isLoading: true, isSubscribed: false }); - unsubscribe = subscribeTo({ - operationName, - input, - liveQuery, - subscribeOnce, - onError(error) { - set({ isLoading: false, isSubscribed: false }); - onError?.(error); - startedAtRef = null; - }, - onResult(result) { - if (!startedAtRef) { - set({ isLoading: false, isSubscribed: true }); - onSuccess?.(result); - startedAtRef = new Date().getTime(); + subscriptionState.set({ isLoading: true, isSubscribed: false }); + unsubscribe = subscribeTo({ + operationName, + input, + liveQuery, + subscribeOnce, + onError(error) { + subscriptionState.set({ isLoading: false, isSubscribed: false }); + onError?.(error); + startedAtRef = null; + }, + onResult(result) { + if (!startedAtRef) { + subscriptionState.set({ isLoading: false, isSubscribed: true }); + onSuccess?.(result); + startedAtRef = new Date().getTime(); + } + + // Promise is not handled because we are not interested in the result + // Errors are handled by React Query internally + client.setQueryData([operationName, input], () => { + if (result.error) { + throw result.error; } - // Promise is not handled because we are not interested in the result - // Errors are handled by React Query internally - client.setQueryData([operationName, input], () => { - if (result.error) { - throw result.error; - } - - return result.data; - }); - }, - onAbort() { - set({ isLoading: false, isSubscribed: false }); - startedAtRef = null; - }, - }); - return function stop() { - unsubscribe?.(); - }; - } - ); + return result.data; + }); + }, + onAbort() { + subscriptionState.set({ isLoading: false, isSubscribed: false }); + startedAtRef = null; + }, + }); + }); + + onDestroy(() => { + unsubscribe?.(); + }); return subscriptionState; }; diff --git a/packages/svelte-query/src/types.ts b/packages/svelte-query/src/types.ts index 9c8c30915..078c4a855 100644 --- a/packages/svelte-query/src/types.ts +++ b/packages/svelte-query/src/types.ts @@ -16,7 +16,7 @@ import type { CreateMutationOptions as TanstackCreateMutationOptions, CreateMutationResult, } from '@tanstack/svelte-query'; -import type { Readable } from 'svelte/store'; +import type { Writable } from 'svelte/store'; export type QueryFetcher = { < @@ -85,7 +85,7 @@ export type CreateQuery( options: CreateQueryOptions & ExtraOptions ): CreateQueryResult & { - subscriptionState?: Readable<{ + subscriptionState?: Writable<{ isLoading: boolean; isSubscribed: boolean; }>; @@ -121,7 +121,7 @@ export type CreateSubscription = CreateQueryResult & { - subscriptionState: Readable<{ + subscriptionState: Writable<{ isLoading: boolean; isSubscribed: boolean; }>; From f0edb863d22d11468b127e82fe1a6558dae4ef03 Mon Sep 17 00:00:00 2001 From: DaniAkash Date: Tue, 28 Feb 2023 11:53:45 +0530 Subject: [PATCH 09/17] refactor: createQueryUtils to createSvelteClient --- packages/svelte-query/README.md | 6 +++--- .../{createQueryUtils.ts => createSvelteClient.ts} | 2 +- packages/svelte-query/src/index.ts | 2 +- packages/svelte-query/tests/queryUtils.test.ts | 12 ++++++------ packages/svelte-query/tests/svelte-query.test-d.ts | 4 ++-- 5 files changed, 13 insertions(+), 13 deletions(-) rename packages/svelte-query/src/{createQueryUtils.ts => createSvelteClient.ts} (98%) diff --git a/packages/svelte-query/README.md b/packages/svelte-query/README.md index ed893d747..63bf2cc80 100644 --- a/packages/svelte-query/README.md +++ b/packages/svelte-query/README.md @@ -34,7 +34,7 @@ Second, run `wunderctl generate` to generate the code. Now you can use the utility functions. ```ts -import { createQueryUtils } from '@wundergraph/svelte-query'; +import { createSvelteClient } from '@wundergraph/svelte-query'; import { createClient } from '../generated/client'; import type { Operations } from '../generated/client'; @@ -42,7 +42,7 @@ const client = createClient(); // Typesafe WunderGraph client // These utility functions needs to be imported into your app export const { createQuery, createFileUpload, createMutation, createSubscription, getAuth, getUser, queryKey } = - createQueryUtils(client); + createSvelteClient(client); ``` Now, in your svelte layout setup Svelte Query Provider such that it is always wrapping above the rest of the app. @@ -74,7 +74,7 @@ Now you can use svelte-query to call your wundergraph operations! ```svelte diff --git a/packages/svelte-query/tests/queryUtils.test.ts b/packages/svelte-query/tests/queryUtils.test.ts index 0c6c008ca..9470aee29 100644 --- a/packages/svelte-query/tests/queryUtils.test.ts +++ b/packages/svelte-query/tests/queryUtils.test.ts @@ -76,7 +76,7 @@ const nockQuery = (operationName = 'Weather', wgParams = {}) => { .matchHeader('content-type', 'application/json') .matchHeader('WG-SDK-Version', '1.0.0') .get('/operations/' + operationName) - .query({ wg_api_hash: '123', wg_variables: '{}', ...wgParams }); + .query({ wg_api_hash: '123', ...wgParams }); }; const nockMutation = (operationName = 'SetName', wgParams = {}, authenticated = false) => { @@ -247,14 +247,14 @@ describe('Svelte Query - createMutation', () => { .matchHeader('accept', 'application/json') .matchHeader('content-type', 'application/json') .matchHeader('WG-SDK-Version', '1.0.0') - .post('/operations/SetNameWithoutAuth', { name: 'Rick Astley' }) + .post('/operations/SetNameWithoutAuth', { name: 'Not Rick Astley' }) .query({ wg_api_hash: '123' }) - .reply(200, { data: { id: '1', name: 'Rick Astley' } }) + .reply(200, { data: { id: '1', name: 'Not Rick Astley' } }) .matchHeader('accept', 'application/json') .matchHeader('content-type', 'application/json') .matchHeader('WG-SDK-Version', '1.0.0') .get('/operations/Weather') - .query({ wg_api_hash: '123', wg_variables: '{}' }) + .query({ wg_api_hash: '123' }) .reply(200, { data: { id: '1', name: 'Rick Astley' } }); const queryCache = new QueryCache(); @@ -312,7 +312,12 @@ describe('Svelte Query - createSubscription', () => { .matchHeader('accept', 'application/json') .matchHeader('content-type', 'application/json') .get('/operations/Countdown') - .query({ wg_api_hash: '123', wg_variables: JSON.stringify({ from: 100 }), wg_subscribe_once: 'true' }) + .query( + (obj) => + obj.wg_api_hash === '123' && + obj.wg_variables === JSON.stringify({ from: 100 }) && + obj.wg_subscribe_once === '' + ) .reply(200, { data: { count: 100 } }); const subscriptionCreator = () => @@ -359,6 +364,7 @@ describe('Svelte Query - getUser', () => { .matchHeader('content-type', 'application/json') .matchHeader('WG-SDK-Version', '1.0.0') .get('/auth/user') + .query({ wg_api_hash: '123' }) .reply(200, { email: 'info@wundergraph.com' }); const userGetter = () => getUser(); From c91071341974c4dd7de963b8ed45662fb52ffd0e Mon Sep 17 00:00:00 2001 From: Pagebakers Date: Thu, 30 Mar 2023 16:58:02 +0200 Subject: [PATCH 17/17] feat: fix types and tests --- packages/svelte-query/src/lib/types.ts | 48 +++++++++---------- .../svelte-query/tests/queryUtils.test.ts | 44 ++++++++++++----- .../svelte-query/tests/svelte-query.test-d.ts | 26 +++++----- 3 files changed, 66 insertions(+), 52 deletions(-) diff --git a/packages/svelte-query/src/lib/types.ts b/packages/svelte-query/src/lib/types.ts index 9a552d499..e9f2e1666 100644 --- a/packages/svelte-query/src/lib/types.ts +++ b/packages/svelte-query/src/lib/types.ts @@ -5,9 +5,8 @@ import type { FetchUserRequestOptions, OperationsDefinition, WithInput, - ClientResponseError, UploadRequestOptions, - UploadRequestOptionsWithProfile, + ResponseError, } from '@wundergraph/sdk/client'; import type { @@ -22,7 +21,7 @@ import type { Writable, Readable } from 'svelte/store'; export type QueryFetcher = { < OperationName extends Extract, - Data extends Operations['queries'][OperationName]['data'] = Operations['queries'][OperationName]['data'], + Data extends Operations['queries'][OperationName]['response'] = Operations['queries'][OperationName]['response'], RequestOptions extends OperationRequestOptions< Extract, Operations['queries'][OperationName]['input'] @@ -38,7 +37,7 @@ export type QueryFetcher = { export type MutationFetcher = { < OperationName extends Extract, - Data extends Operations['mutations'][OperationName]['data'] = Operations['mutations'][OperationName]['data'], + Data extends Operations['mutations'][OperationName]['response'] = Operations['mutations'][OperationName]['response'], RequestOptions extends OperationRequestOptions< Extract, Operations['mutations'][OperationName]['input'] @@ -81,11 +80,11 @@ export type CreateQuery, Input extends Operations['queries'][OperationName]['input'] = Operations['queries'][OperationName]['input'], - Data extends Operations['queries'][OperationName]['data'] = Operations['queries'][OperationName]['data'], + Response extends Operations['queries'][OperationName]['response'] = Operations['queries'][OperationName]['response'], LiveQuery extends Operations['queries'][OperationName]['liveQuery'] = Operations['queries'][OperationName]['liveQuery'] >( - options: CreateQueryOptions & ExtraOptions - ): CreateQueryResult & { + options: CreateQueryOptions & ExtraOptions + ): CreateQueryResult & { subscriptionState?: Writable<{ isLoading: boolean; isSubscribed: boolean; @@ -115,13 +114,14 @@ export type CreateSubscription, Input extends Operations['subscriptions'][OperationName]['input'] = Operations['subscriptions'][OperationName]['input'], - Data extends Operations['subscriptions'][OperationName]['data'] = Operations['subscriptions'][OperationName]['data'] + Response extends Operations['subscriptions'][OperationName]['response'] = Operations['subscriptions'][OperationName]['response'] >( - options: UseSubscriptionOptions & ExtraOptions - ): CreateSubscriptionResult; + options: UseSubscriptionOptions & + ExtraOptions + ): CreateSubscriptionResult; }; -export type CreateSubscriptionResult = Readable< +export type CreateSubscriptionResult = Readable< QueryObserverResult & { isSubscribed: boolean; } @@ -138,30 +138,30 @@ export type CreateMutation, Input extends Operations['mutations'][OperationName]['input'] = Operations['mutations'][OperationName]['input'], - Data extends Operations['mutations'][OperationName]['data'] = Operations['mutations'][OperationName]['data'] + Response extends Operations['mutations'][OperationName]['response'] = Operations['mutations'][OperationName]['response'] >( - options: UseMutationOptions & ExtraOptions - ): CreateMutationResult; + options: UseMutationOptions & ExtraOptions + ): CreateMutationResult; }; export interface UseUserOptions extends FetchUserRequestOptions, - TanstackCreateQueryOptions { + TanstackCreateQueryOptions { enabled?: boolean; } export type GetUser = { - (options?: UseUserOptions): CreateQueryResult; + (options?: UseUserOptions): CreateQueryResult; }; export type UseUploadOptions = Omit< - TanstackCreateMutationOptions, + TanstackCreateMutationOptions, 'fetcher' >; export type CreateFileUpload = { (options?: UseUploadOptions): Omit< - TanstackCreateMutationOptions, + TanstackCreateMutationOptions, 'mutate' > & { upload: < @@ -172,9 +172,7 @@ export type CreateFileUpload = { >, Meta extends Operations['s3Provider'][ProviderName]['profiles'][ProfileName] = Operations['s3Provider'][ProviderName]['profiles'][ProfileName] >( - options: ProfileName extends string - ? UploadRequestOptionsWithProfile - : UploadRequestOptions, + options: UploadRequestOptions, config?: UseUploadOptions ) => Promise; @@ -186,9 +184,7 @@ export type CreateFileUpload = { >, Meta extends Operations['s3Provider'][ProviderName]['profiles'][ProfileName] = Operations['s3Provider'][ProviderName]['profiles'][ProfileName] >( - options: Operations['s3Provider'][ProviderName]['hasProfiles'] extends true - ? UploadRequestOptionsWithProfile - : UploadRequestOptions, + options: UploadRequestOptions, config?: UseUploadOptions ) => Promise; }; @@ -197,7 +193,7 @@ export type CreateFileUpload = { export interface SubscribeToOptions extends SubscriptionRequestOptions { onResult(response: ClientResponse): void; onSuccess?(response: ClientResponse): void; - onError?(error: ClientResponseError): void; + onError?(error: ResponseError): void; onAbort?(): void; } @@ -206,5 +202,5 @@ export interface CreateSubscribeToProps extends SubscriptionRequestOptions { enabled?: boolean; resetOnMount?: boolean; onSuccess?(response: ClientResponse): void; - onError?(error: ClientResponseError): void; + onError?(error: ResponseError): void; } diff --git a/packages/svelte-query/tests/queryUtils.test.ts b/packages/svelte-query/tests/queryUtils.test.ts index 9470aee29..a57faaf76 100644 --- a/packages/svelte-query/tests/queryUtils.test.ts +++ b/packages/svelte-query/tests/queryUtils.test.ts @@ -1,5 +1,5 @@ import { QueryCache, QueryClient, QueryClientProvider, useQueryClient } from '@tanstack/svelte-query'; -import { Client, ClientConfig, OperationsDefinition } from '@wundergraph/sdk/client'; +import { Client, ClientConfig, GraphQLError, OperationsDefinition } from '@wundergraph/sdk/client'; import nock from 'nock'; import fetch from 'node-fetch'; import { render, fireEvent, screen, waitFor, act } from '@testing-library/svelte'; @@ -13,7 +13,7 @@ import UserWrapper from './TestComponents/UserWrapper.svelte'; export type Queries = { Weather: { - data: any; + response: { data: any }; requiresAuthentication: false; liveQuery: boolean; }; @@ -22,12 +22,12 @@ export type Queries = { export type Mutations = { SetNameWithoutAuth: { input: { name: string }; - data: { id: string }; + response: { data: { id: string }; error: GraphQLError }; requiresAuthentication: false; }; SetName: { input: { name: string }; - data: { id: string }; + response: { data: { id: string }; error: GraphQLError }; requiresAuthentication: true; }; }; @@ -35,7 +35,7 @@ export type Mutations = { export type Subscriptions = { Countdown: { input: { from: number }; - data: { count: number }; + response: { data: { count: number } }; requiresAuthentication: false; }; }; @@ -237,25 +237,33 @@ describe('Svelte Query - createMutation', () => { }); it('should invalidate query', async () => { + const mutation = nockMutation('SetNameWithoutAuth', { name: 'Not Rick Astley' }) + .mutation.once() + .reply(200, { + data: { + name: 'Rick Astley', + }, + }); + const scope = nockQuery() .reply(200, { data: { id: '1', - name: 'Test', + name: 'Rick Astley', }, }) .matchHeader('accept', 'application/json') .matchHeader('content-type', 'application/json') .matchHeader('WG-SDK-Version', '1.0.0') - .post('/operations/SetNameWithoutAuth', { name: 'Not Rick Astley' }) + .get('/operations/Weather') .query({ wg_api_hash: '123' }) - .reply(200, { data: { id: '1', name: 'Not Rick Astley' } }) + .reply(200, { data: { id: '1', name: 'Not Ricky Astley' } }) .matchHeader('accept', 'application/json') .matchHeader('content-type', 'application/json') .matchHeader('WG-SDK-Version', '1.0.0') .get('/operations/Weather') .query({ wg_api_hash: '123' }) - .reply(200, { data: { id: '1', name: 'Rick Astley' } }); + .reply(200, { data: { id: '1', name: 'Not Rick Astley' } }); const queryCache = new QueryCache(); const queryClient = new QueryClient({ queryCache }); @@ -276,15 +284,25 @@ describe('Svelte Query - createMutation', () => { render(MutationWithInvalidationWrapper, { queryClient, queryCreator, mutationCreator }); await waitFor(() => { - screen.getByText('Test'); + screen.getByText('Rick Astley'); }); + await sleep(1000); + fireEvent.click(screen.getByText('submit')); - await waitFor(() => { - screen.getByText('Rick Astley'); - }); + await sleep(1000); + + await waitFor( + () => { + screen.getByText('Not Rick Astley'); + }, + { + timeout: 1000, + } + ); + mutation.done(); scope.done(); }); }); diff --git a/packages/svelte-query/tests/svelte-query.test-d.ts b/packages/svelte-query/tests/svelte-query.test-d.ts index 9dad53778..9d557c62a 100644 --- a/packages/svelte-query/tests/svelte-query.test-d.ts +++ b/packages/svelte-query/tests/svelte-query.test-d.ts @@ -1,6 +1,6 @@ import { createSvelteClient } from '../src/lib'; import { Client } from '@wundergraph/sdk/client'; -import type { ClientResponseError, OperationsDefinition, User } from '@wundergraph/sdk/client'; +import type { ResponseError, OperationsDefinition, User } from '@wundergraph/sdk/client'; import { expectType } from 'tsd'; import { get } from 'svelte/store'; import type { CreateQueryResult } from '@tanstack/svelte-query'; @@ -11,7 +11,7 @@ interface Operations extends OperationsDefinition { input: { city: string; }; - data: any; + response: { data?: { id: 1 }; error?: ResponseError }; requiresAuthentication: boolean; }; }; @@ -20,7 +20,7 @@ interface Operations extends OperationsDefinition { input: { forCity: string; }; - data: any; + response: { data?: { id: 1 }; error?: ResponseError }; requiresAuthentication: boolean; }; }; @@ -29,7 +29,7 @@ interface Operations extends OperationsDefinition { input: { name: string; }; - data: any; + response: { data?: { id: 1 }; error?: ResponseError }; requiresAuthentication: boolean; }; }; @@ -53,8 +53,8 @@ const query = createQuery({ const { data: queryData, error: queryError } = get(query); -expectType(queryData); -expectType(queryError); +expectType(queryData); +expectType(queryError); const subscription = createSubscription({ enabled: true, @@ -66,8 +66,8 @@ const subscription = createSubscription({ }); const { data: subData, error: subError } = get(subscription); -expectType(subData); -expectType(subError); +expectType(subData); +expectType(subError); const mutation = createMutation({ operationName: 'CreateUser', @@ -75,8 +75,8 @@ const mutation = createMutation({ const { data: mutData, error: mutError, mutate, mutateAsync } = get(mutation); -expectType(mutData); -expectType(mutError); +expectType(mutData); +expectType(mutError); expectType( mutate({ @@ -84,14 +84,14 @@ expectType( }) ); -expectType>( +expectType>( mutateAsync({ name: 'John Doe', }) ); -expectType, ClientResponseError>>(getUser()); -expectType, ClientResponseError>>( +expectType, ResponseError>>(getUser()); +expectType, ResponseError>>( getUser({ revalidate: true, abortSignal: new AbortController().signal,