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/README.md b/packages/svelte-query/README.md new file mode 100644 index 000000000..63bf2cc80 --- /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 { createSvelteClient } 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 } = + createSvelteClient(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. 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..08e1c813f --- /dev/null +++ b/packages/svelte-query/jest.config.js @@ -0,0 +1,16 @@ +module.exports = { + testEnvironment: 'jsdom', + 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 new file mode 100644 index 000000000..07a6af98a --- /dev/null +++ b/packages/svelte-query/package.json @@ -0,0 +1,66 @@ +{ + "name": "@wundergraph/svelte-query", + "version": "0.0.1", + "license": "Apache-2.0", + "description": "WunderGraph Svelte Query Integration", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "scripts": { + "build": "svelte-package", + "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": { + "@tanstack/svelte-query": "^4.24.6", + "@wundergraph/sdk": ">=0.110.0", + "svelte": "^3.54.0" + }, + "devDependencies": { + "@sveltejs/package": "^2.0.2", + "@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", + "svelte-jester": "^2.3.2", + "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/src/lib/createSvelteClient.ts b/packages/svelte-query/src/lib/createSvelteClient.ts new file mode 100644 index 000000000..d108d913e --- /dev/null +++ b/packages/svelte-query/src/lib/createSvelteClient.ts @@ -0,0 +1,331 @@ +import { + createQuery as tanstackCreateQuery, + createMutation as tanstackCreateMutation, + useQueryClient, +} from '@tanstack/svelte-query'; +import { writable, derived } from 'svelte/store'; +import type { Readable, 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'; +import type { + CreateFileUpload, + CreateMutation, + CreateQuery, + CreateSubscribeToProps, + CreateSubscription, + GetUser, + MutationFetcher, + QueryFetcher, + QueryKey, + SubscribeToOptions, +} from './types'; + +export const userQueryKey = 'wg_user'; + +const withSubscriptionState = ( + query: Readable, + subscriptionState: Writable<{ + isLoading: boolean; + isSubscribed: boolean; + }> +): Readable => { + const queryWithSubscription = derived< + [ + Readable, + Writable<{ + isLoading: boolean; + isSubscribed: boolean; + }> + ], + Q & { isSubscribed: boolean } + >([query, subscriptionState], ($values, set) => { + const newObject = Object.assign({}, $values[0], { + isSubscribed: $values[1].isSubscribed, + }); + set(newObject); + }); + + return queryWithSubscription; +}; + +export function createSvelteClient(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; + + const queryHash = serialize([operationName, input]); + + const queryResult = tanstackCreateQuery({ + queryKey: queryKey({ operationName, input }), + queryFn: ({ signal }: QueryFunctionContext) => queryFetcher({ operationName, input, abortSignal: signal }), + ...queryOptions, + enabled: liveQuery ? false : enabled, + refetchOnWindowFocus: liveQuery ? false : refetchOnWindowFocus, + }); + + const subscriptionState = createSubscribeTo({ + queryHash, + operationName, + input, + liveQuery, + enabled: options.enabled !== false && liveQuery, + onSuccess: options.onSuccess, + onError: options.onError, + }); + + if (liveQuery) { + return withSubscriptionState(queryResult, subscriptionState); + } + return queryResult; + }; + + /** + * 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 { data, 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 { operationName, input, enabled, liveQuery, subscribeOnce, resetOnMount, onSuccess, onError } = props; + + let startedAtRef: number | null = null; + let unsubscribe: ReturnType; + const subscriptionState = writable({ + isLoading: false, + isSubscribed: false, + }); + + onMount(() => { + if (!startedAtRef && resetOnMount) { + client.removeQueries([operationName, input]); + } + + 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; + } + + return result.data; + }); + }, + onAbort() { + subscriptionState.set({ isLoading: false, isSubscribed: false }); + startedAtRef = null; + }, + }); + }); + + onDestroy(() => { + unsubscribe?.(); + }); + + return subscriptionState; + }; + + /** + * createSubscription + * + * Subscribe to subscription operations. + * + * @usage + * ```ts + * const { data, error, isLoading, isSubscribed } = createSubscription({ + * 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 + }); + + const subscriptionState = createSubscribeTo({ + queryHash, + operationName, + input, + subscribeOnce, + enabled, + onSuccess, + onError, + }); + + return withSubscriptionState(subscription, subscriptionState); + }; + + return { + createQuery, + createMutation, + getAuth, + getUser, + createFileUpload, + queryKey, + createSubscription, + }; +} diff --git a/packages/svelte-query/src/lib/index.ts b/packages/svelte-query/src/lib/index.ts new file mode 100644 index 000000000..cfe3b14a6 --- /dev/null +++ b/packages/svelte-query/src/lib/index.ts @@ -0,0 +1,3 @@ +export { createSvelteClient, userQueryKey } from './createSvelteClient'; + +export * from './types'; diff --git a/packages/svelte-query/src/lib/types.ts b/packages/svelte-query/src/lib/types.ts new file mode 100644 index 000000000..e9f2e1666 --- /dev/null +++ b/packages/svelte-query/src/lib/types.ts @@ -0,0 +1,206 @@ +import type { + ClientResponse, + SubscriptionRequestOptions, + OperationRequestOptions, + FetchUserRequestOptions, + OperationsDefinition, + WithInput, + UploadRequestOptions, + ResponseError, +} from '@wundergraph/sdk/client'; + +import type { + CreateQueryOptions as TanstackCreateQueryOptions, + CreateQueryResult, + CreateMutationOptions as TanstackCreateMutationOptions, + CreateMutationResult, + QueryObserverResult, +} from '@tanstack/svelte-query'; +import type { Writable, Readable } from 'svelte/store'; + +export type QueryFetcher = { + < + OperationName extends Extract, + Data extends Operations['queries'][OperationName]['response'] = Operations['queries'][OperationName]['response'], + 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]['response'] = Operations['mutations'][OperationName]['response'], + 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'], + Response extends Operations['queries'][OperationName]['response'] = Operations['queries'][OperationName]['response'], + LiveQuery extends Operations['queries'][OperationName]['liveQuery'] = Operations['queries'][OperationName]['liveQuery'] + >( + options: CreateQueryOptions & ExtraOptions + ): CreateQueryResult & { + subscriptionState?: Writable<{ + isLoading: boolean; + 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'], + Response extends Operations['subscriptions'][OperationName]['response'] = Operations['subscriptions'][OperationName]['response'] + >( + options: UseSubscriptionOptions & + ExtraOptions + ): CreateSubscriptionResult; +}; + +export type CreateSubscriptionResult = Readable< + QueryObserverResult & { + 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'], + Response extends Operations['mutations'][OperationName]['response'] = Operations['mutations'][OperationName]['response'] + >( + 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: 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: UploadRequestOptions, + config?: UseUploadOptions + ) => Promise; + }; +}; + +export interface SubscribeToOptions extends SubscriptionRequestOptions { + onResult(response: ClientResponse): void; + onSuccess?(response: ClientResponse): void; + onError?(error: ResponseError): void; + onAbort?(): void; +} + +export interface CreateSubscribeToProps extends SubscriptionRequestOptions { + queryHash: string; + enabled?: boolean; + resetOnMount?: boolean; + onSuccess?(response: ClientResponse): void; + onError?(error: ResponseError): void; +} 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..9cf2069b2 --- /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/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/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..a57faaf76 --- /dev/null +++ b/packages/svelte-query/tests/queryUtils.test.ts @@ -0,0 +1,398 @@ +import { QueryCache, QueryClient, QueryClientProvider, useQueryClient } from '@tanstack/svelte-query'; +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'; +import Weather from './TestComponents/WeatherWrapper.svelte'; +import { createSvelteClient } from '../src/lib'; +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: { + response: { data: any }; + requiresAuthentication: false; + liveQuery: boolean; + }; +}; + +export type Mutations = { + SetNameWithoutAuth: { + input: { name: string }; + response: { data: { id: string }; error: GraphQLError }; + requiresAuthentication: false; + }; + SetName: { + input: { name: string }; + response: { data: { id: string }; error: GraphQLError }; + requiresAuthentication: true; + }; +}; + +export type Subscriptions = { + Countdown: { + input: { from: number }; + response: { 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', ...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 - createSvelteClient', () => { + const client = createClient(); + + const queryCache = new QueryCache(); + const queryClient = new QueryClient({ queryCache }); + + beforeEach(() => { + queryClient.clear(); + nock.cleanAll(); + }); + + const utils = createSvelteClient(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 } = createSvelteClient(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 mutation = nockMutation('SetNameWithoutAuth', { name: 'Not Rick Astley' }) + .mutation.once() + .reply(200, { + data: { + name: 'Rick Astley', + }, + }); + + const scope = nockQuery() + .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' }) + .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: 'Not 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('Rick Astley'); + }); + + await sleep(1000); + + fireEvent.click(screen.getByText('submit')); + + await sleep(1000); + + await waitFor( + () => { + screen.getByText('Not Rick Astley'); + }, + { + timeout: 1000, + } + ); + + mutation.done(); + 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 } = createSvelteClient(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( + (obj) => + obj.wg_api_hash === '123' && + obj.wg_variables === JSON.stringify({ from: 100 }) && + obj.wg_subscribe_once === '' + ) + .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 } = createSvelteClient(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') + .query({ wg_api_hash: '123' }) + .reply(200, { email: 'info@wundergraph.com' }); + + const userGetter = () => getUser(); + + render(UserWrapper, { queryClient, userGetter }); + + await waitFor(() => { + screen.getByText('info@wundergraph.com'); + }); + + scope.done(); + }); +}); 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..9d557c62a --- /dev/null +++ b/packages/svelte-query/tests/svelte-query.test-d.ts @@ -0,0 +1,103 @@ +import { createSvelteClient } from '../src/lib'; +import { Client } 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'; + +interface Operations extends OperationsDefinition { + queries: { + Weather: { + input: { + city: string; + }; + response: { data?: { id: 1 }; error?: ResponseError }; + requiresAuthentication: boolean; + }; + }; + subscriptions: { + Weather: { + input: { + forCity: string; + }; + response: { data?: { id: 1 }; error?: ResponseError }; + requiresAuthentication: boolean; + }; + }; + mutations: { + CreateUser: { + input: { + name: string; + }; + response: { data?: { id: 1 }; error?: ResponseError }; + requiresAuthentication: boolean; + }; + }; +} + +const { createSubscription, createQuery, createMutation, getUser, queryKey } = createSvelteClient( + 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, ResponseError>>(getUser()); +expectType, ResponseError>>( + getUser({ + revalidate: true, + abortSignal: new AbortController().signal, + }) +); + +expectType<('Weather' | { city: string } | undefined)[]>( + queryKey({ operationName: 'Weather', input: { city: 'Berlin' } }) +); 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..0a532ad0c --- /dev/null +++ b/packages/svelte-query/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "node", + "declarationDir": "./dist", + "outDir": "./dist", + "typeRoots": ["./node_modules/@types"] + }, + "include": ["src/**/*", "tests/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9dcbac709..2d23c2ded 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -455,6 +455,44 @@ importers: tsup: 6.5.0_typescript@4.9.4 typescript: 4.9.4 + packages/svelte-query: + specifiers: + '@sveltejs/package': ^2.0.2 + '@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 + svelte-jester: ^2.3.2 + tsd: ^0.24.1 + typescript: ^4.8.2 + devDependencies: + '@sveltejs/package': 2.0.2_atrrhq7vg4ekua4nnyrpuardle + '@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 + svelte-jester: 2.3.2_jest@29.4.2+svelte@3.55.1 + tsd: 0.24.1 + typescript: 4.9.4 + packages/swr: specifiers: '@swc/core': ^1.3.14 @@ -3039,7 +3077,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: @@ -3319,7 +3357,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 @@ -5171,6 +5209,22 @@ packages: solid-js: 1.6.11 dev: true + /@sveltejs/package/2.0.2_atrrhq7vg4ekua4nnyrpuardle: + resolution: {integrity: sha512-cCOCcO8yMHnhHyaR51nQtvKZ3o/vSU9UYI1EXLT1j2CKNPMuH1/g6JNwKcNNrtQGwwquudc69ZeYy8D/TDNwEw==} + engines: {node: ^16.14 || >=18} + hasBin: true + peerDependencies: + svelte: ^3.44.0 + dependencies: + chokidar: 3.5.3 + kleur: 4.1.5 + sade: 1.8.1 + svelte: 3.55.1 + svelte2tsx: 0.6.2_atrrhq7vg4ekua4nnyrpuardle + transitivePeerDependencies: + - typescript + dev: true + /@swc/core-darwin-arm64/1.3.14: resolution: {integrity: sha512-QFuUq3341uOCrJMIWGuo+CmRC5qZoM2lUo7o2lmv1FO1Dh9njTG85pLD83vz6y4j/F034DBGzvRgSti/Bsoccw==} engines: {node: '>=10'} @@ -5339,6 +5393,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: @@ -5384,6 +5442,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'} @@ -5441,6 +5508,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 @@ -6443,7 +6520,7 @@ packages: /axios/0.26.1_debug@4.3.4: resolution: {integrity: sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==} dependencies: - follow-redirects: 1.15.2 + follow-redirects: 1.15.2_debug@4.3.4 transitivePeerDependencies: - debug dev: false @@ -7755,6 +7832,10 @@ packages: dependencies: mimic-response: 3.1.0 + /dedent-js/1.0.1: + resolution: {integrity: sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ==} + dev: true + /dedent/0.7.0: resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} dev: true @@ -8854,17 +8935,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.5.0 - jest-get-type: 29.4.3 - jest-matcher-utils: 29.5.0 - jest-message-util: 29.5.0 - jest-util: 29.5.0 - dev: true - /expect/29.4.2: resolution: {integrity: sha512-+JHYg9O3hd3RlICG90OPVjRkPBoiUH7PxvDVMnRiaq1g6JUgZStX514erMl0v2Dc5SkfVbm7ztqbd6qHHPn+mQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -9156,6 +9226,18 @@ packages: debug: optional: true + /follow-redirects/1.15.2_debug@4.3.4: + resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dependencies: + debug: 4.3.4 + dev: false + /for-each/0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} dependencies: @@ -12944,6 +13026,11 @@ packages: engines: {node: '>=6'} dev: true + /kleur/4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + dev: true + /language-subtag-registry/0.3.22: resolution: {integrity: sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==} dev: true @@ -13255,6 +13342,12 @@ packages: get-func-name: 2.0.0 dev: true + /lower-case/2.0.2: + resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + dependencies: + tslib: 2.4.1 + dev: true + /lowercase-keys/2.0.0: resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} engines: {node: '>=8'} @@ -13652,6 +13745,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /mri/1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + dev: true + /mrmime/1.0.1: resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==} engines: {node: '>=10'} @@ -13863,6 +13961,13 @@ packages: resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} dev: true + /no-case/3.0.4: + resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + dependencies: + lower-case: 2.0.2 + tslib: 2.4.1 + dev: true + /nock/13.2.9: resolution: {integrity: sha512-1+XfJNYF1cjGB+TKMWi29eZ0b82QOvQs2YoLNzbpWGqFMtRQHTa57osqdGj4FrFPgkO4D4AZinzUJR9VvW3QUA==} engines: {node: '>= 10.13'} @@ -14766,6 +14871,13 @@ packages: engines: {node: '>= 0.8'} dev: false + /pascal-case/3.1.2: + resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} + dependencies: + no-case: 3.0.4 + tslib: 2.4.1 + dev: true + /path-equal/1.2.5: resolution: {integrity: sha512-i73IctDr3F2W+bsOWDyyVm/lqsXO47aY9nsFZUjTT/aljSbkxHxxCoyZ9UUrM8jK0JVod+An+rl48RCsvWM+9g==} dev: false @@ -16107,6 +16219,13 @@ packages: dependencies: tslib: 2.4.1 + /sade/1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + dependencies: + mri: 1.2.0 + dev: true + /safe-buffer/5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} dev: true @@ -16780,6 +16899,34 @@ 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'} + dev: true + + /svelte2tsx/0.6.2_atrrhq7vg4ekua4nnyrpuardle: + resolution: {integrity: sha512-0ircYY2/jMOfistf+iq8fVHERnu1i90nku56c78+btC8svyafsc3OjOV37LDEOV7buqYY1Rv/uy03eMxhopH2Q==} + peerDependencies: + svelte: ^3.55 + typescript: ^4.9.4 + dependencies: + dedent-js: 1.0.1 + pascal-case: 3.1.2 + svelte: 3.55.1 + typescript: 4.9.4 + dev: true + /swagger2openapi/7.0.8: resolution: {integrity: sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g==} hasBin: true