From a753fdbdd0c78ae0ece9579b7a34dfb488b9f6a6 Mon Sep 17 00:00:00 2001 From: Tadeusz Aleksandruk <t.aleksandruk@gmail.com> Date: Wed, 15 Jun 2022 16:15:46 +0200 Subject: [PATCH 1/4] Adding sandbox with creds includes, but from my own account --- docs/rtk-query/usage/examples.mdx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/rtk-query/usage/examples.mdx b/docs/rtk-query/usage/examples.mdx index 55674e43cf..0e18e790b0 100644 --- a/docs/rtk-query/usage/examples.mdx +++ b/docs/rtk-query/usage/examples.mdx @@ -76,6 +76,22 @@ The example has some intentionally wonky behavior... when editing the name of a sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin" ></iframe> +## React with GraphQL with custom client + +<iframe + src="https://codesandbox.io/s/determined-shannon-iv2p5x" + style={{ + width: '100%', + height: '800px', + border: 0, + borderRadius: '4px', + overflow: 'hidden', + }} + title="RTK Query GraphQL Example with custom client" + allow="geolocation; microphone; camera; midi; vr; accelerometer; gyroscope; payment; ambient-light-sensor; encrypted-media; usb" + sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin" +></iframe> + ## Authentication There are several ways to handle authentication with RTK Query. This is a very basic example of taking a JWT from a login mutation, then setting that in our store. We then use `prepareHeaders` to inject the authentication headers into every subsequent request. From 23407b66786c7b70d514b3edbe314c2362c3908f Mon Sep 17 00:00:00 2001 From: AleksandrukTad <t.aleksandruk@gmail.com> Date: Wed, 15 Jun 2022 16:23:59 +0200 Subject: [PATCH 2/4] fixing section name --- docs/rtk-query/usage/examples.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rtk-query/usage/examples.mdx b/docs/rtk-query/usage/examples.mdx index 0e18e790b0..d2964e8958 100644 --- a/docs/rtk-query/usage/examples.mdx +++ b/docs/rtk-query/usage/examples.mdx @@ -76,7 +76,7 @@ The example has some intentionally wonky behavior... when editing the name of a sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin" ></iframe> -## React with GraphQL with custom client +## React GraphQL with custom client <iframe src="https://codesandbox.io/s/determined-shannon-iv2p5x" From db3ae4e725520844b12da3093944ee80786c4afa Mon Sep 17 00:00:00 2001 From: Aleksandruk <e-tzau@gft.com> Date: Mon, 13 Feb 2023 13:23:49 +0100 Subject: [PATCH 3/4] Moving example to examples folder - graphql-custom-client --- docs/rtk-query/usage/examples.mdx | 2 +- .../query/react/graphql-custom-client/.env | 3 + .../react/graphql-custom-client/package.json | 55 +++ .../graphql-custom-client/public/index.html | 44 +++ .../public/manifest.json | 8 + .../public/mockServiceWorker.js | 323 ++++++++++++++++++ .../react/graphql-custom-client/src/App.tsx | 12 + .../src/app/services/posts.ts | 83 +++++ .../src/features/posts/PostsManager.tsx | 110 ++++++ .../react/graphql-custom-client/src/index.tsx | 26 ++ .../src/mocks/browser.ts | 4 + .../graphql-custom-client/src/mocks/db.ts | 71 ++++ .../src/react-app-env.d.ts | 1 + .../react/graphql-custom-client/tsconfig.json | 25 ++ 14 files changed, 766 insertions(+), 1 deletion(-) create mode 100644 examples/query/react/graphql-custom-client/.env create mode 100644 examples/query/react/graphql-custom-client/package.json create mode 100644 examples/query/react/graphql-custom-client/public/index.html create mode 100644 examples/query/react/graphql-custom-client/public/manifest.json create mode 100644 examples/query/react/graphql-custom-client/public/mockServiceWorker.js create mode 100644 examples/query/react/graphql-custom-client/src/App.tsx create mode 100644 examples/query/react/graphql-custom-client/src/app/services/posts.ts create mode 100644 examples/query/react/graphql-custom-client/src/features/posts/PostsManager.tsx create mode 100644 examples/query/react/graphql-custom-client/src/index.tsx create mode 100644 examples/query/react/graphql-custom-client/src/mocks/browser.ts create mode 100644 examples/query/react/graphql-custom-client/src/mocks/db.ts create mode 100644 examples/query/react/graphql-custom-client/src/react-app-env.d.ts create mode 100644 examples/query/react/graphql-custom-client/tsconfig.json diff --git a/docs/rtk-query/usage/examples.mdx b/docs/rtk-query/usage/examples.mdx index d2964e8958..867abd578b 100644 --- a/docs/rtk-query/usage/examples.mdx +++ b/docs/rtk-query/usage/examples.mdx @@ -79,7 +79,7 @@ The example has some intentionally wonky behavior... when editing the name of a ## React GraphQL with custom client <iframe - src="https://codesandbox.io/s/determined-shannon-iv2p5x" + src="https://codesandbox.io/embed/github/reduxjs/redux-toolkit/tree/master/examples/query/reactgraphql-custom-client?fontsize=12&hidenavigation=1&theme=dark&module=%2Fsrc%2Fapi.js&runonclick=" style={{ width: '100%', height: '800px', diff --git a/examples/query/react/graphql-custom-client/.env b/examples/query/react/graphql-custom-client/.env new file mode 100644 index 0000000000..e73ecc6cd4 --- /dev/null +++ b/examples/query/react/graphql-custom-client/.env @@ -0,0 +1,3 @@ +SKIP_PREFLIGHT_CHECK=true +# https://github.com/facebook/create-react-app/issues/11940 +DISABLE_ESLINT_PLUGIN=true diff --git a/examples/query/react/graphql-custom-client/package.json b/examples/query/react/graphql-custom-client/package.json new file mode 100644 index 0000000000..59d3e5f104 --- /dev/null +++ b/examples/query/react/graphql-custom-client/package.json @@ -0,0 +1,55 @@ +{ + "name": "@examples-query-react/graphql-custom-client", + "private": true, + "version": "1.0.0", + "description": "", + "keywords": [], + "main": "./src/index.tsx", + "dependencies": { + "@chakra-ui/react": "1.0.0", + "@emotion/react": "^11.4.0", + "@emotion/styled": "^11.3.0", + "@mswjs/data": "^0.3.0", + "@reduxjs/toolkit": "^1.6.0-rc.1", + "@rtk-query/graphql-request-base-query": "^2.0.0", + "faker": "^5.5.3", + "framer-motion": "^2.9.5", + "graphql": "^15.5.0", + "graphql-request": "^3.4.0", + "msw": "0.28.2", + "react": "^18.1.0", + "react-dom": "^18.1.0", + "react-icons": "3.11.0", + "react-redux": "^8.0.2", + "react-router-dom": "6.3.0", + "react-scripts": "5.0.1" + }, + "devDependencies": { + "@types/faker": "^5.5.5", + "@types/react": "^18.0.5", + "@types/react-dom": "^18.0.5", + "typescript": "~4.2.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "prebuild": "rm -rf dist/" + }, + "eslintConfig": { + "extends": [ + "react-app" + ], + "rules": { + "react/react-in-jsx-scope": "off" + } + }, + "browserslist": [ + ">0.2%", + "not dead", + "not ie <= 11", + "not op_mini all" + ], + "msw": { + "workerDirectory": "public" + } +} diff --git a/examples/query/react/graphql-custom-client/public/index.html b/examples/query/react/graphql-custom-client/public/index.html new file mode 100644 index 0000000000..09e975e218 --- /dev/null +++ b/examples/query/react/graphql-custom-client/public/index.html @@ -0,0 +1,44 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8" /> + <link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <meta name="theme-color" content="#000000" /> + <meta + name="description" + content="Web site created using create-react-app" + /> + <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> + <!-- + manifest.json provides metadata used when your web app is installed on a + user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ + --> + <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> + <!-- + Notice the use of %PUBLIC_URL% in the tags above. + It will be replaced with the URL of the `public` folder during the build. + Only files inside the `public` folder can be referenced from the HTML. + + Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will + work correctly both with client-side routing and a non-root public URL. + Learn how to configure a non-root public URL by running `npm run build`. + --> + <title>React App</title> + </head> + <body> + <noscript>You need to enable JavaScript to run this app.</noscript> + <div id="root"></div> + <!-- + This HTML file is a template. + If you open it directly in the browser, you will see an empty page. + + You can add webfonts, meta tags, or analytics to this file. + The build step will place the bundled scripts into the <body> tag. + + To begin the development, run `npm start` or `yarn start`. + To create a production bundle, use `npm run build` or `yarn build`. + --> + </body> +</html> + diff --git a/examples/query/react/graphql-custom-client/public/manifest.json b/examples/query/react/graphql-custom-client/public/manifest.json new file mode 100644 index 0000000000..11c6e14f7c --- /dev/null +++ b/examples/query/react/graphql-custom-client/public/manifest.json @@ -0,0 +1,8 @@ +{ + "short_name": "RTK Query Pagination Example", + "name": "Pagination Demo", + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/examples/query/react/graphql-custom-client/public/mockServiceWorker.js b/examples/query/react/graphql-custom-client/public/mockServiceWorker.js new file mode 100644 index 0000000000..fa1e5f5319 --- /dev/null +++ b/examples/query/react/graphql-custom-client/public/mockServiceWorker.js @@ -0,0 +1,323 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker (0.30.1). + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + * - Please do NOT serve this file on production. + */ + +const INTEGRITY_CHECKSUM = '82ef9b96d8393b6da34527d1d6e19187' +const bypassHeaderName = 'x-msw-bypass' +const activeClientIds = new Set() + +self.addEventListener('install', function () { + return self.skipWaiting() +}) + +self.addEventListener('activate', async function (event) { + return self.clients.claim() +}) + +self.addEventListener('message', async function (event) { + const clientId = event.source.id + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll() + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE', + }) + break + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: INTEGRITY_CHECKSUM, + }) + break + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId) + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: true, + }) + break + } + + case 'MOCK_DEACTIVATE': { + activeClientIds.delete(clientId) + break + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +// Resolve the "master" client for the given event. +// Client that issues a request doesn't necessarily equal the client +// that registered the worker. It's with the latter the worker should +// communicate with during the response resolving phase. +async function resolveMasterClient(event) { + const client = await self.clients.get(event.clientId) + + if (client.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll() + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +async function handleRequest(event, requestId) { + const client = await resolveMasterClient(event) + const response = await getResponse(event, client, requestId) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + ;(async function () { + const clonedResponse = response.clone() + sendToClient(client, { + type: 'RESPONSE', + payload: { + requestId, + type: clonedResponse.type, + ok: clonedResponse.ok, + status: clonedResponse.status, + statusText: clonedResponse.statusText, + body: + clonedResponse.body === null ? null : await clonedResponse.text(), + headers: serializeHeaders(clonedResponse.headers), + redirected: clonedResponse.redirected, + }, + }) + })() + } + + return response +} + +async function getResponse(event, client, requestId) { + const { request } = event + const requestClone = request.clone() + const getOriginalResponse = () => fetch(requestClone) + + // Bypass mocking when the request client is not active. + if (!client) { + return getOriginalResponse() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return await getOriginalResponse() + } + + // Bypass requests with the explicit bypass header + if (requestClone.headers.get(bypassHeaderName) === 'true') { + const cleanRequestHeaders = serializeHeaders(requestClone.headers) + + // Remove the bypass header to comply with the CORS preflight check. + delete cleanRequestHeaders[bypassHeaderName] + + const originalRequest = new Request(requestClone, { + headers: new Headers(cleanRequestHeaders), + }) + + return fetch(originalRequest) + } + + // Send the request to the client-side MSW. + const reqHeaders = serializeHeaders(request.headers) + const body = await request.text() + + const clientMessage = await sendToClient(client, { + type: 'REQUEST', + payload: { + id: requestId, + url: request.url, + method: request.method, + headers: reqHeaders, + cache: request.cache, + mode: request.mode, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body, + bodyUsed: request.bodyUsed, + keepalive: request.keepalive, + }, + }) + + switch (clientMessage.type) { + case 'MOCK_SUCCESS': { + return delayPromise( + () => respondWithMock(clientMessage), + clientMessage.payload.delay, + ) + } + + case 'MOCK_NOT_FOUND': { + return getOriginalResponse() + } + + case 'NETWORK_ERROR': { + const { name, message } = clientMessage.payload + const networkError = new Error(message) + networkError.name = name + + // Rejecting a request Promise emulates a network error. + throw networkError + } + + case 'INTERNAL_ERROR': { + const parsedBody = JSON.parse(clientMessage.payload.body) + + console.error( + `\ +[MSW] Request handler function for "%s %s" has thrown the following exception: + +${parsedBody.errorType}: ${parsedBody.message} +(see more detailed error stack trace in the mocked response body) + +This exception has been gracefully handled as a 500 response, however, it's strongly recommended to resolve this error. +If you wish to mock an error response, please refer to this guide: https://mswjs.io/docs/recipes/mocking-error-responses\ +`, + request.method, + request.url, + ) + + return respondWithMock(clientMessage) + } + } + + return getOriginalResponse() +} + +self.addEventListener('fetch', function (event) { + const { request } = event + + // Bypass navigation requests. + if (request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been deleted (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + const requestId = uuidv4() + + return event.respondWith( + handleRequest(event, requestId).catch((error) => { + console.error( + '[MSW] Failed to mock a "%s" request to "%s": %s', + request.method, + request.url, + error, + ) + }), + ) +}) + +function serializeHeaders(headers) { + const reqHeaders = {} + headers.forEach((value, name) => { + reqHeaders[name] = reqHeaders[name] + ? [].concat(reqHeaders[name]).concat(value) + : value + }) + return reqHeaders +} + +function sendToClient(client, message) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage(JSON.stringify(message), [channel.port2]) + }) +} + +function delayPromise(cb, duration) { + return new Promise((resolve) => { + setTimeout(() => resolve(cb()), duration) + }) +} + +function respondWithMock(clientMessage) { + return new Response(clientMessage.payload.body, { + ...clientMessage.payload, + headers: clientMessage.payload.headers, + }) +} + +function uuidv4() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + const r = (Math.random() * 16) | 0 + const v = c == 'x' ? r : (r & 0x3) | 0x8 + return v.toString(16) + }) +} diff --git a/examples/query/react/graphql-custom-client/src/App.tsx b/examples/query/react/graphql-custom-client/src/App.tsx new file mode 100644 index 0000000000..d2c53f79bc --- /dev/null +++ b/examples/query/react/graphql-custom-client/src/App.tsx @@ -0,0 +1,12 @@ +import { Route, Routes } from 'react-router-dom' +import { PostsManager } from './features/posts/PostsManager' + +function App() { + return ( + <Routes> + <Route path="/" element={<PostsManager />} /> + </Routes> + ) +} + +export default App diff --git a/examples/query/react/graphql-custom-client/src/app/services/posts.ts b/examples/query/react/graphql-custom-client/src/app/services/posts.ts new file mode 100644 index 0000000000..f3e40f24f0 --- /dev/null +++ b/examples/query/react/graphql-custom-client/src/app/services/posts.ts @@ -0,0 +1,83 @@ +import { createApi } from '@reduxjs/toolkit/query/react' +import { gql } from 'graphql-request' +import {graphqlRequestBaseQuery} from '@rtk-query/graphql-request-base-query' +import { GraphQLClient } from "graphql-request"; + +export const postStatuses = ['draft', 'published', 'pending_review'] as const + +export interface Post { + id: string + title: string + author: string + content: string + status: typeof postStatuses[number] + created_at: string + updated_at: string +} + +export interface Pagination { + page: number + per_page: number + total: number + total_pages: number +} + +export interface GetPostsResponse extends Pagination { + data: { + posts: Post[] + } +} + +interface PostResponse { + data: { + post: Post + } +} + +const client = new GraphQLClient("/graphql", { + credentials: "include" +}); + +export const api = createApi({ + baseQuery: graphqlRequestBaseQuery({ + client, + url: '/graphql', + }), + endpoints: (builder) => ({ + getPosts: builder.query< + GetPostsResponse, + { page?: number; per_page?: number } + >({ + query: ({ page, per_page }) => ({ + document: gql` + query GetPosts($page: Int = 1, $per_page: Int = 10) { + posts(page: $page, per_page: $per_page) { + id + title + } + } + `, + variables: { + page, + per_page, + }, + }), + }), + getPost: builder.query<Post, string>({ + query: (id) => ({ + document: gql` + query GetPost($id: ID!) { + post(id: ${id}) { + id + title + body + } + } + `, + }), + transformResponse: (response: PostResponse) => response.data.post, + }), + }), +}) + +export const { useGetPostsQuery, useGetPostQuery } = api diff --git a/examples/query/react/graphql-custom-client/src/features/posts/PostsManager.tsx b/examples/query/react/graphql-custom-client/src/features/posts/PostsManager.tsx new file mode 100644 index 0000000000..09496ee6b8 --- /dev/null +++ b/examples/query/react/graphql-custom-client/src/features/posts/PostsManager.tsx @@ -0,0 +1,110 @@ +import * as React from 'react' +import { + Badge, + Box, + Button, + Divider, + Flex, + Heading, + HStack, + Icon, + List, + ListIcon, + ListItem, + Spacer, + Stat, + StatLabel, + StatNumber, +} from '@chakra-ui/react' +import { MdArrowBack, MdArrowForward, MdBook } from 'react-icons/md' +import { Post, useGetPostsQuery } from '../../app/services/posts' + +const getColorForStatus = (status: Post['status']) => { + return status === 'draft' + ? 'gray' + : status === 'pending_review' + ? 'orange' + : 'green' +} + +const PostList = () => { + const [page, setPage] = React.useState(1) + const { data: posts, isLoading, isFetching } = useGetPostsQuery({ page }) + + if (isLoading) { + return <div>Loading</div> + } + + if (!posts?.data) { + return <div>No posts :(</div> + } + + return ( + <Box> + <HStack spacing="14px"> + <Button + onClick={() => setPage((prev) => prev - 1)} + isLoading={isFetching} + disabled={page === 1} + > + <Icon as={MdArrowBack} /> + </Button> + <Button + onClick={() => setPage((prev) => prev + 1)} + isLoading={isFetching} + disabled={page === posts.total_pages} + > + <Icon as={MdArrowForward} /> + </Button> + <Box>{`${page} / ${posts.total_pages}`}</Box> + </HStack> + <List spacing={3} mt={6}> + {posts?.data.posts.map(({ id, title, status }) => ( + <ListItem key={id}> + <ListIcon as={MdBook} color="green.500" /> {title}{' '} + <Badge + ml="1" + fontSize="0.8em" + colorScheme={getColorForStatus(status)} + > + {status} + </Badge> + </ListItem> + ))} + </List> + </Box> + ) +} + +export const PostsCountStat = () => { + const { data: posts } = useGetPostsQuery({}) + + return ( + <Stat> + <StatLabel>Total Posts</StatLabel> + <StatNumber>{`${posts?.total || 'NA'}`}</StatNumber> + </Stat> + ) +} + +export const PostsManager = () => { + return ( + <Box> + <Flex wrap="wrap" bg="#011627" p={4} color="white"> + <Box> + <Heading size="xl">Manage Posts</Heading> + </Box> + <Spacer /> + <Box> + <PostsCountStat /> + </Box> + </Flex> + <Divider /> + <Box p={4}> + <PostList /> + </Box> + </Box> + ) +} + +export default PostsManager diff --git a/examples/query/react/graphql-custom-client/src/index.tsx b/examples/query/react/graphql-custom-client/src/index.tsx new file mode 100644 index 0000000000..a7429fe087 --- /dev/null +++ b/examples/query/react/graphql-custom-client/src/index.tsx @@ -0,0 +1,26 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App' +import { api } from './app/services/posts' +import { ChakraProvider } from '@chakra-ui/react' + +import { BrowserRouter } from 'react-router-dom' +import { worker } from './mocks/browser' +import { ApiProvider } from '@reduxjs/toolkit/query/react' + +// Initialize the msw worker, wait for the service worker registration to resolve, then mount +worker.start({ quiet: true }).then(() => { + return ReactDOM.createRoot( + document.getElementById('root') as HTMLElement + ).render( + <React.StrictMode> + <ApiProvider api={api}> + <ChakraProvider> + <BrowserRouter> + <App /> + </BrowserRouter> + </ChakraProvider> + </ApiProvider> + </React.StrictMode> + ) +}) diff --git a/examples/query/react/graphql-custom-client/src/mocks/browser.ts b/examples/query/react/graphql-custom-client/src/mocks/browser.ts new file mode 100644 index 0000000000..82d072ec49 --- /dev/null +++ b/examples/query/react/graphql-custom-client/src/mocks/browser.ts @@ -0,0 +1,4 @@ +import { setupWorker } from 'msw' +import { handlers } from './db' + +export const worker = setupWorker(...handlers) diff --git a/examples/query/react/graphql-custom-client/src/mocks/db.ts b/examples/query/react/graphql-custom-client/src/mocks/db.ts new file mode 100644 index 0000000000..0aa6a3c2a4 --- /dev/null +++ b/examples/query/react/graphql-custom-client/src/mocks/db.ts @@ -0,0 +1,71 @@ +import { nanoid } from '@reduxjs/toolkit' +import { factory, primaryKey } from '@mswjs/data' +import faker from 'faker' +import { graphql } from 'msw' +import { postStatuses } from '../app/services/posts' +import type { Pagination, Post } from '../app/services/posts' + +const db = factory({ + post: { + id: primaryKey(String), + name: String, + title: String, + author: String, + content: String, + status: String, + created_at: String, + updated_at: String, + }, +}) + +const getRandomStatus = () => + postStatuses[Math.floor(Math.random() * postStatuses.length)] + +const createPostData = (): Post => { + const date = faker.date.past().toISOString() + return { + id: nanoid(), + title: faker.lorem.words(), + author: faker.name.findName(), + content: faker.lorem.paragraphs(), + status: getRandomStatus(), + created_at: date, + updated_at: date, + } +} + +;[...new Array(50)].forEach((_) => db.post.create(createPostData())) + +type PaginationOptions = { + page: number; per_page: number +} + +interface Posts extends Pagination { + data: { + posts: Post[] + } +} + +export const handlers = [ + graphql.query<Posts, PaginationOptions>('GetPosts', (req, res, ctx) => { + const { page = 1, per_page = 10 } = req.variables + + const posts = db.post.findMany({ + take: per_page, + skip: Math.max(per_page * (page - 1), 0) + }) + + return res( + ctx.data({ + data: { + posts + } as { posts: Post[] }, + per_page, + page, + total_pages: Math.ceil(db.post.count() / per_page), + total: db.post.count(), + }) + ) + }), + ...db.post.toHandlers('graphql'), +] as const diff --git a/examples/query/react/graphql-custom-client/src/react-app-env.d.ts b/examples/query/react/graphql-custom-client/src/react-app-env.d.ts new file mode 100644 index 0000000000..6431bc5fc6 --- /dev/null +++ b/examples/query/react/graphql-custom-client/src/react-app-env.d.ts @@ -0,0 +1 @@ +/// <reference types="react-scripts" /> diff --git a/examples/query/react/graphql-custom-client/tsconfig.json b/examples/query/react/graphql-custom-client/tsconfig.json new file mode 100644 index 0000000000..5f488e8e73 --- /dev/null +++ b/examples/query/react/graphql-custom-client/tsconfig.json @@ -0,0 +1,25 @@ +{ + "include": [ + "./src/**/*" + ], + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "lib": [ + "dom", + "es2015" + ], + "jsx": "react-jsx", + "target": "es5", + "allowJs": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true + } +} From 2777ccbc88dbd0c7f48d7fe81adb77bcf9a07e8d Mon Sep 17 00:00:00 2001 From: Aleksandruk <e-tzau@gft.com> Date: Mon, 13 Feb 2023 13:51:34 +0100 Subject: [PATCH 4/4] updating yarn lock --- yarn.lock | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/yarn.lock b/yarn.lock index 4fdbfb5d62..7f8b1fa129 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3605,6 +3605,34 @@ __metadata: languageName: unknown linkType: soft +"@examples-query-react/graphql-custom-client@workspace:examples/query/react/graphql-custom-client": + version: 0.0.0-use.local + resolution: "@examples-query-react/graphql-custom-client@workspace:examples/query/react/graphql-custom-client" + dependencies: + "@chakra-ui/react": 1.0.0 + "@emotion/react": ^11.4.0 + "@emotion/styled": ^11.3.0 + "@mswjs/data": ^0.3.0 + "@reduxjs/toolkit": ^1.6.0-rc.1 + "@rtk-query/graphql-request-base-query": ^2.0.0 + "@types/faker": ^5.5.5 + "@types/react": ^18.0.5 + "@types/react-dom": ^18.0.5 + faker: ^5.5.3 + framer-motion: ^2.9.5 + graphql: ^15.5.0 + graphql-request: ^3.4.0 + msw: 0.28.2 + react: ^18.1.0 + react-dom: ^18.1.0 + react-icons: 3.11.0 + react-redux: ^8.0.2 + react-router-dom: 6.3.0 + react-scripts: 5.0.1 + typescript: ~4.2.4 + languageName: unknown + linkType: soft + "@examples-query-react/graphql@workspace:examples/query/react/graphql": version: 0.0.0-use.local resolution: "@examples-query-react/graphql@workspace:examples/query/react/graphql"