diff --git a/docs-website/config/navigation.js b/docs-website/config/navigation.js index 2faee2988..f2bb0aa94 100644 --- a/docs-website/config/navigation.js +++ b/docs-website/config/navigation.js @@ -195,6 +195,10 @@ const navigation = [ title: 'NextJS', href: '/docs/examples/nextjs', }, + { + title: 'NextJS + React Query', + href: '/docs/examples/nextjs-react-query', + }, { title: 'Vite + SWR', href: '/docs/examples/vite-swr', @@ -455,7 +459,7 @@ const navigation = [ href: '/docs/supported-frontend-frameworks', }, { - title: 'React-JS', + title: 'React', href: '/docs/supported-frontend-frameworks/react-js', }, { @@ -463,7 +467,7 @@ const navigation = [ href: '/docs/supported-frontend-frameworks/react-native', }, { - title: 'NextJS', + title: 'Next.js', href: '/docs/supported-frontend-frameworks/nextjs', }, { @@ -929,6 +933,27 @@ const navigation = [ }, ], }, + { + title: 'Clients reference', + links: [ + { + title: 'TypeScript Client', + href: '/docs/clients-reference/typescript-client', + }, + { + title: 'SWR', + href: '/docs/clients-reference/swr', + }, + { + title: 'React Query', + href: '/docs/clients-reference/react-query', + }, + { + title: 'Next.js', + href: '/docs/clients-reference/nextjs', + }, + ], + }, { title: 'Frequently Asked Questions', links: [ diff --git a/docs-website/next.config.js b/docs-website/next.config.js index e9be61a9e..be53d6985 100644 --- a/docs-website/next.config.js +++ b/docs-website/next.config.js @@ -17,6 +17,11 @@ const nextConfig = { destination: '/', permanent: true, }, + { + source: '/docs/examples/nextjs-swr', + destination: '/docs/examples/nextjs', + permanent: true, + }, ] }, } diff --git a/docs-website/src/pages/docs/clients-reference/nextjs.md b/docs-website/src/pages/docs/clients-reference/nextjs.md new file mode 100644 index 000000000..a7367263c --- /dev/null +++ b/docs-website/src/pages/docs/clients-reference/nextjs.md @@ -0,0 +1,182 @@ +--- +title: Next.js Client +pageTitle: WunderGraph - Next.js +description: +--- + +The Next.js client uses [SWR](/docs/clients-reference/swr) under the hood. + +## Installation + +```bash +npm i @wundergraph/nextjs swr@2.0.0-rc.0 +``` + +## Configuration + +Add `NextJsTemplate` to your WunderGraph configuration: + +```typescript +import { NextJsTemplate } from '@wundergraph/nextjs/dist/template' + +// wundergraph.config.ts +configureWunderGraphApplication({ + // ... your config + codeGenerators: [ + { + templates: [...templates.typescript.all], + }, + { + templates: [new NextJsTemplate()], + path: '../components/generated', + }, + ], +}) +``` + +## Hooks + +### useQuery + +This hook accepts most [useSWR Options](https://swr.vercel.app/docs/options) except for key and fetcher. + +```typescript +const { data, error, isValidating, isLoading, mutate } = useQuery({ + operationName: 'Weather', + input: { + forCity: 'Berlin', + }, + enabled: true, +}) +``` + +Calling `mutate` will invalidate and refetch the query. + +```typescript +const { data, mutate } = useQuery({ + operationName: 'Weather', + input: { + forCity: 'Berlin', + }, +}) + +mutate() +``` + +### useQuery (Live query) + +You can turn any query into a live query by adding the `liveQuery` option. + +```typescript +const { data, error, isLoading, isSubscribed, mutate } = useQuery({ + operationName: 'Weather', + input: { + forCity: 'Berlin', + }, + liveQuery: true, +}) +``` + +### useMutation + +This hook accepts most [useSWRMutation Options](https://swr.vercel.app/docs/options) except for key and fetcher. + +```typescript +const { data, error, trigger } = useMutation({ + operationName: 'SetName', +}) + +await trigger({ + name: 'test', +}) + +trigger( + { + name: 'test', + }, + { + throwOnError: false, + } +) +``` + +### useSubscription + +```typescript +const { data, error, isLoading, isSubscribed } = useSubscription({ + operationName: 'Weather', + input: { + forCity: 'Berlin', + }, + enabled: true, + onSuccess(data, key, config) { + // called when the subscription is established. + }, + onError(data, key, config) { + // called when the subscription failed to establish. + }, +}) +``` + +### useAuth + +```typescript +const { login, logout } = useAuth() + +login('github') + +logout({ logoutOpenidConnectProvider: true }) +``` + +### useUser + +This hook accepts most [useSWR Options](https://swr.vercel.app/docs/options) except for key and fetcher. + +```typescript +const { data, error, isLoading } = useUser() +``` + +## File upload + +This hook accepts most [useSWRMutation Options](https://swr.vercel.app/docs/options) except for key and fetcher. + +```typescript +const { upload, data, error } = useFileUpload() + +upload( + { + provider: 'minio', + files: [new File([''], 'test.txt')], + }, + { + throwOnError: false, + } +) +``` + +## SSR + +Wrapping the App or Page in `withWunderGraph` will make sure that Server Side Rendering (SSR) works, +that's it. + +```typescript +import { NextPage } from 'next' +import { useQuery, withWunderGraph } from '../components/generated/nextjs' + +const Home: NextPage = () => { + const dragons = useQuery({ operationName: 'Dragons' }) + return
{JSON.stringify(dragons)}
+} +export default withWunderGraph(Home) +``` + +## Global Configuration + +You can configure the hooks globally by using the [SWRConfig](https://swr.vercel.app/docs/global-configuration) context. + +In case the context configuration isn't working, it's likely due to multiple versions of SWR being installed or due to how PNPM or Yarn PnP link packages. +To resolve this you can import SWR directly from `@wundergraph/nextjs` to make sure the same instance is used. + +```ts +import { SWRConfig, useSWRConfig } from '@wundergraph/nextjs' +``` diff --git a/docs-website/src/pages/docs/clients-reference/react-query.md b/docs-website/src/pages/docs/clients-reference/react-query.md new file mode 100644 index 000000000..e4cda110b --- /dev/null +++ b/docs-website/src/pages/docs/clients-reference/react-query.md @@ -0,0 +1,161 @@ +--- +title: React Query Client +pageTitle: WunderGraph React Query Client +description: React Query Client reference +--- + +This package provides a type-safe integration of [React Query](https://tanstack.com/query/v4/docs/overview) with WunderGraph. +React Query is a data fetching library for React. With just one hook, 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. + +## Installation + +```shell +npm install @wundergraph/react-query @tanstack/react-query +``` + +## Configuration + +Before you can use the hooks, 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', + }, + ], +}) +``` + +Now you can configure the hooks. Create a new file, for example `lib/wundergraph.ts` and add the following code: + +```ts +import { createHooks } from '@wundergraph/react-query' +import { createClient, Operations } from './components/generated/client' + +const client = createClient() // Typesafe WunderGraph client + +export const { + useQuery, + useMutation, + useSubscription, + useUser, + useFileUpload, + useAuth, + queryKey, +} = createHooks(client) +``` + +In your `App.tsx` add QueryClientProvider: + +```tsx +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; + +const queryClient = new QueryClient(); + +export default App() { + return ( + +
...
+
+ ); +} +``` + +## Usage + +Now you can use the hooks in your components: + +### useQuery + +```ts +const { data, error, isLoading } = useQuery({ + operationName: 'Weather', + input: { forCity: city }, +}) +``` + +### useQuery (Live query) + +```ts +const { data, error, isLoading, isSubscribed } = useQuery({ + operationName: 'Weather', + input: { forCity: city }, + liveQuery: true, +}) +``` + +### useSubscription + +```ts +const { data, error, isLoading, isSubscribed } = useSubscription({ + operationName: 'Weather', + input: { + forCity: 'Berlin', + }, +}) +``` + +### useMutation + +```ts +const { data, mutate, mutateAsync } = useMutation({ + operationName: 'SetName', +}) + +mutate({ name: 'WunderGraph' }) + +await mutateAsync({ name: 'WunderGraph' }) +``` + +### useFileUpload + +```ts +const { upload, data: fileKeys, error } = useFileUpload() + +upload({ + provider: 'minio', + files: new FileList(), +}) +``` + +### useAuth + +```ts +const { login, logout } = useAuth() + +login('github') + +logout({ logoutOpenidConnectProvider: true }) +``` + +### useUser + +```ts +const { data: user, error } = useUser() +``` + +### queryKey + +You can use the `queryKey` helper function to create a unique key for the query in a typesafe way. This is useful if you want to invalidate the query after mutating. + +```ts +const queryClient = useQueryClient() + +const { mutate, mutateAsync } = useMutation({ + operationName: 'SetName', + onSuccess() { + queryClient.invalidateQueries(queryKey({ operationName: 'Profile' })) + }, +}) + +mutate({ name: 'WunderGraph' }) +``` + +## Options + +You can use all available options from [React Query](https://tanstack.com/query/v4/docs/reference/useQuery) with the hooks. diff --git a/docs-website/src/pages/docs/clients-reference/swr.md b/docs-website/src/pages/docs/clients-reference/swr.md new file mode 100644 index 000000000..3611620d7 --- /dev/null +++ b/docs-website/src/pages/docs/clients-reference/swr.md @@ -0,0 +1,169 @@ +--- +title: SWR Client +pageTitle: WunderGraph SWR Client +description: SWR Client reference +--- + +The SWR Client is our default client for React projects. It's a lightweight wrapper around the [SWR](https://swr.vercel.app/) library from Vercel. + +## Installation + +> Note: The client depends on SWR version 2.0.0-rc.0. + +```bash +npm i @wundergraph/swr swr@2.0.0-rc.0 +``` + +## Configuration + +Let's start by configuring WunderGraph. We're using `templates.typescript.client` to generate our base client that will be used to create typesafe hooks. + +```typescript +// wundergraph.config.ts +configureWunderGraphApplication({ + // ... your configuration + codeGenerators: [ + { + templates: [templates.typescript.client], + path: '../generated', + }, + ], +}) +``` + +## Create the hooks + +WunderGraph comes with a powerful framework for generating code. +Here we are creating fully typed SWR hooks based on the operations of our WunderGraph application. + +```ts +// lib/wundergraph.ts +import { createClient, Operations } from '../generated/client' + +import { createHooks } from '@wundergraph/swr' + +export const client = createClient() + +export const { useQuery, useMutation, useSubscription, useUser, useAuth } = + createHooks(client) +``` + +## Hooks + +### useQuery + +This hook accepts most [useSWR Options](https://swr.vercel.app/docs/options) except for key and fetcher. + +```typescript +const { data, error, isValidating, isLoading, mutate } = useQuery({ + operationName: 'Weather', + input: { + forCity: 'Berlin', + }, + enabled: true, +}) +``` + +Calling `mutate` will invalidate and refetch the query. + +```typescript +const { data, mutate } = useQuery({ + operationName: 'Weather', + input: { + forCity: 'Berlin', + }, +}) + +mutate() +``` + +### useQuery (Live query) + +You can turn any query into a live query by adding the `liveQuery` option. + +```typescript +const { data, error, isLoading, isSubscribed, mutate } = useQuery({ + operationName: 'Weather', + input: { + forCity: 'Berlin', + }, + liveQuery: true, +}) +``` + +### useMutation + +This hook accepts most [useSWRMutation Options](https://swr.vercel.app/docs/options) except for key and fetcher. + +```typescript +const { data, error, trigger } = useMutation({ + operationName: 'SetName', +}) + +await trigger({ + name: 'test', +}) + +trigger( + { + name: 'test', + }, + { + throwOnError: false, + } +) +``` + +### useSubscription + +```typescript +const { data, error, isLoading, isSubscribed } = useSubscription({ + operationName: 'Weather', + input: { + forCity: 'Berlin', + }, + enabled: true, + onSuccess(data, key, config) { + // called when the subscription is established. + }, + onError(data, key, config) { + // called when the subscription failed to establish. + }, +}) +``` + +### useAuth + +```typescript +const { login, logout } = useAuth() + +login('github') + +logout({ logoutOpenidConnectProvider: true }) +``` + +### useUser + +This hook accepts most [useSWR Options](https://swr.vercel.app/docs/options) except for key and fetcher. + +```typescript +const { data, error, isLoading } = useUser() +``` + +## File upload + +This hook accepts most [useSWRMutation Options](https://swr.vercel.app/docs/options) except for key and fetcher. + +```typescript +const { upload, data, error } = useFileUpload() + +upload( + { + provider: 'minio', + files: [new File([''], 'test.txt')], + }, + { + throwOnError: false, + } +) +``` diff --git a/docs-website/src/pages/docs/clients-reference/typescript-client.md b/docs-website/src/pages/docs/clients-reference/typescript-client.md new file mode 100644 index 000000000..69e082159 --- /dev/null +++ b/docs-website/src/pages/docs/clients-reference/typescript-client.md @@ -0,0 +1,248 @@ +--- +title: TypeScript Client +pageTitle: WunderGraph TypeScript Client +description: WunderGraph TypeScript Client reference +--- + +This is the base implementation of the WunderGraph HTTP protocol in TypeScript that can be used on both browser and server environments. +It's used as the base interface for the Web client, React and Next.js implementations. + +## Configuration + +Let's start by adding the WunderGraph TypeScript client generator to your project. + +```typescript +// wundergraph.config.ts + +configureWunderGraphApplication({ + // ... your configuration + codeGenerators: [ + { + templates: [templates.typescript.client], + path: '../generated', + }, + ], +}) +``` + +## Create the client + +The generated `createClient` will return a fully typesafe client that can be used to execute operations. + +```ts +import { createClient } from 'generated/client' + +const client = createClient() +``` + +## Client configuration + +### Custom baseURL + +The client can be configured with a custom baseURL. + +```ts +const client = createClient({ + baseURL: 'https://my-custom-base-url.com', +}) +``` + +### Node.js support + +You can use the client on server environments that don't have a build-in fetch implementation by using the customFetch configuration option. + +Install node-fetch or any other fetch polyfill. + +```bash +npm i node-fetch +``` + +And add it to the client configuration. + +```ts +import fetch from 'node-fetch' + +const client = createClient({ + customFetch: fetch, +}) +``` + +### Browser + +If you target older browsers you will need a polyfill for fetch, AbortController, AbortSignal and possibly Promise. + +```ts +import 'promise-polyfill/src/polyfill' +import 'yet-another-abortcontroller-polyfill' +import { fetch } from 'whatwg-fetch' + +const client = createClient({ + customFetch: fetch, +}) +``` + +### Adding custom headers + +```ts +const client = createClient({ + extraHeaders: { + customHeader: 'value', + }, +}) + +// or + +client.setExtraHeaders({ + customHeader: 'value', +}) +``` + +## Methods + +### Run a query + +```ts +const response = await client.query({ + operationName: 'Hello', + input: { + hello: 'World', + }, +}) +``` + +### Mutation + +```ts +const response = await client.mutate({ + operationName: 'SetName', + input: { + name: 'WunderGraph', + }, +}) +``` + +### LiveQuery + +```ts +client.subscribe( + { + operationName: 'Hello', + input: { + name: 'World', + }, + liveQuery: true, + }, + (response) => {} +) +``` + +### Subscription + +```ts +client.subscribe( + { + operationName: 'Countdown', + input: { + from: 100, + }, + }, + (response) => {} +) +``` + +### One-of subscription + +You can run subscriptions using `subscribeOnce`, this will return the subscription response directly and will not setup a stream. +This is useful for SSR purposes for example. + +```ts +const response = await client.subscribe( + { + operationName: 'Countdown', + input: { + from: 100, + }, + subscribeOnce: true, + }, + (response) => {} +) +``` + +### Upload files + +```ts +const { fileKeys } = await client.uploadFiles({ + provider: S3Provider.minio, + files, +}) +``` + +## Auth + +### Login + +```ts +client.login('github') +``` + +### Log out + +```ts +client.logout({ + logoutOpenidConnectProvider: true, +}) +``` + +### Fetch user + +```ts +const user = await client.fetchUser() +``` + +## AbortController + +Almost all methods accept an AbortController instance that can be used to cancel the request. + +```ts +const controller = new AbortController() + +const { fileKeys } = await client.uploadFiles({ + abortSignal: abortController.signal, + provider: S3Provider.minio, + files, +}) + +// cancel the request +controller.abort() +``` + +## Error handling + +### Operations + +Query and mutation errors are returned as a `GraphQLResponseError` object. By default, the first error specifiy the error message but you can access all GraphQL errors through the `errors` property. +Network errors and non 2xx responses are returned as a `ResponseError` object and contain the status code as `statusCode` property. + +```ts +const { data, error } = await client.query({ + operationName: 'Hello', + input: { + hello: 'World', + }, +}) + +if (error instanceof GraphQLResponseError) { + error.errors[0].location +} else if (error instanceof ResponseError) { + error.statusCode +} +``` + +### Other + +Methods that initiate a network request throw a `ResponseError` or `Error` if the request fails to initiate or the response is not 2xx. +You can be sure that the request was successful if the method doesn't throw an error. + +## Limitations + +- Subscriptions are not supported server side, but can be fetched using `subscribeOnce`. diff --git a/docs-website/src/pages/docs/examples/nextjs-react-query.md b/docs-website/src/pages/docs/examples/nextjs-react-query.md new file mode 100644 index 000000000..158af924d --- /dev/null +++ b/docs-website/src/pages/docs/examples/nextjs-react-query.md @@ -0,0 +1,123 @@ +--- +title: Next.js + React Query Example +pageTitle: WunderGraph - Examples - Next.js - React Query +description: +--- + +[The NextJS example](https://github.com/wundergraph/wundergraph/tree/main/examples/nextjs-react-query) demonstrates the power of +code generation, +when it comes to integrating WunderGraph with frontend frameworks like Next.js. + +## Configuration + +Let's start by configuring WunderGraph. + +```typescript +// wundergraph.config.ts +const spaceX = introspect.graphql({ + apiNamespace: 'spacex', + url: 'https://spacex-api.fly.dev/graphql/', +}) + +const myApplication = new Application({ + name: 'app', + apis: [spaceX], +}) + +configureWunderGraphApplication({ + application: myApplication, + server, + operations, + codeGenerators: [ + { + templates: [ + ...templates.typescript.all, + templates.typescript.operations, + templates.typescript.linkBuilder, + ], + }, + { + templates: [templates.typescript.client], + path: '../components/generated', + }, + ], +}) +``` + +What's notable here is that we're using `templates.typescript.client` to generate our base client that is used by the React Query [`@wundergraph/react-query`](https://github.com/wundergraph/wundergraph/tree/main/packages/react-query) package. + +## Define an Operation + +```graphql +# .wundergraph/operations/Dragons.graphql +query Dragons { + spacex_dragons { + name + active + } +} +``` + +## Install the React Query Client + +```bash +npm i @wundergraph/react-query @tanstack/react-query +``` + +Next up is setting up the React Query hooks. + +Create a new `.ts` file for example `lib/wundergraph.ts` and add the following code: + +```ts +import { createClient, Operations } from '../generated/client' + +import { createHooks } from '@wundergraph/react-query' + +export const client = createClient() + +export const { + useQuery, + useMutation, + useSubscription, + useUser, + useAuth, + queryKey, +} = createHooks(client) +``` + +## Configure your App + +```ts +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' + +const queryClient = new QueryClient() + +export default function App({ Component, pageProps }) { + return ( + + + + ) +} +``` + +## Running the Operation + +Now we're ready to run the operation. Edit `pages/index.tsx` and add the following code: + +```typescript +import { NextPage } from 'next' +import { useQuery, withWunderGraph } from '../components/generated/nextjs' + +export default function Home() { + const dragons = useQuery({ operationName: 'Dragons' }) + return
{JSON.stringify(dragons.data)}
+} +``` + +That's it! You can now run the example with `npm run dev` and see the result in your browser. + +Learn more: + +- [@wundergraph/react-query reference](/docs/clients-reference/react-query) +- [React Query documentation](https://tanstack.com/query/v4/docs/overview) diff --git a/docs-website/src/pages/docs/examples/nextjs.md b/docs-website/src/pages/docs/examples/nextjs.md index f9c4fb054..ffcd88400 100644 --- a/docs-website/src/pages/docs/examples/nextjs.md +++ b/docs-website/src/pages/docs/examples/nextjs.md @@ -25,14 +25,10 @@ configureWunderGraphApplication({ operations, codeGenerators: [ { - templates: [ - ...templates.typescript.all, - templates.typescript.operations, - templates.typescript.linkBuilder, - ], + templates: [...templates.typescript.all], }, { - templates: [templates.typescript.client, new NextJsTemplate()], + templates: [new NextJsTemplate()], path: '../components/generated', }, ], @@ -76,3 +72,7 @@ so all you have to do is to import he `useQuery` hook and call your newly create Wrapping the Page in `withWunderGraph` will make sure that Server Side Rendering (SSR) works, that's it. + +## What's next + +Check out the [Next.js client documentation](/docs/clients-reference/nextjs) for more information. diff --git a/examples/nextjs-react-query/.gitignore b/examples/nextjs-react-query/.gitignore new file mode 100644 index 000000000..0bbe4a237 --- /dev/null +++ b/examples/nextjs-react-query/.gitignore @@ -0,0 +1,43 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +.vscode +.idea + +# WunderGraph build output +.wundergraph/cache +.wundergraph/generated + +# dependencies +node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env.local +.env.development.local +.env.test.local +.env.production.local + +# vercel +.vercel + +migrations diff --git a/examples/nextjs-react-query/.graphqlconfig b/examples/nextjs-react-query/.graphqlconfig new file mode 100644 index 000000000..5339585b7 --- /dev/null +++ b/examples/nextjs-react-query/.graphqlconfig @@ -0,0 +1,14 @@ +{ + "project": { + "schemaPath": ".wundergraph/generated/wundergraph.schema.graphql", + "extensions": { + "endpoint": { + "introspect": false, + "url": "http://localhost:9991/graphql", + "headers": { + "user-agent": "WunderGraph Client" + } + } + } + } +} \ No newline at end of file diff --git a/examples/nextjs-react-query/.wundergraph/operations/Dragons.graphql b/examples/nextjs-react-query/.wundergraph/operations/Dragons.graphql new file mode 100644 index 000000000..6ec1ad3fd --- /dev/null +++ b/examples/nextjs-react-query/.wundergraph/operations/Dragons.graphql @@ -0,0 +1,7 @@ +query Dragons { + spacex_dragons { + name + active + id + } +} diff --git a/examples/nextjs-react-query/.wundergraph/wundergraph.config.ts b/examples/nextjs-react-query/.wundergraph/wundergraph.config.ts new file mode 100644 index 000000000..2151edbae --- /dev/null +++ b/examples/nextjs-react-query/.wundergraph/wundergraph.config.ts @@ -0,0 +1,38 @@ +import { authProviders, configureWunderGraphApplication, cors, introspect, templates } from '@wundergraph/sdk'; +import { NextJsTemplate } from '@wundergraph/nextjs/dist/template'; +import server from './wundergraph.server'; +import operations from './wundergraph.operations'; + +const spaceX = introspect.graphql({ + apiNamespace: 'spacex', + url: 'https://spacex-api.fly.dev/graphql/', +}); + +// configureWunderGraph emits the configuration +configureWunderGraphApplication({ + apis: [spaceX], + server, + operations, + codeGenerators: [ + { + templates: [...templates.typescript.all, templates.typescript.operations, templates.typescript.linkBuilder], + }, + { + templates: [templates.typescript.client, new NextJsTemplate()], + path: '../components/generated', + }, + ], + cors: { + ...cors.allowAll, + allowedOrigins: process.env.NODE_ENV === 'production' ? ['http://localhost:3000'] : ['http://localhost:3000'], + }, + authentication: { + cookieBased: { + providers: [authProviders.demo()], + authorizedRedirectUris: ['http://localhost:3000'], + }, + }, + security: { + enableGraphQLEndpoint: process.env.NODE_ENV !== 'production', + }, +}); diff --git a/examples/nextjs-react-query/.wundergraph/wundergraph.operations.ts b/examples/nextjs-react-query/.wundergraph/wundergraph.operations.ts new file mode 100644 index 000000000..47ccae4b1 --- /dev/null +++ b/examples/nextjs-react-query/.wundergraph/wundergraph.operations.ts @@ -0,0 +1,32 @@ +import { configureWunderGraphOperations } from '@wundergraph/sdk'; +import type { OperationsConfiguration } from './generated/wundergraph.operations'; + +export default configureWunderGraphOperations({ + operations: { + defaultConfig: { + authentication: { + required: false, + }, + }, + queries: (config) => ({ + ...config, + caching: { + enable: false, + staleWhileRevalidate: 60, + maxAge: 60, + public: true, + }, + liveQuery: { + enable: true, + pollingIntervalSeconds: 1, + }, + }), + mutations: (config) => ({ + ...config, + }), + subscriptions: (config) => ({ + ...config, + }), + custom: {}, + }, +}); diff --git a/examples/nextjs-react-query/.wundergraph/wundergraph.server.ts b/examples/nextjs-react-query/.wundergraph/wundergraph.server.ts new file mode 100644 index 000000000..3c636728e --- /dev/null +++ b/examples/nextjs-react-query/.wundergraph/wundergraph.server.ts @@ -0,0 +1,12 @@ +import { GraphQLObjectType, GraphQLSchema, GraphQLString } from 'graphql'; +import { configureWunderGraphServer } from '@wundergraph/sdk'; +import type { HooksConfig } from './generated/wundergraph.hooks'; +import type { InternalClient } from './generated/wundergraph.internal.client'; + +export default configureWunderGraphServer(() => ({ + hooks: { + queries: {}, + mutations: {}, + }, + graphqlServers: [], +})); diff --git a/examples/nextjs-react-query/README.md b/examples/nextjs-react-query/README.md new file mode 100644 index 000000000..245724431 --- /dev/null +++ b/examples/nextjs-react-query/README.md @@ -0,0 +1,27 @@ +# WunderGraph Next.js React Query Starter + +This example demonstrates how to use WunderGraph with Next.js and React Query. We are going to make your data-source accessible through JSON-RPC to your Next.js app. + +## Getting Started + +Install the dependencies and run the complete example in one command: + +```shell +npm install && npm start +``` + +After a while, a new browser tab will open, +and you can start exploring the application. +If no tab is open, navigate to [http://localhost:3000](http://localhost:3000). + +Running WunderGraph will automatically introspect the data-source and generate an API for you. +You can add more Operations (e.g. Queries or Mutations) by adding more "\*.graphql" files to the directory `./wundergraph/operations`. +Each file becomes an Operation. The Operation name is not relevant, the file name is. + +## Learn More + +Read the [Docs](https://wundergraph.com/docs). + +## Got Questions? + +Join us on [Discord](https://wundergraph.com/discord)! diff --git a/examples/nextjs-react-query/components/Nav.tsx b/examples/nextjs-react-query/components/Nav.tsx new file mode 100644 index 000000000..4901d165a --- /dev/null +++ b/examples/nextjs-react-query/components/Nav.tsx @@ -0,0 +1,14 @@ +const NavBar = () => { + return ( + + ); +}; +export default NavBar; diff --git a/examples/nextjs-react-query/lib/react-query.ts b/examples/nextjs-react-query/lib/react-query.ts new file mode 100644 index 000000000..8cd8e022f --- /dev/null +++ b/examples/nextjs-react-query/lib/react-query.ts @@ -0,0 +1,6 @@ +import { createHooks } from '@wundergraph/react-query'; +import { createClient, Operations } from '../components/generated/client'; +const client = createClient(); // Typesafe WunderGraph client + +export const { useQuery, useMutation, useSubscription, useUser, useFileUpload, useAuth, queryKey } = + createHooks(client); diff --git a/examples/nextjs-react-query/next-env.d.ts b/examples/nextjs-react-query/next-env.d.ts new file mode 100644 index 000000000..4f11a03dc --- /dev/null +++ b/examples/nextjs-react-query/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/examples/nextjs-react-query/next.config.js b/examples/nextjs-react-query/next.config.js new file mode 100644 index 000000000..5ee7a35ec --- /dev/null +++ b/examples/nextjs-react-query/next.config.js @@ -0,0 +1,8 @@ +const path = require('path'); +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, + swcMinify: true, +}; + +module.exports = nextConfig; diff --git a/examples/nextjs-react-query/package.json b/examples/nextjs-react-query/package.json new file mode 100644 index 000000000..3f03e81f2 --- /dev/null +++ b/examples/nextjs-react-query/package.json @@ -0,0 +1,36 @@ +{ + "name": "wundergraph-nextjs", + "version": "0.1.0", + "private": true, + "scripts": { + "start": "run-p dev wundergraph open", + "wundergraph": "wunderctl up --debug", + "open": "wait-on -d 500 http://localhost:9991 && open-cli http://localhost:3000", + "build": "next build", + "dev": "next dev", + "check": "tsc --noEmit" + }, + "dependencies": { + "@tanstack/react-query": "^4.16.1", + "@wundergraph/nextjs": "^0.5.0", + "@wundergraph/react-query": "^0.1.0", + "@wundergraph/sdk": "^0.123.0", + "graphql": "^16.3.0", + "next": "^12.1.6", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "devDependencies": { + "@types/node": "^17.0.15", + "@types/react": "^18.0.6", + "autoprefixer": "^10.4.7", + "node-fetch": "^2.6.7", + "npm-run-all": "^4.1.5", + "open-cli": "^7.0.1", + "postcss": "^8.4.19", + "tailwindcss": "^3.1.4", + "ts-node": "^10.8.0", + "typescript": "^4.8.2", + "wait-on": "^6.0.1" + } +} diff --git a/examples/nextjs-react-query/pages/_app.tsx b/examples/nextjs-react-query/pages/_app.tsx new file mode 100644 index 000000000..7d4cce0e9 --- /dev/null +++ b/examples/nextjs-react-query/pages/_app.tsx @@ -0,0 +1,21 @@ +import Head from 'next/head'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; + +function MyApp({ Component, pageProps }) { + const queryClient = new QueryClient(); + return ( + <> + + + + + +
+ + + +
+ + ); +} +export default MyApp; diff --git a/examples/nextjs-react-query/pages/index.tsx b/examples/nextjs-react-query/pages/index.tsx new file mode 100644 index 000000000..56f9aff94 --- /dev/null +++ b/examples/nextjs-react-query/pages/index.tsx @@ -0,0 +1,64 @@ +import { NextPage } from 'next'; +import { useQuery } from '../lib/react-query'; +import Nav from '../components/Nav'; + +const Home: NextPage = () => { + const dragons = useQuery({ + operationName: 'Dragons', + }); + + return ( +
+
+ ); +}; + +export default Home; diff --git a/examples/nextjs-react-query/postcss.config.js b/examples/nextjs-react-query/postcss.config.js new file mode 100644 index 000000000..e873f1a4f --- /dev/null +++ b/examples/nextjs-react-query/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/examples/nextjs-react-query/public/favicon.ico b/examples/nextjs-react-query/public/favicon.ico new file mode 100644 index 000000000..4965832f2 Binary files /dev/null and b/examples/nextjs-react-query/public/favicon.ico differ diff --git a/examples/nextjs-react-query/tailwind.config.js b/examples/nextjs-react-query/tailwind.config.js new file mode 100644 index 000000000..3a182af4c --- /dev/null +++ b/examples/nextjs-react-query/tailwind.config.js @@ -0,0 +1,8 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'], + theme: { + extend: {}, + }, + plugins: [], +}; diff --git a/examples/nextjs-react-query/tsconfig.json b/examples/nextjs-react-query/tsconfig.json new file mode 100644 index 000000000..2bfacdc5e --- /dev/null +++ b/examples/nextjs-react-query/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true + }, + "include": ["next-env.d.ts", "**/*.ts", ".wundergraph/**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"], + "ts-node": { + "compilerOptions": { + "module": "commonjs" + } + } +} diff --git a/examples/vite-swr/readme.md b/examples/vite-swr/readme.md new file mode 100644 index 000000000..fecb8450a --- /dev/null +++ b/examples/vite-swr/readme.md @@ -0,0 +1,31 @@ +# WunderGraph Vite + SWR Starter + +This example demonstrates how to use WunderGraph with [Vite](https://vitejs.dev/) and [SWR](https://swr.vercel.app). + +Vite is a new build tool that aims to provide a faster and leaner development experience for modern web projects. It is built on top of [Rollup](https://rollupjs.org/guide/en/) and [esbuild](https://esbuild.github.io/). + +SWR is a React library for data fetching. With just one hook, 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. + +## Getting Started + +Install the dependencies and run the complete example in one command: + +```shell +npm install && npm run dev +``` + +After a while, a new browser tab will open, +and you can start exploring the application. +If no tab is open, navigate to [http://localhost:5173](http://localhost:5173). + +Running WunderGraph will automatically introspect the data-source and generate an API for you. +You can add more Operations (e.g. Queries or Mutations) by adding more "\*.graphql" files to the directory `./wundergraph/operations`. +Each file becomes an Operation. The Operation name is not relevant, the file name is. + +## Learn More + +Read the [Docs](https://wundergraph.com/docs). + +## Got Questions? + +Join us on [Discord](https://wundergraph.com/discord)! diff --git a/packages/nextjs/README.md b/packages/nextjs/README.md index a02bac9ab..d158c3ea2 100644 --- a/packages/nextjs/README.md +++ b/packages/nextjs/README.md @@ -9,7 +9,7 @@ WunderGraph codegen template plugin to add deep Next.js integration. ## Getting Started ```shell -npm install @wundergraph/nextjs +npm install @wundergraph/nextjs swr@2.0.0-rc.0 ``` ### 1. Register the codegen template @@ -33,13 +33,21 @@ configureWunderGraphApplication({ ```tsx // pages/authentication.ts -import { useQuery, useMutation, useLiveQuery, AuthProviders } from '.wundergraph/generated/nextjs'; +import { + withWunderGraph, + useQuery, + useMutation, + useSubscription, + useAuth, + useUser, +} from '.wundergraph/generated/nextjs'; const Example: ExamplePage = () => { - const { user, login, logout } = useWunderGraph(); + const { login, logout } = useAuth(); + const { data: user } = useUser(); const onClick = () => { if (user === null || user === undefined) { - login(AuthProviders.github); + login('github'); } else { logout(); } diff --git a/packages/react-query/.npmignore b/packages/react-query/.npmignore new file mode 100644 index 000000000..92f017677 --- /dev/null +++ b/packages/react-query/.npmignore @@ -0,0 +1,5 @@ +node_modules +src +tsconfig.json +.gitignore +dist/tsconfig.tsbuildinfo diff --git a/packages/react-query/CHANGELOG.md b/packages/react-query/CHANGELOG.md new file mode 100644 index 000000000..e4d87c4d4 --- /dev/null +++ b/packages/react-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/react-query/README.md b/packages/react-query/README.md new file mode 100644 index 000000000..be49c512e --- /dev/null +++ b/packages/react-query/README.md @@ -0,0 +1,157 @@ +# WunderGraph React Query Integration + +![wunderctl](https://img.shields.io/npm/v/@wundergraph/react-query.svg) + +This package provides a type-safe integration of [React Query](https://tanstack.com/query/v4/docs/overview) with WunderGraph. +React Query is a data fetching library for React. With just one hook, 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/react-query @tanstack/react-query +``` + +Before you can use the hooks, 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 configure the hooks. Create a new file, for example `lib/wundergraph.ts` and add the following code: + +```ts +import { createHooks } from '@wundergraph/react-query'; +import { createClient, Operations } from './components/generated/client'; + +const client = createClient(); // Typesafe WunderGraph client + +export const { useQuery, useMutation, useSubscription, useUser, useFileUpload, useAuth } = + createHooks(client); +``` + +In your `App.tsx` add QueryClientProvider: + +```tsx +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; + +const queryClient = new QueryClient(); + +export default App() { + return ( + +
...
+
+ ); +} +``` + +Now you can use the hooks in your components: + +### useQuery + +```ts +const { data, error, isLoading } = useQuery({ + operationName: 'Weather', + input: { forCity: city }, +}); +``` + +### useQuery (Live query) + +```ts +const { data, error, isLoading, isSubscribed } = useQuery({ + operationName: 'Weather', + input: { forCity: city }, + liveQuery: true, +}); +``` + +### useSubscription + +```ts +const { data, error, isLoading, isSubscribed } = useSubscription({ + operationName: 'Weather', + input: { + forCity: 'Berlin', + }, +}); +``` + +### useMutation + +```ts +const { data, mutate, mutateAsync } = useMutation({ + operationName: 'SetName', +}); + +mutate({ name: 'WunderGraph' }); + +await mutateAsync({ name: 'WunderGraph' }); +``` + +### useFileUpload + +```ts +const { upload, uploadAsync, data: fileKeys, error } = useFileUpload(); + +upload({ + provider: 'minio', + files: new FileList(), +}); + +await upload({ + provider: 'minio', + files: new FileList(), +}); +``` + +### useAuth + +```ts +const { login, logout } = useAuth(); + +login('github'); + +logout({ logoutOpenidConnectProvider: true }); +``` + +### useUser + +```ts +const { data: user, error } = useUser(); +``` + +### queryKey + +You can use the `queryKey` helper function to create a unique key for the query in a typesafe way. This is useful if you want to invalidate the query after mutating. + +```ts +const queryClient = useQueryClient(); + +const { mutate, mutateAsync } = useMutation({ + operationName: 'SetName', + onSuccess() { + queryClient.invalidateQueries(queryKey({ operationName: 'Profile' })); + }, +}); + +mutate({ name: 'WunderGraph' }); +``` + +## Options + +You can use all available options from [React Query](https://tanstack.com/query/v4/docs/reference/useQuery) with the hooks. diff --git a/packages/react-query/jest-setup.ts b/packages/react-query/jest-setup.ts new file mode 100644 index 000000000..7b0828bfa --- /dev/null +++ b/packages/react-query/jest-setup.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom'; diff --git a/packages/react-query/jest.config.js b/packages/react-query/jest.config.js new file mode 100644 index 000000000..558e4b2c8 --- /dev/null +++ b/packages/react-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/react-query/package.json b/packages/react-query/package.json new file mode 100644 index 000000000..e9083f254 --- /dev/null +++ b/packages/react-query/package.json @@ -0,0 +1,62 @@ +{ + "name": "@wundergraph/react-query", + "version": "0.1.0", + "license": "Apache-2.0", + "description": "WunderGraph React Query Integration", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "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": { + "@tanstack/react-query": "^4.16.1", + "@wundergraph/sdk": ">=0.110.0", + "react": "^16.8.0 || ^17.0.2 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.2 || ^18.0.0" + }, + "devDependencies": { + "@swc/core": "^1.3.14", + "@swc/jest": "^0.2.23", + "@tanstack/react-query": "^4.16.1", + "@testing-library/jest-dom": "^5.16.5", + "@testing-library/react": "^13.4.0", + "@types/jest": "^28.1.1", + "@types/node-fetch": "2.6.2", + "@types/react": "^18.0.24", + "@types/react-dom": "^18.0.6", + "@wundergraph/sdk": "workspace:*", + "jest": "^29.0.3", + "jest-environment-jsdom": "^29.3.0", + "nock": "^13.2.9", + "node-fetch": "2.6.7", + "react": "^18.1.0", + "react-dom": "^18.1.0", + "tsd": "^0.24.1", + "typescript": "^4.8.2" + }, + "homepage": "https://wundergraph.com", + "author": { + "name": "WunderGraph Maintainers", + "email": "info@wundergraph.com" + }, + "keywords": [ + "react-query", + "wundergraph", + "react" + ] +} diff --git a/packages/react-query/src/hooks.ts b/packages/react-query/src/hooks.ts new file mode 100644 index 000000000..e34451694 --- /dev/null +++ b/packages/react-query/src/hooks.ts @@ -0,0 +1,352 @@ +import { + useQuery as useTanstackQuery, + useMutation as useTanstackMutation, + useQueryClient, + QueryFunctionContext, +} from '@tanstack/react-query'; + +import { GraphQLResponseError, OperationsDefinition, LogoutOptions, Client } from '@wundergraph/sdk/client'; +import { serialize } from '@wundergraph/sdk/internal'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { + QueryFetcher, + MutationFetcher, + SubscribeToOptions, + UseSubscribeToProps, + UseQueryHook, + UseMutationHook, + UseSubscriptionHook, + UseUploadHook, + UseUserHook, + QueryKey, +} from './types'; + +export const userQueryKey = 'wg_user'; + +export const createHooks = (client: Client) => { + const queryFetcher: QueryFetcher = async (query) => { + const result = await client.query(query); + + if (result.error) { + throw result.error; + } + + return result.data; + }; + + const mutationFetcher: MutationFetcher = async (mutation) => { + const result = await client.mutate(mutation); + + if (result.error) { + throw result.error; + } + + return result.data; + }; + + const queryKey: QueryKey = ({ operationName, input }) => { + return [operationName, input]; + }; + + /** + * Execute a WunderGraph query. + * + * @usage + * ```ts + * const { data, error, isLoading } = useQuery({ + * 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 useQuery: UseQueryHook = (options) => { + const { operationName, liveQuery, input, ...queryOptions } = options; + const queryHash = serialize([operationName, input]); + + const result = useTanstackQuery( + queryKey({ operationName, input }), + ({ signal }: QueryFunctionContext) => queryFetcher({ operationName, input, abortSignal: signal }), + queryOptions + ); + + const onSuccess = useCallback( + (response: any) => { + options.onSuccess?.(response); + }, + [options.onSuccess] + ); + + const onError = useCallback( + (err: GraphQLResponseError) => { + options.onError?.(err); + }, + [options.onError] + ); + + const { isSubscribed } = useSubscribeTo({ + queryHash, + operationName, + input, + liveQuery, + enabled: options.enabled && liveQuery, + onSuccess, + onError, + }); + + if (liveQuery) { + return { + ...result, + isSubscribed, + }; + } + + return result; + }; + + /** + * Execute a WunderGraph mutation. + * + * @usage + * ```ts + * const { mutate, data, error, isLoading } = useMutation({ + * operationName: 'SetName' + * }) + * + * mutate({ + * name: 'John Doe' + * }) + * ``` + */ + const useMutation: UseMutationHook = (options) => { + const { operationName, ...mutationOptions } = options; + + return useTanstackMutation({ + mutationKey: [operationName], + mutationFn: (input) => mutationFetcher({ operationName, input }), + ...mutationOptions, + }); + }; + + const useAuth = () => { + 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 } = useUser() + * ``` + */ + const useUser: UseUserHook = (options) => { + const { revalidate, ...queryOptions } = options || {}; + return useTanstackQuery( + [userQueryKey], + ({ signal }) => + client.fetchUser({ + revalidate, + abortSignal: signal, + }), + queryOptions + ); + }; + + /** + * Upload a file to S3 compatible storage. + * + * @usage + * ```ts + * const { upload, data, error } = useFileUpload() + * + * const uploadFile = (file: File) => { + * upload(file) + * } + * ``` + */ + const useFileUpload: UseUploadHook = (options) => { + const { mutate, mutateAsync, ...mutation } = useTanstackMutation( + ['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 hook used in useQuery and useSubscription + const useSubscribeTo = (props: UseSubscribeToProps) => { + const client = useQueryClient(); + const { queryHash, operationName, input, enabled, liveQuery, subscribeOnce, resetOnMount, onSuccess, onError } = + props; + + const startedAtRef = useRef(null); + + const [state, setState] = useState({ + isLoading: false, + isSubscribed: false, + }); + + useEffect(() => { + if (!startedAtRef.current && resetOnMount) { + client.removeQueries([operationName, input]); + } + }, []); + + useEffect(() => { + if (enabled) { + setState({ isLoading: true, isSubscribed: false }); + } + }, [enabled]); + + useEffect(() => { + let unsubscribe: ReturnType; + + if (enabled) { + unsubscribe = subscribeTo({ + operationName, + input, + liveQuery, + subscribeOnce, + onError(error) { + setState({ isLoading: false, isSubscribed: false }); + onError?.(error); + startedAtRef.current = null; + }, + onResult(result) { + if (!startedAtRef.current) { + setState({ isLoading: false, isSubscribed: true }); + onSuccess?.(result); + startedAtRef.current = 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() { + setState({ isLoading: false, isSubscribed: false }); + startedAtRef.current = null; + }, + }); + } + + return () => { + unsubscribe?.(); + }; + }, [queryHash, enabled, liveQuery, subscribeOnce, onSuccess, onError]); + + return state; + }; + + /** + * useSubscription + * + * Subscribe to subscription operations. + * + * @usage + * ```ts + * const { data, error, isLoading, isSubscribed } = useSubscription({ + * operationName: 'Countdown', + * }) + */ + const useSubscription: UseSubscriptionHook = (options) => { + const { + enabled = true, + operationName, + input, + subscribeOnce, + onSuccess: onSuccessProp, + onError: onErrorProp, + } = options; + const queryHash = serialize([operationName, input]); + + const subscription = useTanstackQuery([operationName, input]); + + const onSuccess = useCallback( + (response: object) => { + onSuccessProp?.(response); + }, + [onSuccessProp, queryHash] + ); + + const onError = useCallback( + (error: GraphQLResponseError) => { + onErrorProp?.(error); + }, + [onErrorProp, queryHash] + ); + + const { isSubscribed } = useSubscribeTo({ + queryHash, + operationName, + input, + subscribeOnce, + enabled, + onSuccess, + onError, + }); + + return { + ...subscription, + isSubscribed, + }; + }; + + return { + useAuth, + useFileUpload, + useUser, + useMutation, + useQuery, + useSubscription, + queryKey, + }; +}; diff --git a/packages/react-query/src/index.ts b/packages/react-query/src/index.ts new file mode 100644 index 000000000..03272ab46 --- /dev/null +++ b/packages/react-query/src/index.ts @@ -0,0 +1,3 @@ +export { createHooks } from './hooks'; + +export * from './types'; diff --git a/packages/react-query/src/types.ts b/packages/react-query/src/types.ts new file mode 100644 index 000000000..39999258f --- /dev/null +++ b/packages/react-query/src/types.ts @@ -0,0 +1,163 @@ +import { + ClientResponse, + FetchUserRequestOptions, + GraphQLResponseError, + OperationRequestOptions, + OperationsDefinition, + SubscriptionRequestOptions, + UploadRequestOptions, +} from '@wundergraph/sdk/client'; + +import { + UseQueryOptions as UseTanstackQueryOptions, + UseMutationOptions as UseTanstackMutationOptions, + UseQueryResult, + UseMutationResult, +} from '@tanstack/react-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 UseQueryOptions = Omit< + UseTanstackQueryOptions, + 'queryKey' | 'queryFn' +> & { + operationName: OperationName; + liveQuery?: LiveQuery; + input?: Input; +}; + +export type UseQueryHook = { + < + 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: UseQueryOptions & ExtraOptions + ): UseQueryResult; +}; + +export type UseSubscriptionOptions = { + operationName: OperationName; + subscribeOnce?: boolean; + resetOnMount?: boolean; + enabled?: boolean; + input?: Input; + onSuccess?(response: ClientResponse): void; + onError?(error: Error): void; +}; + +export type UseSubscriptionHook = { + < + 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 + ): UseSubscriptionResponse; +}; + +export type UseSubscriptionResponse = UseQueryResult & { + isSubscribed: boolean; +}; + +export type UseMutationOptions = Omit< + UseTanstackMutationOptions, + 'mutationKey' | 'mutationFn' +> & { + operationName: OperationName; +}; + +export type UseMutationHook = { + < + 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 + ): UseMutationResult; +}; + +export interface UseSubscribeToProps extends SubscriptionRequestOptions { + queryHash: string; + enabled?: boolean; + resetOnMount?: boolean; + onSuccess?(response: ClientResponse): void; + onError?(error: GraphQLResponseError): void; +} + +export interface SubscribeToOptions extends SubscriptionRequestOptions { + onResult(response: ClientResponse): void; + onSuccess?(response: ClientResponse): void; + onError?(error: GraphQLResponseError): void; + onAbort?(): void; +} + +export interface UseUserOptions + extends FetchUserRequestOptions, + UseTanstackQueryOptions { + enabled?: boolean; +} + +export type UseUserHook = { + (options?: UseUserOptions): UseQueryResult; +}; + +export type UseUploadHook = { + ( + options?: Omit< + UseTanstackMutationOptions>, + 'fetcher' + > + ): Omit< + UseTanstackMutationOptions>, + 'mutate' + > & { + upload: UseMutationResult>['mutate']; + uploadAsync: UseMutationResult< + string[], + GraphQLResponseError, + UploadRequestOptions + >['mutateAsync']; + }; +}; diff --git a/packages/react-query/tests/hooks.test.tsx b/packages/react-query/tests/hooks.test.tsx new file mode 100644 index 000000000..f6fdd8d2e --- /dev/null +++ b/packages/react-query/tests/hooks.test.tsx @@ -0,0 +1,418 @@ +import { act, waitFor, screen, render, fireEvent } from '@testing-library/react'; +import React from 'react'; +import { QueryCache, QueryClient, QueryClientProvider, useQueryClient } from '@tanstack/react-query'; + +import { Client, ClientConfig, OperationsDefinition } from '@wundergraph/sdk/client'; +import nock from 'nock'; +import fetch from 'node-fetch'; + +import { createHooks } from '../src/hooks'; + +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 _renderWithConfig = (element: React.ReactElement, config: any): ReturnType => { + const TestProvider = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + return render(element, { wrapper: TestProvider }); +}; + +export const renderWithClient = ( + element: React.ReactElement, + queryClient: QueryClient +): ReturnType => { + return _renderWithConfig(element, { client: queryClient }); +}; + +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, + }, + }, + ...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('React Query - createHooks', () => { + const client = createClient(); + + const hooks = createHooks(client); + + it('should be able to init hooks', async () => { + expect(hooks).toBeTruthy(); + }); +}); + +describe('React Query - useQuery', () => { + const client = createClient(); + + const queryCache = new QueryCache(); + const queryClient = new QueryClient({ queryCache }); + + beforeEach(() => { + queryClient.clear(); + nock.cleanAll(); + }); + + const { useQuery } = createHooks(client); + + it('should return data', async () => { + const scope = nockQuery() + .once() + .reply(200, { + data: { + id: '1', + }, + }); + + function Page() { + const { data, error } = useQuery({ + operationName: 'Weather', + }); + + return
Response: {data?.id}
; + } + + renderWithClient(, queryClient); + + await waitFor(() => { + screen.getByText('Response: 1'); + }); + + scope.done(); + }); + + it('should be disabled', async () => { + const scope = nockQuery().reply(200, { + data: { + id: '2', + }, + }); + + function Page() { + const { data, isFetched } = useQuery({ + operationName: 'Weather', + input: { + forCity: 'berlin', + }, + enabled: false, + }); + + return ( +
+
Fetched: {isFetched ? 'true' : 'false'}
+
+ ); + } + + renderWithClient(, queryClient); + + screen.getByText('Fetched: false'); + + await act(() => sleep(150)); + + screen.getByText('Fetched: false'); + + expect(() => scope.done()).toThrow(); + }); +}); + +describe('React Query - useMutation', () => { + const client = createClient(); + + const queryCache = new QueryCache(); + const queryClient = new QueryClient({ queryCache }); + + beforeEach(() => { + queryClient.clear(); + nock.cleanAll(); + }); + + const { useMutation, useQuery, queryKey } = createHooks(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', + }, + }); + + function Page() { + const { data, mutate } = useMutation({ + operationName: 'SetName', + }); + + React.useEffect(() => { + mutate({ name: 'Rick Astley' }); + }, []); + + return
{data?.id}
; + } + + renderWithClient(, queryClient); + + 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', + }, + }); + + function Page() { + const { data, mutate } = useMutation({ + operationName: 'SetNameWithoutAuth', + }); + + React.useEffect(() => { + mutate({ name: 'Rick Astley' }); + }, []); + + return
{data?.id}
; + } + + renderWithClient(, queryClient); + + 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/SetName', { 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' } }); + + function Page() { + const queryClient = useQueryClient(); + + const query = useQuery({ + operationName: 'Weather', + }); + + const { mutate } = useMutation({ + operationName: 'SetName', + onSuccess: (data, input) => { + queryClient.invalidateQueries(queryKey({ operationName: 'Weather' })); + }, + }); + + const onClick = () => { + mutate({ name: 'Rick Astley' }); + }; + + return ( +
+
{query.data?.name}
+ +
+ ); + } + + renderWithClient(, queryClient); + + await waitFor(() => { + screen.getByText('Test'); + }); + + fireEvent.click(screen.getByText('submit')); + + await waitFor(() => { + screen.getByText('Rick Astley'); + }); + + scope.done(); + }); +}); + +describe('React Query - useSubscription', () => { + const client = createClient(); + + const queryCache = new QueryCache(); + const queryClient = new QueryClient({ queryCache }); + + beforeEach(() => { + queryCache.clear(); + }); + + afterAll(() => { + queryCache.clear(); + }); + + const { useSubscription } = createHooks(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: '{}', wg_subscribe_once: 'true' }) + .reply(200, { data: { count: 100 } }); + + function Page() { + const { data } = useSubscription({ + operationName: 'Countdown', + subscribeOnce: true, + }); + + return
{data?.count ? data.count : 'loading'}
; + } + + renderWithClient(, queryClient); + + screen.getByText('loading'); + + await waitFor(() => { + screen.getByText('100'); + }); + + scope.done(); + }); +}); + +describe('React Query - useUser', () => { + const client = createClient(); + + const queryCache = new QueryCache(); + const queryClient = new QueryClient({ queryCache }); + + beforeEach(() => { + queryCache.clear(); + }); + + const { useUser } = createHooks(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/cookie/user') + .reply(200, { email: 'info@wundergraph.com' }); + + function Page() { + const { data, error } = useUser(); + + return
{data?.email}
; + } + + renderWithClient(, queryClient); + + await waitFor(() => { + screen.getByText('info@wundergraph.com'); + }); + + scope.done(); + }); +}); diff --git a/packages/react-query/tests/index.test-d.ts b/packages/react-query/tests/index.test-d.ts new file mode 100644 index 000000000..24130b8a5 --- /dev/null +++ b/packages/react-query/tests/index.test-d.ts @@ -0,0 +1,100 @@ +import { createHooks } from '../src/hooks'; +import { Client, OperationsDefinition, GraphQLResponseError, User } from '@wundergraph/sdk/client'; +import { expectType } from 'tsd'; +import { UseQueryResult } from '@tanstack/react-query'; +import { QueryKey } from '../src/types'; + +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 { useSubscription, useQuery, useMutation, useUser, queryKey } = createHooks( + new Client({ + baseURL: 'http://localhost:8080', + applicationHash: 'my-application-hash', + sdkVersion: '0.0.0', + }) +); + +const { data: queryData, error: queryError } = useQuery({ + enabled: true, + operationName: 'Weather', + input: { + city: 'Berlin', + }, +}); + +expectType(queryData); +expectType(queryError); + +const { data: subData, error: subError } = useSubscription({ + enabled: true, + subscribeOnce: true, + operationName: 'Weather', + input: { + forCity: 'Berlin', + }, +}); + +expectType(subData); +expectType(subError); + +const { + data: mutData, + error: mutError, + mutate, + mutateAsync, +} = useMutation({ + operationName: 'CreateUser', +}); + +expectType(mutData); +expectType(mutError); + +expectType( + mutate({ + name: 'John Doe', + }) +); + +expectType>( + mutateAsync({ + name: 'John Doe', + }) +); + +expectType, GraphQLResponseError>>(useUser()); +expectType, GraphQLResponseError>>( + useUser({ + revalidate: true, + abortSignal: new AbortController().signal, + }) +); + +expectType<('Weather' | { city: string } | undefined)[]>(queryKey({ operationName: 'Weather' })); diff --git a/packages/react-query/tsconfig.build.json b/packages/react-query/tsconfig.build.json new file mode 100644 index 000000000..bdc63200b --- /dev/null +++ b/packages/react-query/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*"] +} diff --git a/packages/react-query/tsconfig.json b/packages/react-query/tsconfig.json new file mode 100644 index 000000000..36dd1508f --- /dev/null +++ b/packages/react-query/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "target": "ES2019", + "module": "commonjs", + "moduleResolution": "node", + "declarationDir": "./dist", + "jsx": "react", + "outDir": "./dist" + }, + "include": ["src/**/*", "tests"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/sdk/package.json b/packages/sdk/package.json index bacd1a3ba..8359f42c6 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -63,13 +63,16 @@ "url": "https://github.com/wundergraph/wundergraph/issues" }, "tsd": { - "directory": "test-d" + "directory": "./test-d", + "compilerOptions": { + "typeRoots": [] + } }, "scripts": { "clean": "rimraf ./dist", "build": "tsc", "watch": "tsc -w", - "test": "jest", + "test": "jest && tsd", "test:int": "TEST_INTEGRATION=true jest" }, "devDependencies": { @@ -90,7 +93,8 @@ "nock": "^13.2.9", "node-fetch": "^2.6.7", "ts-jest": "^29.0.1", - "typescript": "^4.8.2" + "typescript": "^4.8.2", + "tsd": "^0.24.1" }, "dependencies": { "@fastify/formbody": "^7.3.0", diff --git a/packages/type-tests/test/client.test-d.ts b/packages/sdk/test-d/client.test-d.ts similarity index 94% rename from packages/type-tests/test/client.test-d.ts rename to packages/sdk/test-d/client.test-d.ts index d43a5bf1b..390896041 100644 --- a/packages/type-tests/test/client.test-d.ts +++ b/packages/sdk/test-d/client.test-d.ts @@ -1,11 +1,5 @@ import { expectType } from 'tsd'; -import { - Client, - ClientResponse, - OperationRequestOptions, - SubscriptionRequestOptions, - User, -} from '@wundergraph/sdk/client'; +import { Client, ClientResponse, OperationRequestOptions, SubscriptionRequestOptions, User } from '../src/client'; const client = new Client({ baseURL: 'https://api.com', diff --git a/packages/swr/README.md b/packages/swr/README.md index 5eac217e0..09bb513f4 100644 --- a/packages/swr/README.md +++ b/packages/swr/README.md @@ -10,7 +10,7 @@ SWR is a React Hooks library for data fetching. With just one hook, you can sign ## Getting Started ```shell -npm install @wundergraph/swr +npm install @wundergraph/swr swr@2.0.0-rc.0 ``` Before you can use the hooks, you need to modify your code generation to include the base typescript client. diff --git a/packages/swr/jest.config.js b/packages/swr/jest.config.js index d3beac922..558e4b2c8 100644 --- a/packages/swr/jest.config.js +++ b/packages/swr/jest.config.js @@ -1,6 +1,6 @@ module.exports = { testEnvironment: 'jsdom', - testRegex: '/src/.*\\.test\\.tsx?$', + testRegex: '/tests/.*\\.test\\.tsx?$', setupFilesAfterEnv: ['/jest-setup.ts'], transform: { '^.+\\.(t|j)sx?$': '@swc/jest', diff --git a/packages/swr/package.json b/packages/swr/package.json index 1720e339e..2bbc297d5 100644 --- a/packages/swr/package.json +++ b/packages/swr/package.json @@ -6,8 +6,11 @@ "main": "./dist/index.js", "types": "./dist/index.d.ts", "scripts": { - "build": "tsc", - "test": "jest" + "build": "tsc -p tsconfig.build.json", + "test": "jest && tsd" + }, + "tsd": { + "directory": "tests" }, "publishConfig": { "registry": "https://registry.npmjs.org", @@ -43,7 +46,8 @@ "react": "^18.1.0", "react-dom": "^18.1.0", "swr": "^2.0.0-rc.0", - "typescript": "^4.8.2" + "typescript": "^4.8.2", + "tsd": "^0.24.1" }, "homepage": "https://wundergraph.com", "author": { diff --git a/packages/swr/src/hooks.test.tsx b/packages/swr/tests/hooks.test.tsx similarity index 99% rename from packages/swr/src/hooks.test.tsx rename to packages/swr/tests/hooks.test.tsx index 896c27795..74643613f 100644 --- a/packages/swr/src/hooks.test.tsx +++ b/packages/swr/tests/hooks.test.tsx @@ -6,7 +6,7 @@ import { Client, ClientConfig } from '@wundergraph/sdk/client'; import nock from 'nock'; import fetch from 'node-fetch'; -import { createHooks } from './hooks'; +import { createHooks } from '../src/hooks'; export function sleep(time: number) { return new Promise((resolve) => setTimeout(resolve, time)); diff --git a/packages/type-tests/test/swr.test-d.ts b/packages/swr/tests/swr.test-d.ts similarity index 97% rename from packages/type-tests/test/swr.test-d.ts rename to packages/swr/tests/swr.test-d.ts index be49d8ddf..7b7de80dd 100644 --- a/packages/type-tests/test/swr.test-d.ts +++ b/packages/swr/tests/swr.test-d.ts @@ -1,7 +1,7 @@ -import { createHooks } from '@wundergraph/swr'; import { Client, OperationsDefinition, GraphQLResponseError, User } from '@wundergraph/sdk/client'; import { expectType } from 'tsd'; import { SWRResponse } from 'swr'; +import { createHooks } from '../src/hooks'; interface Operations extends OperationsDefinition { queries: { diff --git a/packages/swr/tsconfig.build.json b/packages/swr/tsconfig.build.json new file mode 100644 index 000000000..bdc63200b --- /dev/null +++ b/packages/swr/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*"] +} diff --git a/packages/swr/tsconfig.json b/packages/swr/tsconfig.json index f291ede93..17190b706 100644 --- a/packages/swr/tsconfig.json +++ b/packages/swr/tsconfig.json @@ -9,6 +9,6 @@ "outDir": "./dist", "typeRoots": ["./node_modules/@types", "./src/typedefinitions"] }, - "include": ["src/**/*"], + "include": ["src/**/*", "tests"], "exclude": ["node_modules", "dist"] } diff --git a/packages/type-tests/README.md b/packages/type-tests/README.md deleted file mode 100644 index efa3080af..000000000 --- a/packages/type-tests/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# @wundergraph/type-tests - -Tests to ensure correct types in the WunderGraph SDK and related packages. - -## Usage - -```sh -pnpm test -``` diff --git a/packages/type-tests/package.json b/packages/type-tests/package.json deleted file mode 100644 index bf81883a1..000000000 --- a/packages/type-tests/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "@wundergraph/type-tests", - "private": true, - "version": "0.0.0", - "types": "test", - "scripts": { - "test": "tsd" - }, - "keywords": [], - "author": "", - "license": "ISC", - "tsd": { - "directory": "test" - }, - "dependencies": { - "@wundergraph/sdk": "workspace:*", - "@wundergraph/swr": "workspace:*", - "swr": "^2.0.0-rc.0", - "tsd": "^0.24.1" - } -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 37f8c864e..6fba1e169 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -240,6 +240,46 @@ importers: cpy-cli: 4.2.0 typescript: 4.8.4 + packages/react-query: + specifiers: + '@swc/core': ^1.3.14 + '@swc/jest': ^0.2.23 + '@tanstack/react-query': ^4.16.1 + '@testing-library/jest-dom': ^5.16.5 + '@testing-library/react': ^13.4.0 + '@types/jest': ^28.1.1 + '@types/node-fetch': 2.6.2 + '@types/react': ^18.0.24 + '@types/react-dom': ^18.0.6 + '@wundergraph/sdk': workspace:* + jest: ^29.0.3 + jest-environment-jsdom: ^29.3.0 + nock: ^13.2.9 + node-fetch: 2.6.7 + react: 18.2.0 + react-dom: ^18.1.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/react-query': 4.16.1_biqbaboplfbrettd7655fr4n2y + '@testing-library/jest-dom': 5.16.5 + '@testing-library/react': 13.4.0_biqbaboplfbrettd7655fr4n2y + '@types/jest': 28.1.8 + '@types/node-fetch': 2.6.2 + '@types/react': 18.0.24 + '@types/react-dom': 18.0.8 + '@wundergraph/sdk': link:../sdk + jest: 29.3.1 + jest-environment-jsdom: 29.3.0 + nock: 13.2.9 + node-fetch: 2.6.7 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + tsd: 0.24.1 + typescript: 4.8.4 + packages/sdk: specifiers: '@fastify/formbody': ^7.3.0 @@ -287,6 +327,7 @@ importers: protobufjs: ^6.11.2 swagger2openapi: ^7.0.8 ts-jest: ^29.0.1 + tsd: ^0.24.1 typescript: ^4.8.2 dependencies: '@fastify/formbody': 7.3.0 @@ -335,6 +376,7 @@ importers: nock: 13.2.9 node-fetch: 2.6.7 ts-jest: 29.0.3_r7gdwuj75x2gz25bauak3xcnbm + tsd: 0.24.1 typescript: 4.8.4 packages/swr: @@ -356,6 +398,7 @@ importers: react-dom: ^18.1.0 react-ssr-prepass: ^1.5.0 swr: ^2.0.0-rc.0 + tsd: ^0.24.1 typescript: ^4.8.2 dependencies: react-ssr-prepass: 1.5.0_react@18.2.0 @@ -376,19 +419,8 @@ importers: react: 18.2.0 react-dom: 18.2.0_react@18.2.0 swr: 2.0.0-rc.0_react@18.2.0 - typescript: 4.8.4 - - packages/type-tests: - specifiers: - '@wundergraph/sdk': workspace:* - '@wundergraph/swr': workspace:* - swr: ^2.0.0-rc.0 - tsd: ^0.24.1 - dependencies: - '@wundergraph/sdk': link:../sdk - '@wundergraph/swr': link:../swr - swr: 2.0.0-rc.0 tsd: 0.24.1 + typescript: 4.8.4 packages/wunderctl: specifiers: @@ -459,9 +491,11 @@ importers: testapps/nextjs: specifiers: '@graphql-yoga/node': ^2.13.13 + '@tanstack/react-query': ^4.16.1 '@types/node': ^17.0.27 '@types/react': ^18.0.7 '@wundergraph/nextjs': workspace:* + '@wundergraph/react-query': workspace:^0.1.0 '@wundergraph/sdk': workspace:* '@wundergraph/swr': workspace:* concurrently: ^6.0.0 @@ -475,7 +509,9 @@ importers: wait-on: ^6.0.0 dependencies: '@graphql-yoga/node': 2.13.13_graphql@16.6.0 + '@tanstack/react-query': 4.16.1_biqbaboplfbrettd7655fr4n2y '@wundergraph/nextjs': link:../../packages/nextjs + '@wundergraph/react-query': link:../../packages/react-query '@wundergraph/sdk': link:../../packages/sdk '@wundergraph/swr': link:../../packages/swr next: 12.3.2_biqbaboplfbrettd7655fr4n2y @@ -670,6 +706,7 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@babel/highlight': 7.18.6 + dev: true /@babel/compat-data/7.20.1: resolution: {integrity: sha512-EWZ4mE2diW3QALKvDMiXnbZpRvlj+nayZ112nK93SnhqOtpdsbVD4W+2tEoT3YNBAG9RBR0ISY758ZkOgsn6pQ==} @@ -798,6 +835,7 @@ packages: /@babel/helper-validator-identifier/7.19.1: resolution: {integrity: sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==} engines: {node: '>=6.9.0'} + dev: true /@babel/helper-validator-option/7.18.6: resolution: {integrity: sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==} @@ -822,6 +860,7 @@ packages: '@babel/helper-validator-identifier': 7.19.1 chalk: 2.4.2 js-tokens: 4.0.0 + dev: true /@babel/parser/7.20.1: resolution: {integrity: sha512-hp0AYxaZJhxULfM1zyp7Wgr+pSUKBcP3M+PHnSzWGdXOzg/kHWIgiUWARvubhUKGOEw3xqY4x+lyZ9ytBVcELw==} @@ -3446,6 +3485,26 @@ packages: tailwindcss: 3.2.1 dev: false + /@tanstack/query-core/4.15.1: + resolution: {integrity: sha512-+UfqJsNbPIVo0a9ANW0ZxtjiMfGLaaoIaL9vZeVycvmBuWywJGtSi7fgPVMCPdZQFOzMsaXaOsDtSKQD5xLRVQ==} + + /@tanstack/react-query/4.16.1_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-PDE9u49wSDykPazlCoLFevUpceLjQ0Mm8i6038HgtTEKb/aoVnUZdlUP7C392ds3Cd75+EGlHU7qpEX06R7d9Q==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + dependencies: + '@tanstack/query-core': 4.15.1 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + use-sync-external-store: 1.2.0_react@18.2.0 + /@testing-library/dom/8.19.0: resolution: {integrity: sha512-6YWYPPpxG3e/xOo6HIWwB/58HukkwIVTOaZ0VwdMVjhRUX/01E4FtQbck9GazOOj7MXHc5RBzMrU86iBJHbI+A==} engines: {node: '>=12'} @@ -3521,7 +3580,7 @@ packages: /@tsd/typescript/4.8.4: resolution: {integrity: sha512-WMFNVstwWGyDuZP2LGPRZ+kPHxZLmhO+2ormstDvnXiyoBPtW1qq9XhhrkI4NVtxgs+2ZiUTl9AG7nNIRq/uCg==} - dev: false + dev: true /@typeform/embed-react/2.1.0_react@18.2.0: resolution: {integrity: sha512-usjuo7gVbf/GdcAUxfVV5f0/VbSikdJaw2wYkSSaWQFVrgFewZ3yowhjSuJCTcOsOKwAb2IrTB+kHvNHNrJMlQ==} @@ -3631,11 +3690,11 @@ packages: dependencies: '@types/estree': 1.0.0 '@types/json-schema': 7.0.11 - dev: false + dev: true /@types/estree/1.0.0: resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==} - dev: false + dev: true /@types/figlet/1.5.5: resolution: {integrity: sha512-0sMBeFoqdGgdXoR/hgKYSWMpFufSpToosNsI2VgmkPqZJgeEXsXNu2hGr0FN401dBro2tNO5y2D6uw3UxVaxbg==} @@ -3714,6 +3773,7 @@ packages: /@types/json-schema/7.0.11: resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==} + dev: true /@types/json5/0.0.29: resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} @@ -3750,6 +3810,7 @@ packages: /@types/minimist/1.2.2: resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==} + dev: true /@types/minipass/3.3.5: resolution: {integrity: sha512-M2BLHQdEmDmH671h0GIlOQQJrgezd1vNqq7PVj1VOsHZ2uQQb4iPiQIl0SlMdhxZPUsLIfEklmeEHXg8DJRewA==} @@ -3780,6 +3841,7 @@ packages: /@types/normalize-package-data/2.4.1: resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} + dev: true /@types/object-hash/1.3.4: resolution: {integrity: sha512-xFdpkAkikBgqBdG9vIlsqffDV8GpvnPEzs0IUtr1v3BEB97ijsFQ4RXVbUZwjFThhB4MDSTUfvmxUD5PGx0wXA==} @@ -4332,6 +4394,7 @@ packages: /array-union/2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} + dev: true /array.prototype.flat/1.3.1: resolution: {integrity: sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==} @@ -4367,6 +4430,7 @@ packages: /arrify/1.0.1: resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} engines: {node: '>=0.10.0'} + dev: true /arrify/3.0.0: resolution: {integrity: sha512-tLkvA81vQG/XqE2mjDkGQHoOINtMHtysSnemrmoGe6PydDPMRbVugqyk4A6V/WDWEfm3l+0d8anA9r8cv/5Jaw==} @@ -4848,6 +4912,7 @@ packages: camelcase: 5.3.1 map-obj: 4.3.0 quick-lru: 4.0.1 + dev: true /camelcase-keys/7.0.2: resolution: {integrity: sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg==} @@ -4862,6 +4927,7 @@ packages: /camelcase/5.3.1: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} engines: {node: '>=6'} + dev: true /camelcase/6.3.0: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} @@ -5277,8 +5343,8 @@ packages: engines: {node: '>=10'} hasBin: true dependencies: - is-text-path: 1.0.1 JSONStream: 1.3.5 + is-text-path: 1.0.1 lodash: 4.17.21 meow: 8.1.2 split2: 3.2.2 @@ -5547,10 +5613,12 @@ packages: dependencies: decamelize: 1.2.0 map-obj: 1.0.1 + dev: true /decamelize/1.2.0: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} engines: {node: '>=0.10.0'} + dev: true /decamelize/5.0.1: resolution: {integrity: sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA==} @@ -5768,6 +5836,7 @@ packages: engines: {node: '>=8'} dependencies: path-type: 4.0.0 + dev: true /dlv/1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} @@ -5904,6 +5973,7 @@ packages: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} dependencies: is-arrayish: 0.2.1 + dev: true /es-abstract/1.20.4: resolution: {integrity: sha512-0UtvRN79eMe2L+UNEF1BwRe364sj/DXhQ/k5FmivgoSdpM90b8Jc0mDzKMGo7QS0BVbOP/bTwBKNnDc9rNzaPA==} @@ -6274,7 +6344,7 @@ packages: plur: 4.0.0 string-width: 4.2.3 supports-hyperlinks: 2.3.0 - dev: false + dev: true /eslint-import-resolver-node/0.3.6: resolution: {integrity: sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==} @@ -6420,7 +6490,7 @@ packages: /eslint-rule-docs/1.1.235: resolution: {integrity: sha512-+TQ+x4JdTnDoFEXXb3fDvfGOwnyNV7duH8fXWTPD1ieaBmB8omj7Gw/pMBBu4uI2uJCCU8APDaQJzWuXnTsH4A==} - dev: false + dev: true /eslint-scope/7.1.1: resolution: {integrity: sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==} @@ -6838,6 +6908,7 @@ packages: dependencies: locate-path: 5.0.0 path-exists: 4.0.0 + dev: true /find-up/5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} @@ -7239,6 +7310,7 @@ packages: ignore: 5.2.0 merge2: 1.4.1 slash: 3.0.0 + dev: true /globby/13.1.2: resolution: {integrity: sha512-LKSDZXToac40u8Q1PQtZihbNdTYSNMuWe+K5l+oa6KgDzSvVrHXlJy40hUP522RjAIoNLJYBJi7ow+rbFpIhHQ==} @@ -7461,6 +7533,7 @@ packages: /hard-rejection/2.1.0: resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} engines: {node: '>=6'} + dev: true /has-bigints/1.0.2: resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} @@ -7512,12 +7585,14 @@ packages: /hosted-git-info/2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + dev: true /hosted-git-info/4.1.0: resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} engines: {node: '>=10'} dependencies: lru-cache: 6.0.0 + dev: true /hosted-git-info/5.2.1: resolution: {integrity: sha512-xIcQYMnhcx2Nr4JTjsFmwwnr9vldugPy9uVm0o87bjqqWMv9GaqsTeT+i99wTl0mk1uLxJtHxLb8kymqTENQsw==} @@ -7677,6 +7752,7 @@ packages: /ignore/5.2.0: resolution: {integrity: sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==} engines: {node: '>= 4'} + dev: true /import-fresh/3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} @@ -7708,6 +7784,7 @@ packages: /indent-string/4.0.0: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} + dev: true /indent-string/5.0.0: resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} @@ -7816,7 +7893,7 @@ packages: /irregular-plurals/3.3.0: resolution: {integrity: sha512-MVBLKUTangM3EfRPFROhmWQQKRDsrgI83J8GS3jXy+OwYqiR2/aoWndYQ5416jLE3uaGgLH7ncme3X9y09gZ3g==} engines: {node: '>=8'} - dev: false + dev: true /is-arguments/1.1.1: resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} @@ -7827,6 +7904,7 @@ packages: /is-arrayish/0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + dev: true /is-bigint/1.0.4: resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} @@ -7973,6 +8051,7 @@ packages: /is-plain-obj/1.1.0: resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} engines: {node: '>=0.10.0'} + dev: true /is-plain-obj/2.1.0: resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} @@ -8271,6 +8350,34 @@ packages: - ts-node dev: true + /jest-cli/29.3.1: + resolution: {integrity: sha512-TO/ewvwyvPOiBBuWZ0gm04z3WWP8TIK8acgPzE4IxgsLKQgb377NYGrQLc3Wl/7ndWzIH2CDNNsUjGxwLL43VQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.3.1 + '@jest/test-result': 29.3.1 + '@jest/types': 29.3.1 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.10 + import-local: 3.1.0 + jest-config: 29.3.1 + jest-util: 29.3.1 + jest-validate: 29.3.1 + prompts: 2.4.2 + yargs: 17.6.2 + transitivePeerDependencies: + - '@types/node' + - supports-color + - ts-node + dev: true + /jest-cli/29.3.1_@types+node@17.0.45: resolution: {integrity: sha512-TO/ewvwyvPOiBBuWZ0gm04z3WWP8TIK8acgPzE4IxgsLKQgb377NYGrQLc3Wl/7ndWzIH2CDNNsUjGxwLL43VQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -8443,6 +8550,44 @@ packages: - supports-color dev: true + /jest-config/29.3.1: + resolution: {integrity: sha512-y0tFHdj2WnTEhxmGUK1T7fgLen7YK4RtfvpLFBXfQkh2eMJAQq24Vx9472lvn5wg0MAO6B+iPfJfzdR9hJYalg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + dependencies: + '@babel/core': 7.19.6 + '@jest/test-sequencer': 29.3.1 + '@jest/types': 29.3.1 + babel-jest: 29.3.1_@babel+core@7.19.6 + chalk: 4.1.2 + ci-info: 3.5.0 + deepmerge: 4.2.2 + glob: 7.2.3 + graceful-fs: 4.2.10 + jest-circus: 29.3.1 + jest-environment-node: 29.3.1 + jest-get-type: 29.2.0 + jest-regex-util: 29.2.0 + jest-resolve: 29.3.1 + jest-runner: 29.3.1 + jest-util: 29.3.1 + jest-validate: 29.3.1 + micromatch: 4.0.5 + parse-json: 5.2.0 + pretty-format: 29.3.1 + slash: 3.0.0 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + dev: true + /jest-config/29.3.1_@types+node@17.0.45: resolution: {integrity: sha512-y0tFHdj2WnTEhxmGUK1T7fgLen7YK4RtfvpLFBXfQkh2eMJAQq24Vx9472lvn5wg0MAO6B+iPfJfzdR9hJYalg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -9133,6 +9278,26 @@ packages: - ts-node dev: true + /jest/29.3.1: + resolution: {integrity: sha512-6iWfL5DTT0Np6UYs/y5Niu7WIfNv/wRTtN5RSXt2DIEft3dx3zPuw/3WJQBCJfmEzvDiEKwoqMbGD9n49+qLSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.3.1 + '@jest/types': 29.3.1 + import-local: 3.1.0 + jest-cli: 29.3.1 + transitivePeerDependencies: + - '@types/node' + - supports-color + - ts-node + dev: true + /jest/29.3.1_@types+node@17.0.45: resolution: {integrity: sha512-6iWfL5DTT0Np6UYs/y5Niu7WIfNv/wRTtN5RSXt2DIEft3dx3zPuw/3WJQBCJfmEzvDiEKwoqMbGD9n49+qLSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -9261,6 +9426,7 @@ packages: /json-parse-even-better-errors/2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + dev: true /json-parse-even-better-errors/3.0.0: resolution: {integrity: sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA==} @@ -9401,6 +9567,7 @@ packages: /kind-of/6.0.3: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} + dev: true /klaw-sync/6.0.0: resolution: {integrity: sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==} @@ -9498,6 +9665,7 @@ packages: /lines-and-columns/1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + dev: true /lint-staged/12.5.0: resolution: {integrity: sha512-BKLUjWDsKquV/JuIcoQW4MSAI3ggwEImF1+sB4zaKvyVx1wBk3FsG7UK9bpnmBTN1pm7EH2BBcMwINJzCRv12g==} @@ -9600,6 +9768,7 @@ packages: engines: {node: '>=8'} dependencies: p-locate: 4.1.0 + dev: true /locate-path/6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} @@ -9838,10 +10007,12 @@ packages: /map-obj/1.0.1: resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} engines: {node: '>=0.10.0'} + dev: true /map-obj/4.3.0: resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} engines: {node: '>=8'} + dev: true /media-typer/0.3.0: resolution: {integrity: sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=} @@ -9899,7 +10070,7 @@ packages: trim-newlines: 3.0.1 type-fest: 0.18.1 yargs-parser: 20.2.9 - dev: false + dev: true /merge-stream/2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -9957,6 +10128,7 @@ packages: /min-indent/1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} + dev: true /minimatch/3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -9976,6 +10148,7 @@ packages: arrify: 1.0.1 is-plain-obj: 1.1.0 kind-of: 6.0.3 + dev: true /minimist/1.2.7: resolution: {integrity: sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==} @@ -10339,6 +10512,7 @@ packages: resolve: 1.22.1 semver: 5.7.1 validate-npm-package-license: 3.0.4 + dev: true /normalize-package-data/3.0.3: resolution: {integrity: sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==} @@ -10348,6 +10522,7 @@ packages: is-core-module: 2.11.0 semver: 7.3.8 validate-npm-package-license: 3.0.4 + dev: true /normalize-package-data/4.0.1: resolution: {integrity: sha512-EBk5QKKuocMJhB3BILuKhmaPjI8vNRSpIfO9woLC6NyHVkKKdVEdAO1mrT0ZfxNR1lKwCcTkuZfmGIFdizZ8Pg==} @@ -10878,6 +11053,7 @@ packages: engines: {node: '>=6'} dependencies: p-try: 2.2.0 + dev: true /p-limit/3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} @@ -10898,6 +11074,7 @@ packages: engines: {node: '>=8'} dependencies: p-limit: 2.3.0 + dev: true /p-locate/5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} @@ -10953,6 +11130,7 @@ packages: /p-try/2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} + dev: true /pac-proxy-agent/5.0.0: resolution: {integrity: sha512-CcFG3ZtnxO8McDigozwE3AqAw15zDvGH+OjXO4kzf7IkEKkQ4gxQ+3sdF50WmhQ4P/bVusXcqNE2S3XrNURwzQ==} @@ -11084,6 +11262,7 @@ packages: error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + dev: true /parse-path/7.0.0: resolution: {integrity: sha512-Euf9GG8WT9CdqwuWJGdf3RkUcTBArppHABkO7Lm8IzRQp0e2r/kkFnmhu4TSK30Wcu5rVAZLmfPKSBBi9tWFog==} @@ -11116,6 +11295,7 @@ packages: /path-exists/4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + dev: true /path-is-absolute/1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} @@ -11143,6 +11323,7 @@ packages: /path-type/4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + dev: true /path/0.12.7: resolution: {integrity: sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==} @@ -11333,7 +11514,7 @@ packages: engines: {node: '>=10'} dependencies: irregular-plurals: 3.3.0 - dev: false + dev: true /pluralize/7.0.0: resolution: {integrity: sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==} @@ -11820,6 +12001,7 @@ packages: /quick-lru/4.0.1: resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==} engines: {node: '>=8'} + dev: true /quick-lru/5.1.1: resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} @@ -11969,6 +12151,7 @@ packages: find-up: 4.1.0 read-pkg: 5.2.0 type-fest: 0.8.1 + dev: true /read-pkg-up/8.0.0: resolution: {integrity: sha512-snVCqPczksT0HS2EC+SxUndvSzn6LRCwpfSvLrIfR5BKDQQZMaI6jPRC9dYvYFDRAuFEAnkwww8kBBNE/3VvzQ==} @@ -11996,6 +12179,7 @@ packages: normalize-package-data: 2.5.0 parse-json: 5.2.0 type-fest: 0.6.0 + dev: true /read-pkg/6.0.0: resolution: {integrity: sha512-X1Fu3dPuk/8ZLsMhEj5f4wFAF0DWoK7qhGJvgaijocXxBmSToKfbFtqbxMO7bVjNA1dmE5huAzjXj/ey86iw9Q==} @@ -12087,6 +12271,7 @@ packages: dependencies: indent-string: 4.0.0 strip-indent: 3.0.0 + dev: true /redent/4.0.0: resolution: {integrity: sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==} @@ -12488,6 +12673,7 @@ packages: /slash/3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} + dev: true /slash/4.0.0: resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} @@ -12603,18 +12789,22 @@ packages: dependencies: spdx-expression-parse: 3.0.1 spdx-license-ids: 3.0.12 + dev: true /spdx-exceptions/2.3.0: resolution: {integrity: sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==} + dev: true /spdx-expression-parse/3.0.1: resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} dependencies: spdx-exceptions: 2.3.0 spdx-license-ids: 3.0.12 + dev: true /spdx-license-ids/3.0.12: resolution: {integrity: sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==} + dev: true /split/1.0.1: resolution: {integrity: sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==} @@ -12788,6 +12978,7 @@ packages: engines: {node: '>=8'} dependencies: min-indent: 1.0.1 + dev: true /strip-indent/4.0.0: resolution: {integrity: sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==} @@ -12902,7 +13093,7 @@ packages: dependencies: has-flag: 4.0.0 supports-color: 7.2.0 - dev: false + dev: true /supports-preserve-symlinks-flag/1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} @@ -12927,15 +13118,6 @@ packages: - encoding dev: false - /swr/2.0.0-rc.0: - resolution: {integrity: sha512-QOp+4Cqnb/uuLKeuRDh7aT+ws6wSWWKPqfyIpBXK8DM3IugOYeLO5v+390I0p1MIfRd0CQlAIJZBEgmHaTfDuA==} - engines: {pnpm: '7'} - peerDependencies: - react: ^16.11.0 || ^17.0.0 || ^18.0.0 - dependencies: - use-sync-external-store: 1.2.0 - dev: false - /swr/2.0.0-rc.0_react@18.2.0: resolution: {integrity: sha512-QOp+4Cqnb/uuLKeuRDh7aT+ws6wSWWKPqfyIpBXK8DM3IugOYeLO5v+390I0p1MIfRd0CQlAIJZBEgmHaTfDuA==} engines: {pnpm: '7'} @@ -13154,6 +13336,7 @@ packages: /trim-newlines/3.0.1: resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==} engines: {node: '>=8'} + dev: true /trim-newlines/4.0.2: resolution: {integrity: sha512-GJtWyq9InR/2HRiLZgpIKv+ufIKrVrvjQWEj7PxAXNc5dwbNJkqhAUoAGgzRmULAnoOM5EIpveYd3J2VeSAIew==} @@ -13302,7 +13485,7 @@ packages: meow: 9.0.0 path-exists: 4.0.0 read-pkg-up: 7.0.1 - dev: false + dev: true /tslib/1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} @@ -13347,6 +13530,7 @@ packages: /type-fest/0.18.1: resolution: {integrity: sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==} engines: {node: '>=10'} + dev: true /type-fest/0.20.2: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} @@ -13365,10 +13549,12 @@ packages: /type-fest/0.6.0: resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} engines: {node: '>=8'} + dev: true /type-fest/0.8.1: resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} engines: {node: '>=8'} + dev: true /type-fest/1.4.0: resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} @@ -13543,12 +13729,6 @@ packages: react: 18.2.0 dev: false - /use-sync-external-store/1.2.0: - resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - dev: false - /use-sync-external-store/1.2.0_react@18.2.0: resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} peerDependencies: @@ -13607,6 +13787,7 @@ packages: dependencies: spdx-correct: 3.1.1 spdx-expression-parse: 3.0.1 + dev: true /validate-npm-package-name/4.0.0: resolution: {integrity: sha512-mzR0L8ZDktZjpX4OB46KT+56MAhl4EIazWP/+G/HPGuvfdaqg4YsCdtOm6U9+LOFyYDoh4dpnpxZRB9MQQns5Q==} @@ -14005,6 +14186,7 @@ packages: /yargs-parser/20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} + dev: true /yargs-parser/21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} diff --git a/testapps/nextjs/lib/react-query.ts b/testapps/nextjs/lib/react-query.ts new file mode 100644 index 000000000..8cd8e022f --- /dev/null +++ b/testapps/nextjs/lib/react-query.ts @@ -0,0 +1,6 @@ +import { createHooks } from '@wundergraph/react-query'; +import { createClient, Operations } from '../components/generated/client'; +const client = createClient(); // Typesafe WunderGraph client + +export const { useQuery, useMutation, useSubscription, useUser, useFileUpload, useAuth, queryKey } = + createHooks(client); diff --git a/testapps/nextjs/next.config.js b/testapps/nextjs/next.config.js new file mode 100644 index 000000000..4e9dc8c1f --- /dev/null +++ b/testapps/nextjs/next.config.js @@ -0,0 +1,19 @@ +const path = require('path'); +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, + swcMinify: true, + webpack: (config, options) => { + // This is to make sure the React Query context is working correctly in the monorepo, since PNPM installs the react-query package twice. + if (options.isServer) { + config.externals = ['@tanstack/react-query', ...config.externals]; + } + + const reactQuery = path.resolve(require.resolve('@tanstack/react-query')); + + config.resolve.alias['@tanstack/react-query'] = reactQuery; + return config; + }, +}; + +module.exports = nextConfig; diff --git a/testapps/nextjs/package.json b/testapps/nextjs/package.json index fa0a36dc6..a83eef00f 100755 --- a/testapps/nextjs/package.json +++ b/testapps/nextjs/package.json @@ -27,7 +27,9 @@ }, "dependencies": { "@graphql-yoga/node": "^2.13.13", + "@tanstack/react-query": "^4.16.1", "@wundergraph/nextjs": "workspace:*", + "@wundergraph/react-query": "workspace:^0.1.0", "@wundergraph/sdk": "workspace:*", "@wundergraph/swr": "workspace:*", "next": "^12.1.6", diff --git a/testapps/nextjs/pages/_app.tsx b/testapps/nextjs/pages/_app.tsx index da9799b74..d425702df 100755 --- a/testapps/nextjs/pages/_app.tsx +++ b/testapps/nextjs/pages/_app.tsx @@ -1,7 +1,12 @@ import '../styles/globals.css'; - +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +const queryClient = new QueryClient(); function MyApp({ Component, pageProps }) { - return ; + return ( + + + + ); } export default MyApp; diff --git a/testapps/nextjs/pages/react-query/authentication.tsx b/testapps/nextjs/pages/react-query/authentication.tsx new file mode 100755 index 000000000..7e4c5c88d --- /dev/null +++ b/testapps/nextjs/pages/react-query/authentication.tsx @@ -0,0 +1,93 @@ +import { NextPage } from 'next'; +import styles from '../../styles/Home.module.css'; +import { FC, useState } from 'react'; +import { useQuery, useUser, useAuth } from '../../lib/react-query'; + +const JobsPage: NextPage = () => { + const user = useUser(); + const { login, logout } = useAuth(); + const [city, setCity] = useState('Berlin'); + const onClick = () => { + if (!user.data) { + login('github'); + } else { + logout(); + } + }; + + return ( +
+

Authentication aware Data Fetching

+

+ This Example looks similar to Realtime Subscriptions with one exception, users have to be authenticated to be + able to use this Operation. +

+

Click the Login Button to login using GitHub. We provide a GitHub demo Oauth2 account for this to work.

+

While not logged in, the LiveQuery will reset and wait until the user is logged in.

+

Once you logged in, the LiveQuery will start streaming until you de-focus the browser tab or log out.

+

+ When you re-focus the tab, the LiveQuery will start streaming again. You can open the DevTools / Network tab to + observe this. +

+

+ Next, you can try something. First, make sure you're logged in. Then open a second tab and open the same url. + Log yourself out in the second tab. Then come back to the first tab and see what happens once you focus the tab. + =) +

+

+ Finally, log in again and go back to the second tab. This is what we call{' '} + + authentication aware data fetching + +

+

+ You can change the authentication configuration at{' '} + .wundergraph/wundergraph.operations.ts:54 +

+

+ Learn more on how to configure your Operations:{' '} + + Operations Configuration Docs + +

+

Login State

+
{user.data ?

{JSON.stringify(user)}

:

Not logged in

}
+ +

Enter City Search

+ setCity(e.target.value)} /> + +
+ ); +}; + +const ProtectedLiveWeather: FC<{ city: string }> = ({ city }) => { + const liveWeather = useQuery({ + operationName: 'ProtectedWeather', + input: { forCity: city }, + liveQuery: true, + }); + return ( +
+ {liveWeather.isLoading &&

Loading...

} + {liveWeather.error &&

Error

} + {liveWeather.data && ( +
+

City: {liveWeather.data.getCityByName?.name}

+

{JSON.stringify(liveWeather.data.getCityByName?.coord)}

+

Temperature

+

{JSON.stringify(liveWeather.data.getCityByName?.weather?.temperature)}

+

Wind

+

{JSON.stringify(liveWeather.data.getCityByName?.weather?.wind)}

+
+ )} +
+ ); +}; + +export default JobsPage; diff --git a/testapps/nextjs/pages/react-query/caching.tsx b/testapps/nextjs/pages/react-query/caching.tsx new file mode 100755 index 000000000..44d142266 --- /dev/null +++ b/testapps/nextjs/pages/react-query/caching.tsx @@ -0,0 +1,49 @@ +import { NextPage } from 'next'; +import styles from '../../styles/Home.module.css'; +import { useQuery } from '../../lib/react-query'; + +const JobsPage: NextPage = () => { + const launches = useQuery({ + operationName: 'PastLaunches', + }); + return ( +
+

Cached SpaceX rocket launches

+

+ Have a look at the Network Tab / Devtools, the response is cached up to 120 seconds whereas after 60 seconds, + the Browser cache will be invalidated through stale while revalidate. +

+

+ Cache- & revalidation timing can be edited in{' '} + .wundergraph/wundergraph.operations.ts:36 +

+

+ Caching is enabled in .wundergraph/wundergraph.operations.ts:55 +

+

+ The use of the generated client can be found at pages/caching.tsx:6 +

+

+ Additionally, if you re-focus the Browser window/tab you'll see a network request kick off to refresh the page. +

+
    + {launches.data?.spacex_launchesPast?.map((launch, i) => ( +
  • +

    {launch.mission_name}

    +

    {launch.rocket?.rocket_name}

    +

    {launch.launch_site?.site_name_long}

    + + Article + +    + + Video + +
  • + ))} +
+
+ ); +}; + +export default JobsPage; diff --git a/testapps/nextjs/pages/react-query/index.tsx b/testapps/nextjs/pages/react-query/index.tsx new file mode 100755 index 000000000..17a5198fa --- /dev/null +++ b/testapps/nextjs/pages/react-query/index.tsx @@ -0,0 +1,84 @@ +import Head from 'next/head'; +import styles from '../../styles/Home.module.css'; +import NextLink from 'next/link'; + +export default function Home() { + return ( +
+ + Create Next App + + +
+

+ Welcome to Next.js! +

+

+ ... with WunderGraph +

+

Take a look at the examples below...

+
+ +
+

Caching →

+

Example using WunderGraph Caching

+
+
+ +
+

Typesafe Mocking →

+

WunderGraph allows your do mock any API with type safety.

+
+
+ +
+

Realtime Subscriptions →

+

Turn any API into a Realtime Subscription

+
+
+ +
+

Authentication →

+

Authentication aware Data Fetching

+
+
+ +
+

File uploading →

+

Upload files to a S3 compatible server

+
+
+ +

Docs →

+

Read the full Getting Started Guide

+
+ +

Feedback →

+

We'd love to hear from you! Join us on Discord and Chat with us.

+
+
+ +

Book a Meeting with the Makers of WunderGraph →

+

Talk to the Founders, learn more about our tool and let us help you find the right solution for you.

+
+
+ +
+ Powered by{' '} + + Vercel Logo + +   and  + WunderGraph Logo +
+
+ ); +} diff --git a/testapps/nextjs/pages/react-query/mocks.tsx b/testapps/nextjs/pages/react-query/mocks.tsx new file mode 100755 index 000000000..bb9d29bd3 --- /dev/null +++ b/testapps/nextjs/pages/react-query/mocks.tsx @@ -0,0 +1,46 @@ +import { NextPage } from 'next'; +import styles from '../../styles/Home.module.css'; +import { useQuery } from '../../lib/react-query'; + +const Mocks: NextPage = () => { + const { data } = useQuery({ + operationName: 'FakeWeather', + }); + + return ( +
+

Mocks: Fake Weather

+

With WunderGraph, it's very easy to create typesafe mocks.

+

+ For each Operation you define, the code generator automatically generates all the models and scaffolds a config + object to create typesafe mocks. +

+

All you have to do is implement a function that returns a mock object.

+

+ You can define complex logic if you want, or use an in memory data structure or even a database if you want + stateful mocks. +

+

+ To modify the mock, look at  + wundergraph.server.ts:6 +

+

+ The use of the method from the clients' perspective can be found at  + pages/mocks.ts:6 +

+

Try changing the implementation and update the UI.

+

+ Learn more about Mocking:{' '} + + Mocking Documentation + +

+

Response

+

{JSON.stringify(data)}

+
+ ); +}; +export default Mocks; diff --git a/testapps/nextjs/pages/react-query/realtime.tsx b/testapps/nextjs/pages/react-query/realtime.tsx new file mode 100755 index 000000000..d279428d5 --- /dev/null +++ b/testapps/nextjs/pages/react-query/realtime.tsx @@ -0,0 +1,61 @@ +import { NextPage } from 'next'; +import styles from '../../styles/Home.module.css'; +import { FC, useState } from 'react'; +import { useQuery } from '../../lib/react-query'; + +const RealtimePage: NextPage = () => { + const [city, setCity] = useState('Berlin'); + return ( +
+

Live Weather

+

+ If you watch the Network Tab / DevTools, you'll see no WebSockets, No Subscriptions, No Polling, just a GET + request with chunked encoding (HTTP 1.1) or a stream (HTTP/2). +

+

If you blur (un-focus) the browser window/tab you'll see that the stream ends.

+

Once you re-enter the window, the stream re-starts and keepts the UI updated.

+

+ The upstream doesn't support Subscriptions or Realtime Updates. WunderGraph polls the upstream on the serverside + and distributed the response to the clients. +

+

+ You can change the polling interval by adjusting the "liveQuery" config in at{' '} + .wundergraph/wundergraph.operations.ts:44 +

+

+ Learn more about Realtime Subscriptions:{' '} + + Realtime Subscriptions Overview + +

+

Enter City Search

+ setCity(e.target.value)} /> + +
+ ); +}; + +const LiveWeather: FC<{ city: string }> = ({ city }) => { + const liveWeather = useQuery({ + operationName: 'Weather', + input: { forCity: city }, + liveQuery: true, + }); + return ( +
+
+

City: {liveWeather.data?.getCityByName?.name}

+

{JSON.stringify(liveWeather.data?.getCityByName?.coord)}

+

Temperature

+

{JSON.stringify(liveWeather.data?.getCityByName?.weather?.temperature)}

+

Wind

+

{JSON.stringify(liveWeather.data?.getCityByName?.weather?.wind)}

+
+
+ ); +}; + +export default RealtimePage; diff --git a/testapps/nextjs/pages/react-query/upload.tsx b/testapps/nextjs/pages/react-query/upload.tsx new file mode 100755 index 000000000..5a55f1ebe --- /dev/null +++ b/testapps/nextjs/pages/react-query/upload.tsx @@ -0,0 +1,55 @@ +import { NextPage } from 'next'; +import { useState } from 'react'; +import styles from '../../styles/Home.module.css'; +import { useFileUpload } from '../../lib/react-query'; + +const UploadPage: NextPage = () => { + const [files, setFiles] = useState(); + const [data, setData] = useState([]); + const { uploadAsync } = useFileUpload({}); + const onFileChange = (e: React.ChangeEvent) => { + if (e.target.files) setFiles(e.target.files); + }; + const onSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!files) { + return; + } + try { + const result = await uploadAsync({ + provider: 'minio', + files, + }); + result && setData(result); + } catch (e) { + console.error("Couldn't upload files", e); + } + }; + + return ( +
+

Upload multiple files to any S3 compatible file server

+

+ To enable file uploads cd into minio, run docker-compose up and then{' '} + sh setup.sh to start your own S3 server using docker compose & minio. +

+
+
+ + +
+ +
+
+ ); +}; + +export default UploadPage;