├── .all-contributorsrc ├── .editorconfig ├── .env.sample ├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── Bug_Report.md │ └── Feature_Request.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── acceptance-tests.yml │ ├── check-linked-issues.yml │ ├── ci.yml │ ├── create-react-app-server.yml │ ├── notify-release.yml │ └── stale.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .netlify └── functions │ └── deploy-succeeded.js ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── RELEASE_PROCESS.md ├── babel.config.js ├── commitlint.config.js ├── config ├── rollup.config.js └── tsconfig.base.json ├── cypress.config.ts ├── cypress ├── e2e │ ├── create-react-app.cy.ts │ └── persisted-queries.cy.ts ├── support │ ├── commands.js │ └── e2e.js └── tsconfig.json ├── examples ├── create-react-app │ ├── .env │ ├── CHANGELOG.md │ ├── Dockerfile │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ └── manifest.json │ ├── server │ │ └── db.cjs │ ├── src │ │ ├── App.js │ │ ├── components │ │ │ ├── CreatePost.js │ │ │ ├── CreatePostForm.js │ │ │ ├── ErrorBoundary.js │ │ │ ├── Posts.js │ │ │ └── PostsWithErrorBoundary.js │ │ ├── index.css │ │ └── index.js │ └── test │ │ ├── CreatePost.test.tsx │ │ ├── Posts.test.tsx │ │ ├── setup.js │ │ └── test-utils.tsx ├── fastify-ssr │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── app │ │ │ ├── AppShell.js │ │ │ ├── components │ │ │ │ └── Hello.js │ │ │ └── pages │ │ │ │ ├── HomePage.js │ │ │ │ ├── NotFoundPage.js │ │ │ │ └── PaginationPage.js │ │ ├── client │ │ │ └── js │ │ │ │ └── app-shell.js │ │ ├── index.js │ │ └── server │ │ │ ├── graphql │ │ │ ├── index.js │ │ │ ├── resolvers.js │ │ │ └── schema.js │ │ │ ├── handlers │ │ │ └── app-shell.js │ │ │ ├── helpers │ │ │ └── manifest.js │ │ │ └── index.js │ └── webpack.config.js ├── full-ws-transport │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── index.html │ ├── server │ │ ├── index.js │ │ └── package.json │ └── src │ │ ├── App.js │ │ ├── index.css │ │ └── index.js ├── persisted-queries │ ├── README.md │ ├── client │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── public │ │ │ ├── favicon.ico │ │ │ ├── index.html │ │ │ └── manifest.json │ │ ├── src │ │ │ ├── App.tsx │ │ │ ├── index.css │ │ │ ├── index.tsx │ │ │ └── react-app-env.d.ts │ │ └── tsconfig.json │ └── server │ │ ├── CHANGELOG.md │ │ ├── apollo.ts │ │ ├── mercurius.ts │ │ ├── package-lock.json │ │ ├── package.json │ │ └── tsconfig.json ├── subscription │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── client │ │ │ ├── App.js │ │ │ └── index.js │ │ └── server │ │ │ ├── index.html │ │ │ └── index.js │ └── webpack.config.js └── typescript │ ├── .env │ ├── CHANGELOG.md │ ├── README.md │ ├── codegen.ts │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json │ ├── src │ ├── App.tsx │ ├── gql │ │ ├── fragment-masking.ts │ │ ├── gql.ts │ │ ├── graphql.ts │ │ └── index.ts │ ├── index.css │ ├── index.tsx │ └── react-app-env.d.ts │ └── tsconfig.json ├── jest.config.js ├── lerna.json ├── package.json └── packages ├── babel-plugin-extract-gql ├── .babelrc ├── .gitignore ├── CHANGELOG.md ├── README.md ├── examples │ ├── mercurius │ │ ├── README.md │ │ ├── index.js │ │ └── package.json │ └── proj-a │ │ ├── README.md │ │ └── src │ │ ├── App.js │ │ ├── components │ │ └── MyComp │ │ │ ├── MyComp.js │ │ │ └── index.js │ │ └── index.js ├── package-lock.json ├── package.json ├── src │ └── index.js └── test │ └── unit │ ├── fixtures │ ├── actual │ │ ├── .4.js │ │ ├── 1.js │ │ ├── 2.js │ │ └── 3.js │ └── expected │ │ ├── 1.js │ │ ├── 2.js │ │ ├── 3.js │ │ └── 4.js │ └── index.js ├── graphql-hooks-memcache ├── CHANGELOG.md ├── README.md ├── babel.config.js ├── index.d.ts ├── package.json ├── rollup.config.js ├── src │ ├── fnv1a.js │ └── index.js └── test │ ├── fnv1a.test.ts │ └── index.test.ts ├── graphql-hooks-ssr ├── CHANGELOG.md ├── README.md ├── index.d.ts ├── index.js ├── index.test.tsx └── package.json └── graphql-hooks ├── CHANGELOG.md ├── package.json ├── rollup.config.js ├── src ├── .babelrc.js ├── ClientContext.ts ├── GraphQLClient.ts ├── LocalGraphQLClient.ts ├── LocalGraphQLError.ts ├── Middleware.js ├── canUseDOM.js ├── createRefetchMutationsMap.ts ├── events.ts ├── index.ts ├── isExtractableFileEnhanced.ts ├── middlewares │ ├── README.md │ ├── apqMiddleware.ts │ └── examples │ │ ├── cacheMiddleware.ts │ │ └── debugMiddleware.ts ├── types │ ├── common-types.ts │ └── typedDocumentNode.ts ├── useClientRequest.ts ├── useDeepCompareCallback.ts ├── useManualQuery.ts ├── useMutation.ts ├── useQuery.ts ├── useQueryClient.ts ├── useSubscription.ts └── utils.ts ├── test-jsdom ├── integration │ ├── GraphQLClient.test.ts │ ├── useManualQuery.test.tsx │ └── useQuery.test.tsx ├── setup.js ├── unit │ ├── GraphQLClient.test.ts │ ├── LocalGraphQLClient.test.tsx │ ├── Middleware.test.ts │ ├── apqMiddleware.test.ts │ ├── createRefetchMutationsMap.test.ts │ ├── useClientRequest.test.tsx │ ├── useDeepCompareCallback.test.ts │ ├── useManualQuery.test.ts │ ├── useMutation.test.tsx │ ├── useQuery.test.tsx │ ├── useQueryClient.test.tsx │ ├── useSubscription.test.tsx │ └── utils.test.ts └── utils.ts ├── test-node ├── GraphQLClient.test.ts ├── sample.txt └── utils.test.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | end_of_line = lf -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | // used when publishing to create a GitHub release 2 | GH_TOKEN= 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | es 4 | lib 5 | build 6 | **/fixtures 7 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@babel/eslint-parser', 3 | env: { 4 | browser: true, 5 | node: true, 6 | es6: true, 7 | jest: true 8 | }, 9 | extends: [ 10 | 'eslint:recommended', 11 | 'plugin:react/recommended', 12 | 'prettier', 13 | 'plugin:prettier/recommended' 14 | ], 15 | globals: { 16 | Atomics: 'readonly', 17 | SharedArrayBuffer: 'readonly', 18 | fixture: true, 19 | test: true 20 | }, 21 | parserOptions: { 22 | ecmaFeatures: { 23 | jsx: true 24 | }, 25 | ecmaVersion: 2018, 26 | sourceType: 'module' 27 | }, 28 | settings: { 29 | react: { 30 | version: '16.8.3' 31 | } 32 | }, 33 | plugins: ['prettier', 'react', 'react-hooks'], 34 | rules: { 35 | 'prettier/prettier': 'error', 36 | 'react-hooks/rules-of-hooks': 'error', 37 | 'react-hooks/exhaustive-deps': 'warn', 38 | 'no-unused-vars': 'error', 39 | 'linebreak-style': ['error', 'unix'] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Package 2 | 3 | 4 | 5 | - [ ] `graphql-hooks` 6 | - [ ] `graphql-hooks-ssr` 7 | - [ ] `graphql-hooks-memcache` 8 | 9 | ### Environment 10 | 11 | - `graphql-hooks` version: 12 | 13 | - `graphql-hooks-ssr` version: 14 | - `graphql-hooks-memcache` version: 15 | - `react` version: 16 | - Browser: 17 | 18 | ### Description 19 | 20 | 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_Report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B Bug Report" 3 | about: Bugs or any unexpected issues. 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | ### Package 10 | 11 | 12 | 13 | `graphql-hooks` 14 | `graphql-hooks-ssr` 15 | `graphql-hooks-memcache` 16 | 17 | ### Environment 18 | 19 | - `graphql-hooks` version: 20 | 21 | - `graphql-hooks-ssr` version: 22 | - `graphql-hooks-memcache` version: 23 | - `react` version: 24 | - Browser: 25 | 26 | ### Description 27 | 28 | 29 | 30 | ### How to reproduce 31 | 32 | 33 | 34 | ```jsx 35 | Your code sample 36 | ``` 37 | 38 | ### Suggested solution (optional) 39 | 40 | 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_Request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F4A1 Feature Request" 3 | about: I have a suggestion for a new feature! 4 | title: '' 5 | labels: feature 6 | assignees: '' 7 | --- 8 | 9 | ### Description 10 | 11 | 12 | 13 | ### Suggested implementation 14 | 15 | 16 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### What does this PR do? 2 | 3 | 4 | 5 | ### Related issues 6 | 7 | 8 | 9 | ### Checklist 10 | 11 | - [ ] I have checked the [contributing document](../blob/master/CONTRIBUTING.md) 12 | - [ ] I have added or updated any relevant documentation 13 | - [ ] I have added or updated any relevant tests 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | - package-ecosystem: github-actions 8 | directory: / 9 | schedule: 10 | interval: weekly 11 | -------------------------------------------------------------------------------- /.github/workflows/acceptance-tests.yml: -------------------------------------------------------------------------------- 1 | name: acceptance-tests 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | acceptance-tests: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: actions/setup-node@v3 15 | with: 16 | node-version-file: '.nvmrc' 17 | - name: Install root 18 | run: npm install 19 | - name: Cypress create-react-app 20 | uses: cypress-io/github-action@v6 21 | with: 22 | browser: chrome 23 | install: false 24 | spec: cypress/e2e/create-react-app.cy.ts 25 | start: npm start --prefix ./examples/create-react-app 26 | - name: Cypress persisted-queries Mercurius 27 | uses: cypress-io/github-action@v6 28 | with: 29 | browser: chrome 30 | install: false 31 | spec: cypress/e2e/persisted-queries.cy.ts 32 | start: npm run start:mercurius --prefix ./examples/persisted-queries/server, npm start --prefix ./examples/persisted-queries/client 33 | env: 34 | SERVER_PORT: 8001 35 | REACT_APP_SERVER_PORT: 8001 36 | PORT: 3002 37 | CYPRESS_CLIENT_PORT: 3002 38 | - name: Cypress persisted-queries Apollo Server 39 | uses: cypress-io/github-action@v6 40 | with: 41 | install: false 42 | spec: cypress/e2e/persisted-queries.cy.ts 43 | start: npm run start:apollo --prefix ./examples/persisted-queries/server, npm start --prefix ./examples/persisted-queries/client 44 | env: 45 | SERVER_PORT: 8002 46 | REACT_APP_SERVER_PORT: 8002 47 | PORT: 3003 48 | CYPRESS_CLIENT_PORT: 3003 49 | -------------------------------------------------------------------------------- /.github/workflows/check-linked-issues.yml: -------------------------------------------------------------------------------- 1 | name: Check Linked Issues 2 | 'on': 3 | pull_request_target: 4 | types: 5 | - opened 6 | - edited 7 | - reopened 8 | - synchronize 9 | jobs: 10 | check_pull_requests: 11 | runs-on: ubuntu-latest 12 | name: Check linked issues 13 | permissions: 14 | issues: read 15 | pull-requests: write 16 | steps: 17 | - uses: nearform-actions/github-action-check-linked-issues@v1 18 | with: 19 | github-token: ${{ secrets.GITHUB_TOKEN }} 20 | exclude-branches: release/**, dependabot/** 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Cache node modules 16 | uses: actions/cache@v3 17 | with: 18 | path: ~/.npm 19 | key: npm-${{ hashFiles('**/package.json') }} 20 | - uses: actions/setup-node@v3 21 | with: 22 | node-version-file: '.nvmrc' 23 | - run: npm install 24 | - run: npm run lint 25 | - run: npm run test:coverage -- --silent 26 | - run: npm run check-types 27 | - run: npm run build 28 | - uses: coverallsapp/github-action@v2.3.4 29 | with: 30 | github-token: ${{ secrets.GITHUB_TOKEN }} 31 | -------------------------------------------------------------------------------- /.github/workflows/create-react-app-server.yml: -------------------------------------------------------------------------------- 1 | name: Mock GraphQL Server to GCP 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | workflow_dispatch: 8 | 9 | env: 10 | PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }} 11 | RUN_REGION: europe-west1 12 | SERVICE_NAME: create-react-app-server 13 | 14 | jobs: 15 | setup-build-deploy: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | 21 | - uses: google-github-actions/auth@v2 22 | with: 23 | project_id: ${{ secrets.GCP_PROJECT_ID }} 24 | credentials_json: ${{ secrets.GCP_SA_KEY }} 25 | 26 | - uses: google-github-actions/setup-gcloud@v2 27 | 28 | - working-directory: ./examples/create-react-app 29 | run: |- 30 | gcloud builds submit \ 31 | --quiet \ 32 | --tag "gcr.io/$PROJECT_ID/$SERVICE_NAME:$GITHUB_SHA" 33 | 34 | - working-directory: ./examples/create-react-app 35 | run: |- 36 | gcloud run deploy "$SERVICE_NAME" \ 37 | --quiet \ 38 | --region "$RUN_REGION" \ 39 | --image "gcr.io/$PROJECT_ID/$SERVICE_NAME:$GITHUB_SHA" \ 40 | --platform "managed" \ 41 | --allow-unauthenticated 42 | -------------------------------------------------------------------------------- /.github/workflows/notify-release.yml: -------------------------------------------------------------------------------- 1 | name: notify-release 2 | 'on': 3 | workflow_dispatch: 4 | schedule: 5 | - cron: 30 8 * * * 6 | release: 7 | types: 8 | - published 9 | issues: 10 | types: 11 | - closed 12 | jobs: 13 | setup: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | issues: write 17 | contents: read 18 | steps: 19 | - name: Notify release 20 | uses: nearform-actions/github-action-notify-release@v1 21 | with: 22 | github-token: ${{ secrets.GITHUB_TOKEN }} 23 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: '30 1 * * *' 6 | 7 | jobs: 8 | stale: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/stale@v9 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | graphql-hooks-*.*.*.tgz 4 | coverage 5 | .DS_Store 6 | lib 7 | dist 8 | es 9 | packages/graphql-hooks/README.md 10 | packages/*/LICENSE 11 | build 12 | .size-snapshot.json 13 | yarn.lock 14 | .vscode 15 | .env 16 | *.log 17 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx --no-install commitlint --edit $1 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx --no-install lint-staged 2 | -------------------------------------------------------------------------------- /.netlify/functions/deploy-succeeded.js: -------------------------------------------------------------------------------- 1 | const https = require('https') 2 | 3 | exports.handler = function (event, context, callback) { 4 | const e = JSON.parse(event.body) 5 | 6 | const options = { 7 | hostname: 'api.github.com', 8 | port: 443, 9 | path: `/repos/nearform/graphql-hooks/actions/workflows/acceptance-tests.yml/dispatches`, 10 | method: 'POST', 11 | headers: { 12 | Accept: 'application/vnd.github.v3+json', 13 | 'Content-Type': 'application/json', 14 | 'User-Agent': 'netlify' 15 | }, 16 | auth: process.env.GITHUB_BASIC_AUTH 17 | } 18 | 19 | const postData = JSON.stringify({ 20 | ref: e.payload.branch, 21 | inputs: { 22 | ACCEPTANCE_URL: e.payload.deploy_ssl_url 23 | } 24 | }) 25 | 26 | const req = https.request(options, res => { 27 | res.setEncoding('utf8') 28 | 29 | let body = '' 30 | 31 | res.on('data', chunk => { 32 | body += chunk 33 | }) 34 | 35 | res.on('error', e => callback(e)) 36 | 37 | res.on('end', () => { 38 | callback(null, { 39 | statusCode: res.statusCode, 40 | body 41 | }) 42 | }) 43 | }) 44 | 45 | req.on('error', e => callback(e)) 46 | 47 | // write data to request body 48 | req.write(postData) 49 | req.end() 50 | } 51 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/* 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | es 4 | lib 5 | build 6 | CHANGELOG.md 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 80, 4 | "semi": false, 5 | "trailingComma": "none", 6 | "arrowParens": "avoid" 7 | } 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | - Using welcoming and inclusive language 12 | - Being respectful of differing viewpoints and experiences 13 | - Gracefully accepting constructive criticism 14 | - Focusing on what is best for the community 15 | - Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | - Trolling, insulting/derogatory comments, and personal or political attacks 21 | - Public or private harassment 22 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | - Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at opensource@nearform.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Welcome to GraphQL Hooks! 2 | 3 | Please take a second to read over this before opening an issue. Providing complete information upfront will help us address any issue (and ship new features!) faster. 4 | 5 | We greatly appreciate bug fixes, documentation improvements and new features, however when contributing a new major feature, it is a good idea to first open an issue, to make sure the feature it fits with the goal of the project, so we don't waste your or our time. 6 | 7 | ## Bug Reports 8 | 9 | A perfect bug report would have the following: 10 | 11 | 1. Summary of the issue you are experiencing. 12 | 2. Details on what versions of node and graphql-hooks you are using (`node -v`). 13 | 3. A simple repeatable test case for us to run. Please try to run through it 2-3 times to ensure it is completely repeatable. 14 | 15 | We would like to avoid issues that require a follow up questions to identify the bug. These follow ups are difficult to do unless we have a repeatable test case. 16 | 17 | ## For Developers 18 | 19 | ### Packages 20 | 21 | We use [Lerna](https://lernajs.io) to manage this monorepo, you will find the different `graphql-hooks-*` modules in the `packages` directory. 22 | 23 | ### Getting started 24 | 25 | Clone the repository and run `npm install`. This will install the root dependencies and all of the dependencies required by each package, using `lerna bootstrap`. 26 | 27 | If you want to test your changes against an example app then take a look at our [fastify-ssr example](examples/fastify-ssr). 28 | 29 | All contributions should use the [prettier](https://prettier.io/) formatter, pass linting and pass tests. 30 | You can do this by running: 31 | 32 | ``` 33 | npm run lint 34 | npm run prettier 35 | npm test 36 | ``` 37 | 38 | In addition, make sure to add tests for any new features. 39 | You can test the test coverage by running: 40 | 41 | ``` 42 | npm run test:coverage 43 | ``` 44 | 45 | **Important** 46 | We don't use async/await in this library due to transpilation costs. Instead we use Promises and inform the user that they should provide a suitable environment. 47 | 48 | ### Acceptance tests 49 | 50 | We use [Cypress](https://www.cypress.io/) to run acceptance tests against our [create-react-app example](examples/create-react-app) application. This is to ensure that the bundled versions of the code work as they should in an application. 51 | You can run these locally on Chrome by running `npm run test:acceptance`. You should make sure that the packages have been built and the create-react-app example is running on port 3000 locally. 52 | 53 | ## For Collaborators 54 | 55 | Make sure to get a `:thumbsup:`, `+1` or `LGTM` from another collaborator before merging a PR. If you aren't sure if a release should happen, open an issue. 56 | 57 | ## Updating Contributors list in README.md 58 | 59 | You can add yourself or another contributor by commenting on an issue or pull request: 60 | 61 | ``` 62 | @all-contributors please add for 63 | ``` 64 | 65 | For more information on `@all-contributors` see it's [usage docs](https://allcontributors.org/docs/en/bot/usage) 66 | 67 | ### Release process: 68 | 69 | When merging a pull request to `master`, the squashed commit message should follow the [Conventional Commits](https://www.conventionalcommits.org) specification. This enables us to automatically generate CHANGELOGs & determine a semantic version bump. 70 | 71 | Follow the [guide found here](./RELEASE_PROCESS.md) to make a release. 72 | 73 | --- 74 | 75 | 76 | 77 | ## Developer's Certificate of Origin 1.1 78 | 79 | By making a contribution to this project, I certify that: 80 | 81 | - (a) The contribution was created in whole or in part by me and I 82 | have the right to submit it under the open source license 83 | indicated in the file; or 84 | 85 | - (b) The contribution is based upon previous work that, to the best 86 | of my knowledge, is covered under an appropriate open source 87 | license and I have the right under that license to submit that 88 | work with modifications, whether created in whole or in part 89 | by me, under the same open source license (unless I am 90 | permitted to submit under a different license), as indicated 91 | in the file; or 92 | 93 | - (c) The contribution was provided directly to me by some other 94 | person who certified (a), (b) or (c) and I have not modified 95 | it. 96 | 97 | - (d) I understand and agree that this project and the contribution 98 | are public and that a record of the contribution (including all 99 | personal information I submit with it, including my sign-off) is 100 | maintained indefinitely and may be redistributed consistent with 101 | this project or the open source license(s) involved. 102 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Copyright 2019 nearForm 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. -------------------------------------------------------------------------------- /RELEASE_PROCESS.md: -------------------------------------------------------------------------------- 1 | # Releasing graphql-hooks 2 | 3 | _Note that this guide is intended for people in charge of managing internal releases in NearForm. If you have made changes to this repository, in most cases you should not need to take any action for the repo to be released._ 4 | 5 | If you need to release graphql-hooks, you should follow these steps: 6 | 7 | - Ensure you have sufficient permissions to publish the graphql-hooks package on npm. If you require permissions, contact IT. 8 | 9 | - Clone this repository locally 10 | 11 | - Run `npm install` in the root directory. When the installation is complete, the script will automatically run the build command, `npm run build:packages`. Ensure the builds complete successfully, if they do not, you may need to use a different version of node and re-run the command. 12 | 13 | - Create a GitHub personal access token with the following permissions on the graphql-hooks repo: 14 | 15 | - Metadata read-only 16 | - Content read and write 17 | - Issues read and write 18 | - Pull Requests read and write 19 | 20 | - Set a the `GH_TOKEN` environment variable in your terminal to the token you just made using 21 | 22 | ``` 23 | export GH_TOKEN= 24 | ``` 25 | 26 | - Authenticate to npm in the same terminal by running `npm login`. 27 | 28 | - Check the tests are passing with `npm test` 29 | 30 | - Run `npm run release` to run the release script. Follow the prompts from [`lerna publish`](https://lernajs.io/#command-publish), the script will ask you to verify to npm with 2FA, so have your authentication service ready. 31 | 32 | - Once the script has finished running, the release is complete. You can verify this by checking the [npm page](https://www.npmjs.com/package/graphql-hooks), and the [latest releases on the GitHub repo](https://github.com/nearform/graphql-hooks/releases/tag/graphql-hooks%406.3.1). 33 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | // this is used by jest only 2 | module.exports = { 3 | presets: [ 4 | [ 5 | '@babel/preset-env', 6 | { 7 | targets: { 8 | node: 'current' 9 | } 10 | } 11 | ], 12 | '@babel/preset-react', 13 | '@babel/preset-typescript' 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] } 2 | -------------------------------------------------------------------------------- /config/rollup.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import nodeResolve from '@rollup/plugin-node-resolve' 3 | import babel from '@rollup/plugin-babel' 4 | import replace from 'rollup-plugin-replace' 5 | import commonjs from '@rollup/plugin-commonjs' 6 | import terser from '@rollup/plugin-terser' 7 | import { sizeSnapshot } from 'rollup-plugin-size-snapshot' 8 | import esbuild from 'rollup-plugin-esbuild' // Used for TS transpiling 9 | 10 | // get the package.json for the current package 11 | const packageDir = path.join(__filename, '..') 12 | const pkg = require(`${packageDir}/package.json`) 13 | const external = [...Object.keys(pkg.peerDependencies || {})] 14 | 15 | // name will be used as the global name exposed in the UMD bundles 16 | const generateRollupConfig = ({ name, overrides, entryPoint }) => { 17 | overrides = overrides || {} 18 | entryPoint = entryPoint || 'src/index.js' 19 | // CommonJS 20 | return [ 21 | { 22 | input: entryPoint, 23 | output: { file: `lib/${pkg.name}.js`, format: 'cjs', indent: false }, 24 | external, 25 | plugins: [commonjs(), esbuild(), babel(), sizeSnapshot()], 26 | ...overrides 27 | }, 28 | 29 | // ES 30 | { 31 | input: entryPoint, 32 | output: { file: `es/${pkg.name}.js`, format: 'es', indent: false }, 33 | external, 34 | plugins: [commonjs(), esbuild(), babel(), sizeSnapshot()], 35 | ...overrides 36 | }, 37 | 38 | // ES for Browsers 39 | { 40 | input: entryPoint, 41 | output: { file: `es/${pkg.name}.mjs`, format: 'es', indent: false }, 42 | external, 43 | plugins: [ 44 | commonjs(), 45 | nodeResolve(), 46 | esbuild(), 47 | replace({ 48 | 'process.env.NODE_ENV': JSON.stringify('production') 49 | }), 50 | terser({ 51 | compress: { 52 | pure_getters: true, 53 | unsafe: true, 54 | unsafe_comps: true, 55 | warnings: false 56 | } 57 | }), 58 | sizeSnapshot() 59 | ], 60 | ...overrides 61 | }, 62 | 63 | // UMD Development 64 | { 65 | input: entryPoint, 66 | output: { 67 | file: `dist/${pkg.name}.js`, 68 | format: 'umd', 69 | name, 70 | indent: false 71 | }, 72 | external, 73 | plugins: [ 74 | commonjs(), 75 | nodeResolve(), 76 | esbuild(), 77 | babel({ 78 | exclude: 'node_modules/**' 79 | }), 80 | replace({ 81 | 'process.env.NODE_ENV': JSON.stringify('development') 82 | }), 83 | sizeSnapshot() 84 | ], 85 | ...overrides 86 | }, 87 | 88 | // UMD Production 89 | { 90 | input: entryPoint, 91 | output: { 92 | file: `dist/${pkg.name}.min.js`, 93 | format: 'umd', 94 | name, 95 | indent: false 96 | }, 97 | external, 98 | plugins: [ 99 | commonjs(), 100 | nodeResolve(), 101 | esbuild(), 102 | babel({ 103 | exclude: 'node_modules/**' 104 | }), 105 | replace({ 106 | 'process.env.NODE_ENV': JSON.stringify('production') 107 | }), 108 | terser({ 109 | compress: { 110 | pure_getters: true, 111 | unsafe: true, 112 | unsafe_comps: true, 113 | warnings: false 114 | } 115 | }), 116 | sizeSnapshot() 117 | ], 118 | ...overrides 119 | } 120 | ] 121 | } 122 | 123 | export default generateRollupConfig 124 | -------------------------------------------------------------------------------- /config/tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es2021", "dom"], 4 | "module": "esnext", 5 | "target": "es2021", 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "allowJs": true, 12 | "jsx": "react-jsx", 13 | "noFallthroughCasesInSwitch": true, 14 | // Eventually those should be switched to more strict options, but we can ease into it while we're converting to typescript 15 | "noImplicitAny": false 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress' 2 | 3 | export default defineConfig({ 4 | e2e: { 5 | defaultCommandTimeout: 6000, 6 | video: false 7 | } 8 | }) 9 | -------------------------------------------------------------------------------- /cypress/e2e/create-react-app.cy.ts: -------------------------------------------------------------------------------- 1 | describe('create-react-app', () => { 2 | it('The example renders a list of posts', () => { 3 | cy.visit('http://localhost:3000') 4 | cy.findAllByRole('listitem').should('have.length.gte', 4) 5 | }) 6 | }) 7 | -------------------------------------------------------------------------------- /cypress/e2e/persisted-queries.cy.ts: -------------------------------------------------------------------------------- 1 | describe('persisted-queries', () => { 2 | it('The example adds two numbers', () => { 3 | const x = 4 4 | const y = 5 5 | 6 | cy.visit(`http://localhost:${Cypress.env('CLIENT_PORT') ?? 3000}`) 7 | cy.findByLabelText('x').type(x.toString()) 8 | cy.findByLabelText('y').type(y.toString()) 9 | cy.findByRole('button', { name: /add/i }).click() 10 | cy.findByText(x + y).should('exist') 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add('login', (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 26 | import '@testing-library/cypress/add-commands' 27 | -------------------------------------------------------------------------------- /cypress/support/e2e.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["es5", "dom"], 5 | "types": ["cypress", "@testing-library/cypress"] 6 | }, 7 | "include": ["**/*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /examples/create-react-app/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true 2 | -------------------------------------------------------------------------------- /examples/create-react-app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16-alpine 2 | 3 | WORKDIR /service 4 | 5 | COPY package.json ./ 6 | COPY server ./server 7 | COPY public ./public 8 | 9 | RUN npm i 10 | USER node 11 | 12 | CMD ["npm", "run", "start:server"] 13 | -------------------------------------------------------------------------------- /examples/create-react-app/README.md: -------------------------------------------------------------------------------- 1 | [![Netlify Status](https://api.netlify.com/api/v1/badges/ea51044b-e4eb-4b1d-8661-858260d82d7d/deploy-status)](https://app.netlify.com/sites/graphql-hooks-cra/deploys) 2 | 3 | See this example live at https://graphql-hooks-cra.netlify.app/ 4 | 5 | --- 6 | 7 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 8 | 9 | ## Available Scripts 10 | 11 | In the project directory, you can run: 12 | 13 | ### `npm start` 14 | 15 | Runs the app in the development mode.
16 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 17 | 18 | The page will reload if you make edits.
19 | You will also see any lint errors in the console. 20 | 21 | ### `npm test` 22 | 23 | Launches the test runner in the interactive watch mode.
24 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 25 | 26 | ### `npm run build` 27 | 28 | Builds the app for production to the `build` folder.
29 | It correctly bundles React in production mode and optimizes the build for the best performance. 30 | 31 | The build is minified and the filenames include the hashes.
32 | Your app is ready to be deployed! 33 | 34 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 35 | 36 | ### `npm run eject` 37 | 38 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 39 | 40 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 41 | 42 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 43 | 44 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 45 | 46 | ## Learn More 47 | 48 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 49 | 50 | To learn React, check out the [React documentation](https://reactjs.org/). 51 | 52 | ### Code Splitting 53 | 54 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 55 | 56 | ### Analyzing the Bundle Size 57 | 58 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 59 | 60 | ### Making a Progressive Web App 61 | 62 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 63 | 64 | ### Advanced Configuration 65 | 66 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 67 | 68 | ### Deployment 69 | 70 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 71 | 72 | ### `npm run build` fails to minify 73 | 74 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 75 | -------------------------------------------------------------------------------- /examples/create-react-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-react-app", 3 | "version": "5.1.1", 4 | "private": true, 5 | "engines": { 6 | "node": ">=12.0.0" 7 | }, 8 | "dependencies": { 9 | "graphql-hooks": "^8.2.0", 10 | "graphql-hooks-memcache": "^3.2.0", 11 | "json-graphql-server": "^3.0.1", 12 | "prop-types": "^15.7.2", 13 | "react": "^18.0.0", 14 | "react-app-polyfill": "^2.0.0", 15 | "react-dom": "^18.0.0", 16 | "react-scripts": "^5.0.0" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "eject": "react-scripts eject", 22 | "start:server": "json-graphql-server server/db.cjs --h 0.0.0.0 --p 8080" 23 | }, 24 | "browserslist": [ 25 | "ie >= 11" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /examples/create-react-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nearform/graphql-hooks/5a124588a3ce684601712bb7076ba2162322c937/examples/create-react-app/public/favicon.ico -------------------------------------------------------------------------------- /examples/create-react-app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 14 | 15 | 19 | 20 | 29 | Postie - A graphql-hooks example 30 | 31 | 32 | 33 |
34 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /examples/create-react-app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Postie", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /examples/create-react-app/server/db.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | posts: [ 3 | { id: 1, title: 'Lorem Ipsum', url: 'http://lorum.com' }, 4 | { id: 2, title: 'Sic Dolor amet', url: 'http://dolor.com' } 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /examples/create-react-app/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { GraphQLClient, ClientContext } from 'graphql-hooks' 3 | import memCache from 'graphql-hooks-memcache' 4 | 5 | import Posts from './components/Posts' 6 | import PostsWithErrorBoundary from './components/PostsWithErrorBoundary' 7 | 8 | const client = new GraphQLClient({ 9 | cache: memCache(), 10 | url: 'https://create-react-app-server-kqtv5azt3q-ew.a.run.app' 11 | }) 12 | 13 | export default function App() { 14 | const [showPosts, setShowPosts] = useState(true) 15 | 16 | const togglePosts = () => setShowPosts(!showPosts) 17 | 18 | return ( 19 | 20 |

Postie

21 | Verify caching by hiding/showing the posts: 22 | 23 | {showPosts && } 24 | {showPosts && } 25 |
26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /examples/create-react-app/src/components/CreatePost.js: -------------------------------------------------------------------------------- 1 | import { useMutation, useQueryClient } from 'graphql-hooks' 2 | import React from 'react' 3 | import CreatePostForm from './CreatePostForm' 4 | import { allPostsQuery } from './Posts' 5 | 6 | export const createPostMutation = ` 7 | mutation CreatePost($title: String!, $url: String!) { 8 | createPost(title: $title, url: $url) { 9 | id 10 | } 11 | } 12 | ` 13 | 14 | export default function CreatePost() { 15 | const client = useQueryClient() 16 | const [createPost, { loading, error }] = useMutation(createPostMutation, { 17 | onSuccess: () => { 18 | // Update cache without refetch data 19 | // client.setQueryData(allPostsQuery, oldState => { 20 | // return { 21 | // allPosts: [ 22 | // ...oldState.allPosts, 23 | // { id: result.data.createPost.id, ...variables } 24 | // ] 25 | // } 26 | // }) 27 | 28 | // Update cache by refetching data 29 | client.invalidateQuery(allPostsQuery) 30 | } 31 | }) 32 | 33 | async function handleSubmit({ title, url }) { 34 | await createPost({ variables: { title, url } }) 35 | } 36 | 37 | return ( 38 | 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /examples/create-react-app/src/components/CreatePostForm.js: -------------------------------------------------------------------------------- 1 | import T from 'prop-types' 2 | import React, { useState } from 'react' 3 | 4 | export default function CreatePostForm({ loading, error, onSubmit }) { 5 | const [title, setTitle] = useState('') 6 | const [url, setUrl] = useState('') 7 | 8 | function handleSubmit(e) { 9 | e.preventDefault() 10 | onSubmit({ title, url }) 11 | } 12 | 13 | return ( 14 |
15 | 16 | setTitle(e.currentTarget.value)} 21 | /> 22 | 23 | setUrl(e.currentTarget.value)} 29 | /> 30 | 33 | {error &&

Oh no! There was an error when adding this post.

} 34 |
35 | ) 36 | } 37 | 38 | CreatePostForm.propTypes = { 39 | loading: T.bool, 40 | error: T.bool, 41 | onSubmit: T.func.isRequired 42 | } 43 | -------------------------------------------------------------------------------- /examples/create-react-app/src/components/ErrorBoundary.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import T from 'prop-types' 3 | 4 | function stringify(value) { 5 | return JSON.stringify( 6 | value, 7 | (key, value) => { 8 | if (key && typeof value === 'string') { 9 | try { 10 | return JSON.parse(value) 11 | } catch (err) { 12 | return value 13 | } 14 | } 15 | 16 | return value 17 | }, 18 | 2 19 | ) 20 | } 21 | 22 | export default class ErrorBoundary extends React.Component { 23 | static propTypes = { 24 | children: T.node 25 | } 26 | 27 | constructor(props) { 28 | super(props) 29 | this.state = { error: null, errorInfo: null } 30 | } 31 | 32 | componentDidCatch(error, errorInfo) { 33 | this.setState({ 34 | error: error, 35 | errorInfo: errorInfo 36 | }) 37 | } 38 | 39 | render() { 40 | if (this.state.error) { 41 | return ( 42 |
43 |
Something went wrong.
44 |
45 | 46 | More details 47 | 48 |
{stringify(this.state.error)}
49 |
{this.state.errorInfo?.componentStack}
50 |
51 |
52 | ) 53 | } 54 | return this.props.children 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /examples/create-react-app/src/components/Posts.js: -------------------------------------------------------------------------------- 1 | import { useQuery } from 'graphql-hooks' 2 | import T from 'prop-types' 3 | import React from 'react' 4 | import CreatePost from './CreatePost' 5 | 6 | export const allPostsQuery = ` 7 | query { 8 | allPosts { 9 | id 10 | title 11 | url 12 | } 13 | } 14 | ` 15 | 16 | export default function Posts() { 17 | const { loading, data, error } = useQuery(allPostsQuery) 18 | 19 | return ( 20 | <> 21 |

Add post

22 | 23 |

Posts

24 | 25 | 26 | ) 27 | } 28 | 29 | function PostList({ loading, error, data }) { 30 | if (loading) return 'Loading...' 31 | if (error) return 'There was an error loading the posts :(' 32 | if (!data || !data.allPosts || !data.allPosts.length) return 'No posts' 33 | 34 | return ( 35 |
    36 | {data.allPosts.map(post => ( 37 |
  • 38 | {post.title} 39 |
  • 40 | ))} 41 |
42 | ) 43 | } 44 | 45 | PostList.propTypes = { 46 | loading: T.bool, 47 | error: T.shape({ 48 | fetchError: T.any, 49 | httpError: T.any, 50 | graphQLErrors: T.array 51 | }), 52 | data: T.shape({ 53 | allPosts: T.array 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /examples/create-react-app/src/components/PostsWithErrorBoundary.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { useQuery } from 'graphql-hooks' 3 | import T from 'prop-types' 4 | 5 | import ErrorBoundary from './ErrorBoundary' 6 | 7 | export const allPostsQuery = ` 8 | query { 9 | allPosts { 10 | id 11 | title 12 | url 13 | } 14 | } 15 | ` 16 | 17 | export const errorAllPostsQuery = ` 18 | query { 19 | allPosts { 20 | id 21 | title 22 | url 23 | boom 24 | } 25 | } 26 | ` 27 | 28 | export default function PostsWithErrorBoundary() { 29 | const [generateError, setGenerateError] = useState(false) 30 | return ( 31 | <> 32 |

Posts with error boundary

33 |
34 | 37 |
38 | 39 | 40 | 41 | 42 | ) 43 | } 44 | 45 | function PostList({ generateError }) { 46 | const { loading, data } = useQuery( 47 | generateError ? errorAllPostsQuery : allPostsQuery, 48 | { throwErrors: true } 49 | ) 50 | 51 | if (loading) return 'Loading...' 52 | if (!data || !data.allPosts || !data.allPosts.length) return 'No posts' 53 | 54 | return ( 55 |
    56 | {data.allPosts.map(post => ( 57 |
  • 58 | {post.title} 59 |
  • 60 | ))} 61 |
62 | ) 63 | } 64 | 65 | PostList.propTypes = { 66 | generateError: T.bool 67 | } 68 | -------------------------------------------------------------------------------- /examples/create-react-app/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 10px; 3 | padding: 10px; 4 | font-family: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', 5 | 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 6 | 'Helvetica Neue', sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 13 | monospace; 14 | } 15 | -------------------------------------------------------------------------------- /examples/create-react-app/src/index.js: -------------------------------------------------------------------------------- 1 | import 'react-app-polyfill/ie11' 2 | import React from 'react' 3 | import ReactDOM from 'react-dom' 4 | 5 | import App from './App' 6 | import './index.css' 7 | 8 | ReactDOM.render(, document.getElementById('root')) 9 | -------------------------------------------------------------------------------- /examples/create-react-app/test/CreatePost.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen, fireEvent, waitFor } from './test-utils' 2 | import CreatePost, { createPostMutation } from '../src/components/CreatePost' 3 | import React from 'react' 4 | 5 | const localQueries = { 6 | [createPostMutation]: () => ({ createPost: { id: 1 } }) 7 | } 8 | 9 | describe('CreatePost', () => { 10 | afterEach(() => { 11 | jest.resetAllMocks() 12 | }) 13 | 14 | it('should submit the new post', async () => { 15 | const createPostSpy = jest.spyOn(localQueries, createPostMutation) 16 | 17 | render(, { 18 | localQueries 19 | }) 20 | 21 | fireEvent.input( 22 | screen.getByRole('textbox', { 23 | name: /title/i 24 | }), 25 | { 26 | target: { 27 | value: 'Test' 28 | } 29 | } 30 | ) 31 | 32 | fireEvent.input( 33 | screen.getByRole('textbox', { 34 | name: /url/i 35 | }), 36 | { 37 | target: { 38 | value: 'https://example.com' 39 | } 40 | } 41 | ) 42 | 43 | fireEvent.click( 44 | screen.getByRole('button', { 45 | name: /Add post/i 46 | }) 47 | ) 48 | 49 | waitFor(() => 50 | expect(createPostSpy).toHaveBeenCalledWith({ 51 | title: 'Test', 52 | url: 'https://example.com' 53 | }) 54 | ) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /examples/create-react-app/test/Posts.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from './test-utils' 2 | import Posts, { allPostsQuery } from '../src/components/Posts' 3 | import React from 'react' 4 | 5 | const localQueries = { 6 | [allPostsQuery]: () => ({ 7 | allPosts: [ 8 | { 9 | id: 1, 10 | title: 'Test', 11 | url: 'https://example.com' 12 | } 13 | ] 14 | }) 15 | } 16 | 17 | describe('Posts', () => { 18 | it('should render successfully', async () => { 19 | render(, { 20 | localQueries 21 | }) 22 | 23 | expect( 24 | await screen.findByRole('link', { 25 | name: /Test/i 26 | }) 27 | ).toBeTruthy() 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /examples/create-react-app/test/setup.js: -------------------------------------------------------------------------------- 1 | if (typeof global.Response === 'undefined') { 2 | global.Response = function () {} 3 | } 4 | -------------------------------------------------------------------------------- /examples/create-react-app/test/test-utils.tsx: -------------------------------------------------------------------------------- 1 | import { ClientContext, LocalGraphQLClient } from 'graphql-hooks' 2 | import { render } from '@testing-library/react' 3 | import React from 'react' 4 | import T from 'prop-types' 5 | 6 | const customRender = (ui, options) => { 7 | const client = new LocalGraphQLClient({ 8 | localQueries: options.localQueries 9 | }) 10 | 11 | const Wrapper = ({ children }) => { 12 | return ( 13 | {children} 14 | ) 15 | } 16 | 17 | Wrapper.propTypes = { 18 | children: T.node.isRequired 19 | } 20 | 21 | return render(ui, { 22 | wrapper: Wrapper, 23 | ...options 24 | }) 25 | } 26 | 27 | export * from '@testing-library/react' 28 | 29 | export { customRender as render } 30 | -------------------------------------------------------------------------------- /examples/fastify-ssr/README.md: -------------------------------------------------------------------------------- 1 | # GraphQL Hooks Fastify SSR Example 2 | 3 | This example uses [Fastify](https://github.com/fastify/fastify) to serve a graphql server and do server side rendering. It's very basic but showcases the different functionality that `graphql-hooks` offers. This example is intended more for local development of the `graphql-hooks` packages. 4 | 5 | ## How to use 6 | 7 | ### Running as part of this repo 8 | 9 | In the root of this repository run: 10 | 11 | ```bash 12 | npm install 13 | lerna run build 14 | cd examples/fastify-ssr 15 | npm run watch 16 | ``` 17 | 18 | To develop `packages/` with this example locally, you'll need to run `lerna run build` from the root to rebuild files after they've been changed. 19 | 20 | ### Download the example in isolation: 21 | 22 | ```bash 23 | curl https://codeload.github.com/nearform/graphql-hooks/tar.gz/master | tar -xz --strip=2 graphql-hooks-master/examples/fastify-ssr 24 | cd fastify-ssr 25 | ``` 26 | 27 | Install it and run: 28 | 29 | ```bash 30 | npm install 31 | npm run watch 32 | ``` 33 | -------------------------------------------------------------------------------- /examples/fastify-ssr/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fastify-ssr", 3 | "version": "6.1.1", 4 | "private": true, 5 | "description": "", 6 | "type": "module", 7 | "engines": { 8 | "node": ">=12.0.0" 9 | }, 10 | "exports": "./index.js", 11 | "scripts": { 12 | "test": "echo \"Error: no test specified\" && exit 1", 13 | "build:client": "webpack", 14 | "build:server": "babel src -d lib --copy-files", 15 | "prebuild": "rm -rf build lib", 16 | "build": "npm run build:server && npm run build:client", 17 | "start": "node ./lib/index.js", 18 | "watch:client": "webpack --watch", 19 | "watch:server": "nodemon --watch src --ignore src/client --exec 'npm run build:server && npm run start | pino-pretty'", 20 | "prewatch": "npm run prebuild", 21 | "watch": "npm run build:client && npm run watch:server & npm run watch:client" 22 | }, 23 | "keywords": [], 24 | "author": "", 25 | "license": "ISC", 26 | "dependencies": { 27 | "@fastify/static": "^7.0.4", 28 | "babel-plugin-dynamic-import-node": "^2.2.0", 29 | "fastify": "^4.2.0", 30 | "graphql-hooks": "^8.2.0", 31 | "graphql-hooks-memcache": "^3.2.0", 32 | "graphql-hooks-ssr": "^3.0.1", 33 | "isomorphic-unfetch": "^4.0.2", 34 | "mercurius": "^14.0.0", 35 | "prop-types": "^15.7.2", 36 | "react": "^18.0.0", 37 | "react-dom": "^18.0.0", 38 | "react-router-dom": "^6.0.2" 39 | }, 40 | "devDependencies": { 41 | "@babel/cli": "^7.4.4", 42 | "@babel/core": "^7.4.5", 43 | "@babel/preset-env": "^7.4.5", 44 | "@babel/preset-react": "^7.0.0", 45 | "babel-loader": "^9.1.0", 46 | "nodemon": "^3.0.1", 47 | "pino-pretty": "^11.1.0", 48 | "webpack": "^5.64.4", 49 | "webpack-cli": "^5.0.1", 50 | "webpack-manifest-plugin": "^4.0.2", 51 | "webpack-merge": "^6.0.1" 52 | }, 53 | "browserslist": "> 0.25%, not dead", 54 | "babel": { 55 | "presets": [ 56 | [ 57 | "@babel/preset-env", 58 | { 59 | "targets": { 60 | "node": "current" 61 | }, 62 | "modules": false 63 | } 64 | ], 65 | "@babel/preset-react" 66 | ], 67 | "plugins": [ 68 | "@babel/plugin-proposal-object-rest-spread", 69 | "dynamic-import-node" 70 | ] 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /examples/fastify-ssr/src/app/AppShell.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import * as ReactRouterDom from 'react-router-dom' 3 | const { Link } = ReactRouterDom 4 | 5 | // components 6 | import NotFoundPage from './pages/NotFoundPage.js' 7 | import PaginationPage from './pages/PaginationPage.js' 8 | import HomePage from './pages/HomePage.js' 9 | 10 | class AppShell extends React.Component { 11 | render() { 12 | return ( 13 |
14 |

GraphQL Hooks

15 | 19 | 20 | 21 | 22 | 23 |
24 | ) 25 | } 26 | } 27 | 28 | AppShell.propTypes = {} 29 | 30 | export default AppShell 31 | -------------------------------------------------------------------------------- /examples/fastify-ssr/src/app/components/Hello.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import T from 'prop-types' 3 | import { useQuery } from 'graphql-hooks' 4 | 5 | const HELLO_QUERY = ` 6 | query Hello($name: String) { 7 | hello(name: $name) 8 | } 9 | ` 10 | 11 | function HelloComponent({ user }) { 12 | const { loading, error, data } = useQuery(HELLO_QUERY, { 13 | variables: { name: user.name } 14 | }) 15 | 16 | if (loading) return 'loading HelloComponent...' 17 | if (error) return 'error HelloComponent' 18 | 19 | return
{data.hello}
20 | } 21 | 22 | HelloComponent.propTypes = { 23 | user: T.shape({ 24 | name: T.string 25 | }) 26 | } 27 | 28 | export default HelloComponent 29 | -------------------------------------------------------------------------------- /examples/fastify-ssr/src/app/pages/HomePage.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react' 2 | 3 | import { useQuery, useManualQuery, useMutation } from 'graphql-hooks' 4 | 5 | // components 6 | import HelloComponent from '../components/Hello.js' 7 | 8 | const HOMEPAGE_QUERY = ` 9 | query HomepageQuery { 10 | users { 11 | name 12 | } 13 | } 14 | ` 15 | 16 | const GET_FIRST_USER_QUERY = ` 17 | query FirstUser { 18 | firstUser { 19 | name 20 | } 21 | } 22 | ` 23 | 24 | const CREATE_USER_MUTATION = ` 25 | mutation CreateUser($name: String!) { 26 | createUser(name: $name) { 27 | name 28 | } 29 | } 30 | ` 31 | 32 | function HomePage() { 33 | const [name, setName] = React.useState('') 34 | const { 35 | loading, 36 | data, 37 | error, 38 | refetch: refetchUsers 39 | } = useQuery(HOMEPAGE_QUERY) 40 | const [createUser] = useMutation(CREATE_USER_MUTATION) 41 | 42 | const [getFirstUser, { data: firstUserData }] = useManualQuery( 43 | GET_FIRST_USER_QUERY, 44 | { 45 | fetchOptionsOverrides: { 46 | method: 'GET' 47 | } 48 | } 49 | ) 50 | 51 | async function createNewUser() { 52 | await createUser({ variables: { name } }) 53 | setName('') 54 | refetchUsers() 55 | } 56 | 57 | return ( 58 |
59 | Home page 60 | {loading &&
...loading
} 61 | {error &&
error occurred
} 62 | {!loading && !error && data.users && ( 63 | 64 | List of users: 65 | {data.users.length === 0 && No users found} 66 | {!!data.users.length && ( 67 |
    68 | {data.users.map((user, i) => ( 69 |
  • {user.name}
  • 70 | ))} 71 |
72 | )} 73 | 74 |
75 | )} 76 |
77 | setName(e.target.value)} 81 | /> 82 | 83 |
84 | 87 |
First User: {firstUserData && firstUserData.firstUser.name}
88 |
89 | ) 90 | } 91 | 92 | export default HomePage 93 | -------------------------------------------------------------------------------- /examples/fastify-ssr/src/app/pages/NotFoundPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | function NotFoundPage() { 4 | return
404 - Not Found
5 | } 6 | 7 | export default NotFoundPage 8 | -------------------------------------------------------------------------------- /examples/fastify-ssr/src/app/pages/PaginationPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { useQuery } from 'graphql-hooks' 4 | 5 | const USERS_QUERY = ` 6 | query UsersQuery($skip: Int, $limit: Int) { 7 | users(skip: $skip, limit: $limit) { 8 | name 9 | } 10 | } 11 | ` 12 | 13 | function PaginationPage() { 14 | const [page, setPage] = React.useState(1) 15 | const { data } = useQuery(USERS_QUERY, { 16 | variables: { 17 | limit: 1, 18 | skip: page - 1 19 | } 20 | }) 21 | 22 | return ( 23 |
24 | User Pagination Users: 25 |
    26 | {data && 27 | data.users && 28 | data.users.map((user, i) =>
  • {user.name}
  • )} 29 |
30 | 31 | 32 |
33 | ) 34 | } 35 | 36 | export default PaginationPage 37 | -------------------------------------------------------------------------------- /examples/fastify-ssr/src/client/js/app-shell.js: -------------------------------------------------------------------------------- 1 | import { ClientContext, GraphQLClient } from 'graphql-hooks' 2 | import memCache from 'graphql-hooks-memcache' 3 | import React from 'react' 4 | import { hydrate } from 'react-dom' 5 | import { BrowserRouter } from 'react-router-dom' 6 | import AppShell from '../../app/AppShell.js' 7 | 8 | const initialState = window.__INITIAL_STATE__ 9 | const client = new GraphQLClient({ 10 | url: '/graphql', 11 | cache: memCache({ initialState }) 12 | }) 13 | 14 | const App = ( 15 | 16 | 17 | 18 | 19 | 20 | ) 21 | 22 | hydrate(App, document.getElementById('app-root')) 23 | -------------------------------------------------------------------------------- /examples/fastify-ssr/src/index.js: -------------------------------------------------------------------------------- 1 | import server from './server/index.js' 2 | server() 3 | -------------------------------------------------------------------------------- /examples/fastify-ssr/src/server/graphql/index.js: -------------------------------------------------------------------------------- 1 | import mercurius from 'mercurius' 2 | 3 | import schema from './schema.js' 4 | import resolvers from './resolvers.js' 5 | 6 | function registerGraphQL(fastify, opts, next) { 7 | fastify.register(mercurius, { 8 | schema, 9 | resolvers, 10 | graphiql: true 11 | }) 12 | 13 | next() 14 | } 15 | 16 | registerGraphQL[Symbol.for('skip-override')] = true 17 | 18 | export default registerGraphQL 19 | -------------------------------------------------------------------------------- /examples/fastify-ssr/src/server/graphql/resolvers.js: -------------------------------------------------------------------------------- 1 | const users = [ 2 | { 3 | name: 'Brian' 4 | }, 5 | { 6 | name: 'Jack' 7 | }, 8 | { 9 | name: 'Joe' 10 | } 11 | ] 12 | 13 | const resolvers = { 14 | Query: { 15 | users: (_, { skip = 0, limit }) => { 16 | const end = limit ? skip + limit : undefined 17 | return users.slice(skip, end) 18 | }, 19 | firstUser: () => users[0], 20 | hello: (_, { name }) => `Hello ${name}` 21 | }, 22 | Mutation: { 23 | createUser: (_, user) => { 24 | users.push(user) 25 | return user 26 | } 27 | } 28 | } 29 | 30 | export default resolvers 31 | -------------------------------------------------------------------------------- /examples/fastify-ssr/src/server/graphql/schema.js: -------------------------------------------------------------------------------- 1 | const schema = ` 2 | type User { 3 | name: String 4 | } 5 | 6 | type Query { 7 | users(skip: Int, limit: Int): [User] 8 | firstUser: User 9 | hello(name: String): String 10 | } 11 | 12 | type Mutation { 13 | createUser(name: String!): User 14 | } 15 | ` 16 | 17 | export default schema 18 | -------------------------------------------------------------------------------- /examples/fastify-ssr/src/server/handlers/app-shell.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOMServer from 'react-dom/server.js' 3 | import ReactRouterDom from 'react-router-dom' 4 | const { StaticRouter } = ReactRouterDom 5 | 6 | // graphql-hooks 7 | import { getInitialState } from 'graphql-hooks-ssr' 8 | import { GraphQLClient, ClientContext } from 'graphql-hooks' 9 | import memCache from 'graphql-hooks-memcache' 10 | 11 | // components 12 | import AppShell from '../../app/AppShell.js' 13 | 14 | // helpers 15 | import { getBundlePath } from '../helpers/manifest.js' 16 | import unfetch from 'isomorphic-unfetch' 17 | 18 | function renderHead() { 19 | return ` 20 | 21 | Hello World! 22 | 23 | ` 24 | } 25 | 26 | async function renderScripts({ initialState }) { 27 | const appShellBundlePath = await getBundlePath('app-shell.js') 28 | return ` 29 | 35 | 36 | ` 37 | } 38 | 39 | async function appShellHandler(req, reply) { 40 | const head = renderHead() 41 | 42 | const client = new GraphQLClient({ 43 | url: 'http://127.0.0.1:3000/graphql', 44 | cache: memCache(), 45 | fetch: unfetch, 46 | logErrors: true 47 | }) 48 | 49 | const App = () => ( 50 | 51 | 52 | 53 | 54 | 55 | ) 56 | 57 | const initialState = await getInitialState({ App, client }) 58 | const content = ReactDOMServer.renderToString(App) 59 | const scripts = await renderScripts({ initialState }) 60 | 61 | const html = ` 62 | 63 | 64 | ${head} 65 | 66 |
${content}
67 | ${scripts} 68 | 69 | 70 | ` 71 | 72 | reply.type('text/html').send(html) 73 | } 74 | 75 | export default appShellHandler 76 | -------------------------------------------------------------------------------- /examples/fastify-ssr/src/server/helpers/manifest.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import { promisify } from 'util' 4 | 5 | const readFileAsync = promisify(fs.readFile) 6 | 7 | const manifestPath = path.join(process.cwd(), 'build/public/js/manifest.json') 8 | let cachedManifest 9 | 10 | getManifest() 11 | 12 | export async function getManifest() { 13 | if (cachedManifest) { 14 | return cachedManifest 15 | } 16 | 17 | try { 18 | /* eslint-disable-next-line require-atomic-updates */ 19 | cachedManifest = JSON.parse(await readFileAsync(manifestPath)) 20 | return cachedManifest 21 | } catch (error) { 22 | if (error.code !== 'ENOENT') { 23 | throw error 24 | } 25 | 26 | return {} 27 | } 28 | } 29 | 30 | export async function getBundlePath(manifestKey) { 31 | const manifest = await getManifest() 32 | return manifest[manifestKey] || manifestKey 33 | } 34 | -------------------------------------------------------------------------------- /examples/fastify-ssr/src/server/index.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import fastify from 'fastify' 3 | 4 | // plugins 5 | import graphqlPlugin from './graphql/index.js' 6 | import fastifyStatic from '@fastify/static' 7 | 8 | // handlers 9 | import appShellHandler from './handlers/app-shell.js' 10 | 11 | const startServer = () => { 12 | const app = fastify({ 13 | logger: true 14 | }) 15 | 16 | app.register(fastifyStatic, { 17 | root: path.join(process.cwd(), 'build/public') 18 | }) 19 | 20 | app.register(graphqlPlugin) 21 | 22 | app.get('/', appShellHandler) 23 | app.get('/users', appShellHandler) 24 | 25 | app.listen({ port: 3000 }) 26 | } 27 | 28 | export default startServer 29 | -------------------------------------------------------------------------------- /examples/fastify-ssr/webpack.config.js: -------------------------------------------------------------------------------- 1 | import path, { dirname } from 'path' 2 | import { merge } from 'webpack-merge' 3 | import { WebpackManifestPlugin } from 'webpack-manifest-plugin' 4 | import { fileURLToPath } from 'url' 5 | const __dirname = dirname(fileURLToPath(import.meta.url)) 6 | 7 | const PATHS = { 8 | build: path.join(__dirname, 'build'), 9 | src: path.join(__dirname, 'src'), 10 | node_modules: path.join(__dirname, 'node_modules') 11 | } 12 | 13 | const commonConfig = { 14 | entry: { 15 | 'app-shell': path.join(PATHS.src, 'client/js/app-shell.js') 16 | }, 17 | devtool: 'cheap-module-source-map', 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.(js|jsx)$/, 22 | loader: 'babel-loader', 23 | options: { 24 | babelrc: false, 25 | presets: [ 26 | [ 27 | '@babel/preset-env', 28 | { 29 | targets: '> 5%, not dead' 30 | } 31 | ], 32 | '@babel/preset-react' 33 | ], 34 | plugins: [ 35 | '@babel/plugin-proposal-object-rest-spread', 36 | 'dynamic-import-node' 37 | ] 38 | } 39 | } 40 | ] 41 | }, 42 | resolve: { 43 | extensions: ['.js', '.jsx', '.json'], 44 | modules: [PATHS.src, 'node_modules'], 45 | symlinks: false 46 | }, 47 | output: { 48 | filename: '[name].js', 49 | chunkFilename: '[name].js', 50 | publicPath: '/js/', 51 | path: path.join(PATHS.build, 'public/js') 52 | }, 53 | plugins: [new WebpackManifestPlugin()] 54 | } 55 | 56 | const productionConfig = { 57 | mode: 'production', 58 | optimization: { 59 | minimize: false 60 | }, 61 | output: { 62 | filename: '[chunkhash].[name].js', 63 | chunkFilename: '[chunkhash].[name].js', 64 | publicPath: '/js/', 65 | path: path.join(PATHS.build, 'public/js') 66 | } 67 | } 68 | 69 | const developmentConfig = { 70 | mode: 'development' 71 | } 72 | 73 | const config = () => { 74 | if ( 75 | process.env.NODE_ENV === 'production' || 76 | process.env.NODE_ENV === 'staging' 77 | ) { 78 | return merge(commonConfig, productionConfig) 79 | } 80 | 81 | return merge(commonConfig, developmentConfig) 82 | } 83 | 84 | export default config 85 | -------------------------------------------------------------------------------- /examples/full-ws-transport/README.md: -------------------------------------------------------------------------------- 1 | # Full WS transport Example 2 | 3 | An example showing how to send every operation (subscription, query, mutation) via WebSocket. 4 | 5 | ## Usage 6 | 7 | Run `npm start` to start the app. 8 | 9 | Run `npm run server` to start the server. 10 | -------------------------------------------------------------------------------- /examples/full-ws-transport/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-hooks-full-ws-transport-example", 3 | "version": "3.1.1", 4 | "private": true, 5 | "dependencies": { 6 | "fastify": "^4.2.0", 7 | "graphql-hooks": "^8.2.0", 8 | "graphql-ws": "^5.5.5", 9 | "mercurius": "^14.0.0", 10 | "react": "^18.0.0", 11 | "react-dom": "^18.0.0", 12 | "react-scripts": "^5.0.0" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "server": "node server/index.js" 17 | }, 18 | "browserslist": { 19 | "production": [ 20 | ">0.2%", 21 | "not dead", 22 | "not op_mini all" 23 | ], 24 | "development": [ 25 | "last 1 chrome version", 26 | "last 1 firefox version", 27 | "last 1 safari version" 28 | ] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/full-ws-transport/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nearform/graphql-hooks/5a124588a3ce684601712bb7076ba2162322c937/examples/full-ws-transport/public/favicon.ico -------------------------------------------------------------------------------- /examples/full-ws-transport/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | GraphQL hooks - Full WS transport Example 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/full-ws-transport/server/index.js: -------------------------------------------------------------------------------- 1 | import Fastify from 'fastify' 2 | import mercurius from 'mercurius' 3 | 4 | const app = Fastify({ logger: true }) 5 | 6 | let x = 0 7 | 8 | const schema = ` 9 | type ResultChange { 10 | operation: String 11 | prev: Int 12 | current: Int 13 | } 14 | type Query { 15 | result: Int 16 | } 17 | type Mutation { 18 | add(num: Int): Int 19 | subtract(num: Int): Int 20 | } 21 | type Subscription { 22 | onResultChange: ResultChange 23 | } 24 | ` 25 | 26 | const resolvers = { 27 | Query: { 28 | result: async () => { 29 | return x 30 | } 31 | }, 32 | Mutation: { 33 | add: async (_, args, { pubsub }) => { 34 | const prev = x 35 | const { num } = args 36 | 37 | x = prev + num 38 | 39 | pubsub.publish({ 40 | topic: 'RESULT_TOPIC', 41 | payload: { 42 | onResultChange: { 43 | operation: 'add', 44 | prev, 45 | current: x 46 | } 47 | } 48 | }) 49 | 50 | return x 51 | }, 52 | subtract: async (_, args, { pubsub }) => { 53 | const prev = x 54 | const { num } = args 55 | 56 | x = prev - num 57 | 58 | pubsub.publish({ 59 | topic: 'RESULT_TOPIC', 60 | payload: { 61 | onResultChange: { 62 | operation: 'subtract', 63 | prev, 64 | current: x 65 | } 66 | } 67 | }) 68 | 69 | return x 70 | } 71 | }, 72 | Subscription: { 73 | onResultChange: { 74 | subscribe: async (_, __, { pubsub }) => { 75 | return await pubsub.subscribe('RESULT_TOPIC') 76 | } 77 | } 78 | } 79 | } 80 | 81 | app.register(mercurius, { 82 | schema, 83 | resolvers, 84 | graphiql: true, 85 | subscription: { 86 | fullWsTransport: true 87 | } 88 | }) 89 | 90 | app.listen({ port: 4000 }) 91 | -------------------------------------------------------------------------------- /examples/full-ws-transport/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } -------------------------------------------------------------------------------- /examples/full-ws-transport/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { useQuery, useMutation, useSubscription } from 'graphql-hooks' 3 | 4 | const RESULT = `query GetResult { 5 | result 6 | }` 7 | 8 | const ADD = `mutation AddValue($num: Int) { 9 | add(num: $num) 10 | }` 11 | 12 | const SUBTRACT = `mutation SubtractValue($num: Int) { 13 | subtract(num: $num) 14 | }` 15 | 16 | const ON_RESULT_CHANGE = `subscription OnResultChange { 17 | onResultChange { 18 | operation 19 | prev 20 | current 21 | } 22 | }` 23 | 24 | export default function App() { 25 | const { isLoading, data, refetch } = useQuery(RESULT) 26 | 27 | const [addMutation] = useMutation(ADD) 28 | 29 | const [subtractMutation] = useMutation(SUBTRACT) 30 | 31 | const [resultState, setResultState] = useState({ 32 | operation: '', 33 | prev: '', 34 | current: '' 35 | }) 36 | 37 | useSubscription( 38 | { 39 | query: ON_RESULT_CHANGE 40 | }, 41 | ({ data: { onResultChange }, errors }) => { 42 | if (errors && errors.length > 0) { 43 | console.log(errors[0]) 44 | } 45 | if (onResultChange) { 46 | setResultState(onResultChange) 47 | refetch() 48 | } 49 | } 50 | ) 51 | 52 | return ( 53 |
54 |
55 |

{isLoading ? 'Loading…' : data?.result}

56 |
57 |
58 | 65 | 72 |
73 |
{JSON.stringify(resultState, null, 2)}
74 |
75 | ) 76 | } 77 | -------------------------------------------------------------------------------- /examples/full-ws-transport/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | pre { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | font-size: 24px; 14 | } 15 | 16 | .app { 17 | display: flex; 18 | flex-direction: column; 19 | align-items: center; 20 | justify-content: center; 21 | height: 100vh; 22 | font-size: 48px; 23 | } 24 | 25 | .buttons button { 26 | font-size: 60px; 27 | width: 80px; 28 | height: 80px; 29 | margin: 0 40px; 30 | } 31 | -------------------------------------------------------------------------------- /examples/full-ws-transport/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { createClient } from 'graphql-ws' 4 | import { GraphQLClient, ClientContext } from 'graphql-hooks' 5 | import App from './App' 6 | 7 | import './index.css' 8 | 9 | const subscriptionClient = createClient({ 10 | url: 'ws://localhost:4000/graphql', 11 | lazy: false 12 | }) 13 | 14 | const client = new GraphQLClient({ 15 | fullWsTransport: true, 16 | subscriptionClient 17 | }) 18 | 19 | ReactDOM.render( 20 | 21 | 22 | 23 | 24 | , 25 | document.getElementById('root') 26 | ) 27 | -------------------------------------------------------------------------------- /examples/persisted-queries/README.md: -------------------------------------------------------------------------------- 1 | # Persisted Queries Example 2 | 3 | An example showing the Automatic Persisted Queries (APQ) middleware. 4 | 5 | ## Usage 6 | 7 | cd `server` and `npm run start:mercurius` to start a [Mercurius](https://mercurius.dev/) server or `npm run start:apollo` to start an [Apollo](https://www.apollographql.com/docs/apollo-server/) server. 8 | 9 | cd `client` and `npm start` to start the client. 10 | -------------------------------------------------------------------------------- /examples/persisted-queries/client/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | 46 | ### Code Splitting 47 | 48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 49 | 50 | ### Analyzing the Bundle Size 51 | 52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 53 | 54 | ### Making a Progressive Web App 55 | 56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 57 | 58 | ### Advanced Configuration 59 | 60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 61 | 62 | ### Deployment 63 | 64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 65 | 66 | ### `npm run build` fails to minify 67 | 68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 69 | -------------------------------------------------------------------------------- /examples/persisted-queries/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "persisted-queries-client", 3 | "version": "4.1.1", 4 | "private": true, 5 | "dependencies": { 6 | "@types/jest": "^29.0.3", 7 | "@types/node": "^22.10.5", 8 | "@types/react": "^18.0.0", 9 | "@types/react-dom": "^18.0.0", 10 | "graphql-hooks": "^8.2.0", 11 | "graphql-hooks-memcache": "^3.2.0", 12 | "react": "^18.0.0", 13 | "react-dom": "^18.0.0", 14 | "react-scripts": "^5.0.0", 15 | "typescript": "^4.1.2" 16 | }, 17 | "scripts": { 18 | "start": "react-scripts start", 19 | "build": "react-scripts build", 20 | "test": "react-scripts test", 21 | "eject": "react-scripts eject" 22 | }, 23 | "browserslist": [ 24 | ">0.2%", 25 | "not dead", 26 | "not ie <= 11", 27 | "not op_mini all" 28 | ], 29 | "eslintConfig": { 30 | "extends": [ 31 | "react-app", 32 | "react-app/jest" 33 | ] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/persisted-queries/client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nearform/graphql-hooks/5a124588a3ce684601712bb7076ba2162322c937/examples/persisted-queries/client/public/favicon.ico -------------------------------------------------------------------------------- /examples/persisted-queries/client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 25 | Persisted Queries | A GraphQL Hooks Example 26 | 27 | 28 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /examples/persisted-queries/client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Postie.ts", 3 | "name": "Postie.ts | A GraphQL Hooks Example", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /examples/persisted-queries/client/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { ClientContext, GraphQLClient, useManualQuery } from 'graphql-hooks' 2 | import { APQMiddleware } from 'graphql-hooks/lib/middlewares/apqMiddleware' 3 | import { useState } from 'react' 4 | 5 | const client = new GraphQLClient({ 6 | url: `http://localhost:${process.env.REACT_APP_SERVER_PORT ?? 8000}/graphql`, 7 | middleware: [APQMiddleware] 8 | }) 9 | 10 | export const addQuery = ` 11 | query add($x: Int!, $y: Int!) { add(x: $x, y: $y) } 12 | ` 13 | 14 | function Add() { 15 | const [x, setX] = useState('') 16 | const [y, setY] = useState('') 17 | const [add, { error, data }] = useManualQuery(addQuery, { 18 | variables: { 19 | x: isNaN(Number(x)) ? 0 : Number(x), 20 | y: isNaN(Number(y)) ? 0 : Number(y) 21 | } 22 | }) 23 | 24 | return ( 25 |
26 | 30 | 34 | 35 | {error ?

There was an error.

: data ?

{data.add}

: null} 36 |
37 | ) 38 | } 39 | 40 | export default function App() { 41 | return ( 42 | 43 |

Persisted Queries

44 | 45 |
46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /examples/persisted-queries/client/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 5 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 13 | monospace; 14 | } 15 | -------------------------------------------------------------------------------- /examples/persisted-queries/client/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import './index.css' 4 | import App from './App' 5 | 6 | ReactDOM.render(, document.getElementById('root')) 7 | -------------------------------------------------------------------------------- /examples/persisted-queries/client/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/persisted-queries/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react-jsx", 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /examples/persisted-queries/server/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [3.0.1](https://github.com/nearform/graphql-hooks/compare/persisted-queries-server@3.0.0...persisted-queries-server@3.0.1) (2025-01-08) 7 | 8 | **Note:** Version bump only for package persisted-queries-server 9 | 10 | 11 | 12 | 13 | 14 | # [3.0.0](https://github.com/nearform/graphql-hooks/compare/persisted-queries-server@2.0.5...persisted-queries-server@3.0.0) (2024-06-07) 15 | 16 | **Note:** Version bump only for package persisted-queries-server 17 | 18 | 19 | 20 | 21 | 22 | ## [2.0.5](https://github.com/nearform/graphql-hooks/compare/persisted-queries-server@2.0.4...persisted-queries-server@2.0.5) (2024-04-11) 23 | 24 | **Note:** Version bump only for package persisted-queries-server 25 | 26 | 27 | 28 | 29 | 30 | ## [2.0.4](https://github.com/nearform/graphql-hooks/compare/persisted-queries-server@2.0.3...persisted-queries-server@2.0.4) (2023-10-02) 31 | 32 | **Note:** Version bump only for package persisted-queries-server 33 | 34 | 35 | 36 | 37 | 38 | ## [2.0.3](https://github.com/nearform/graphql-hooks/compare/persisted-queries-server@2.0.2...persisted-queries-server@2.0.3) (2023-07-18) 39 | 40 | **Note:** Version bump only for package persisted-queries-server 41 | 42 | 43 | 44 | 45 | 46 | ## [2.0.2](https://github.com/nearform/graphql-hooks/compare/persisted-queries-server@2.0.1...persisted-queries-server@2.0.2) (2023-04-26) 47 | 48 | **Note:** Version bump only for package persisted-queries-server 49 | 50 | 51 | 52 | 53 | 54 | ## [2.0.1](https://github.com/nearform/graphql-hooks/compare/persisted-queries-server@2.0.0...persisted-queries-server@2.0.1) (2023-03-31) 55 | 56 | **Note:** Version bump only for package persisted-queries-server 57 | 58 | 59 | 60 | 61 | 62 | # [2.0.0](https://github.com/nearform/graphql-hooks/compare/persisted-queries-server@1.0.1...persisted-queries-server@2.0.0) (2022-09-26) 63 | 64 | **Note:** Version bump only for package persisted-queries-server 65 | 66 | 67 | 68 | 69 | 70 | ## 1.0.1 (2022-05-05) 71 | 72 | 73 | ### Bug Fixes 74 | 75 | * update error handling in apq middleware ([#851](https://github.com/nearform/graphql-hooks/issues/851)) ([8f6c178](https://github.com/nearform/graphql-hooks/commit/8f6c1787b19b9d6d93f221de2f63314ffd60ac9d)) 76 | 77 | 78 | ### Features 79 | 80 | * add apq example ([#848](https://github.com/nearform/graphql-hooks/issues/848)) ([edef262](https://github.com/nearform/graphql-hooks/commit/edef262309185d209e52e278e4e8267320fa8275)) 81 | -------------------------------------------------------------------------------- /examples/persisted-queries/server/apollo.ts: -------------------------------------------------------------------------------- 1 | import { ApolloServer, gql } from 'apollo-server' 2 | 3 | const typeDefs = gql` 4 | type Query { 5 | add(x: Int!, y: Int!): Int! 6 | } 7 | ` 8 | 9 | const resolvers = { 10 | Query: { 11 | add: async (_: unknown, obj: { x: number; y: number }) => { 12 | const { x, y } = obj 13 | return x + y 14 | } 15 | } 16 | } 17 | 18 | const server = new ApolloServer({ typeDefs, resolvers }) 19 | 20 | server 21 | .listen({ port: process.env.SERVER_PORT ?? 8000 }) 22 | .then(({ url }: { url: string }) => { 23 | console.log(`Apollo Server ready at ${url}`) 24 | }) 25 | -------------------------------------------------------------------------------- /examples/persisted-queries/server/mercurius.ts: -------------------------------------------------------------------------------- 1 | import fastify from 'fastify' 2 | import cors from '@fastify/cors' 3 | import mercurius from 'mercurius' 4 | 5 | const app = fastify({ 6 | logger: true 7 | }) 8 | 9 | const schema = ` 10 | type Query { 11 | add(x: Int!, y: Int!): Int! 12 | } 13 | ` 14 | 15 | const resolvers = { 16 | Query: { 17 | add: async (_: unknown, obj: { x: number; y: number }) => { 18 | const { x, y } = obj 19 | return x + y 20 | } 21 | } 22 | } 23 | 24 | app.register(cors, { 25 | origin: `http://localhost:${process.env.PORT ?? 3000}` 26 | }) 27 | 28 | app.register(mercurius, { 29 | schema, 30 | resolvers, 31 | persistedQueryProvider: mercurius.persistedQueryDefaults.automatic(), 32 | graphiql: true 33 | }) 34 | 35 | async function start() { 36 | try { 37 | return app.listen({ 38 | port: parseInt(process.env.SERVER_PORT || '8080', 10) 39 | }) 40 | } catch (err) { 41 | app.log.error(err) 42 | process.exit(1) 43 | } 44 | } 45 | 46 | start() 47 | -------------------------------------------------------------------------------- /examples/persisted-queries/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "persisted-queries-server", 3 | "version": "3.0.1", 4 | "description": "", 5 | "main": "index.ts", 6 | "private": true, 7 | "scripts": { 8 | "start:mercurius": "ts-node-dev --project ./tsconfig.json mercurius.ts", 9 | "start:apollo": "ts-node-dev --project ./tsconfig.json apollo.ts" 10 | }, 11 | "author": "", 12 | "license": "MIT", 13 | "dependencies": { 14 | "@fastify/cors": "^9.0.1", 15 | "apollo-server": "^3.6.7", 16 | "fastify": "^4.2.0", 17 | "graphql": "^16.6.0", 18 | "mercurius": "^14.0.0" 19 | }, 20 | "devDependencies": { 21 | "@tsconfig/node16": "^16.1.0", 22 | "@types/node": "^22.10.5", 23 | "ts-node-dev": "^2.0.0", 24 | "typescript": "^4.7.4" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/persisted-queries/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node16/tsconfig.json", 3 | "compilerOptions": { 4 | "resolveJsonModule": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/subscription/.gitignore: -------------------------------------------------------------------------------- 1 | votes.json 2 | -------------------------------------------------------------------------------- /examples/subscription/README.md: -------------------------------------------------------------------------------- 1 | # GraphQL Subscriptions Example 2 | 3 | ## Quick start 4 | 5 | 1. Run `npm install` from the root (`../../`) 6 | 1. Start a redis server on port `6379`: `docker run --name some-redis -p 6379:6379 redis` 7 | 1. Run `npm start` 8 | 1. Visit [http://localhost:8000](http://localhost:8000) 9 | 1. Each Vote is isolated, voting yes/no will auto update the total votes using via graphql subscriptions. You can also open the same url in a new tab, add more votes and the changes will be reflected across each app instance. 10 | 11 | ## Running the application 12 | 13 | ### Server 14 | 15 | Make sure that there is a **`Redis`** server running on `127.0.0.1:6379`. Or update the configuration in `/src/server/index.js` file. 16 | 17 | In the `examples/subscription` folder, run the following command 18 | 19 | `node src/server/index.js` 20 | 21 | ### Client 22 | 23 | In the `examples/subscription` folder, run the following command 24 | 25 | `npm run build` 26 | 27 | Open `http://localhost:8000` 28 | 29 | ## Running multiple servers 30 | 31 | The data is persisted and it is possible to run the example in a multi-server set up by starting up the server with the following command 32 | 33 | `APP_PORT=PORT node src/server/index.js` 34 | 35 | where `PORT` is the port number you want the application to listen on. 36 | -------------------------------------------------------------------------------- /examples/subscription/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "subscription", 3 | "version": "5.1.1", 4 | "private": true, 5 | "type": "module", 6 | "engines": { 7 | "node": ">=12.0.0" 8 | }, 9 | "description": "", 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1", 12 | "watch:client": "webpack --watch", 13 | "start:server": "node src/server/index.js", 14 | "start": "npm run build && npm run start:server && npm run watch:client", 15 | "build": "webpack" 16 | }, 17 | "dependencies": { 18 | "@fastify/cors": "^9.0.1", 19 | "@fastify/static": "^7.0.4", 20 | "babel-plugin-dynamic-import-node": "^2.2.0", 21 | "fastify": "^4.2.0", 22 | "graphql": "^16.6.0", 23 | "graphql-hooks": "^8.2.0", 24 | "graphql-hooks-memcache": "^3.2.0", 25 | "lowdb": "^7.0.1", 26 | "mercurius": "^14.0.0", 27 | "mqemitter-redis": "^5.0.0", 28 | "react": "^18.0.0", 29 | "react-dom": "^18.0.0", 30 | "subscriptions-transport-ws": "^0.11.0" 31 | }, 32 | "devDependencies": { 33 | "@babel/cli": "^7.4.4", 34 | "@babel/core": "^7.4.5", 35 | "@babel/preset-env": "^7.4.5", 36 | "@babel/preset-react": "^7.0.0", 37 | "babel-loader": "^9.1.0", 38 | "webpack": "^5.64.4", 39 | "webpack-cli": "^5.0.1", 40 | "webpack-manifest-plugin": "^4.0.2", 41 | "webpack-merge": "^6.0.1" 42 | }, 43 | "author": "", 44 | "license": "ISC", 45 | "browserslist": "> 0.25%, not dead", 46 | "babel": { 47 | "presets": [ 48 | [ 49 | "@babel/preset-env", 50 | { 51 | "targets": { 52 | "node": "current" 53 | }, 54 | "modules": false 55 | } 56 | ], 57 | "@babel/preset-react" 58 | ], 59 | "plugins": [ 60 | "@babel/plugin-proposal-object-rest-spread", 61 | "dynamic-import-node" 62 | ] 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /examples/subscription/src/client/App.js: -------------------------------------------------------------------------------- 1 | /* eslint react/prop-types: 0 */ 2 | import React, { useState } from 'react' 3 | import { useQuery, useSubscription, useMutation } from 'graphql-hooks' 4 | 5 | const GET_VOTES = ` 6 | query { 7 | votes { 8 | id 9 | title 10 | ayes 11 | noes 12 | } 13 | } 14 | ` 15 | 16 | const VOTE_ADDED = ` 17 | subscription VoteAdded($voteId: ID!) { 18 | voteAdded(voteId: $voteId) { 19 | id 20 | title 21 | ayes 22 | noes 23 | } 24 | } 25 | ` 26 | 27 | const VOTE_AYE = ` 28 | mutation VoteAye($voteId: ID!) { 29 | voteAye(voteId: $voteId) { 30 | id 31 | title 32 | ayes 33 | noes 34 | } 35 | } 36 | ` 37 | 38 | const VOTE_NO = ` 39 | mutation VoteNo($voteId: ID!) { 40 | voteNo(voteId: $voteId) { 41 | id 42 | title 43 | ayes 44 | noes 45 | } 46 | } 47 | ` 48 | 49 | const App = () => { 50 | const { loading, data } = useQuery(GET_VOTES) 51 | 52 | if (!data) { 53 | return null 54 | } 55 | 56 | if (loading) { 57 | return Loading ... 58 | } 59 | 60 | return 61 | } 62 | 63 | function Votes(props) { 64 | const { votes } = props 65 | 66 | return ( 67 |
68 |
    69 | {votes.map(vote => ( 70 | 71 | ))} 72 |
73 |
74 | ) 75 | } 76 | 77 | function Vote(props) { 78 | const [vote, setVote] = useState(props.vote) 79 | 80 | const handleSubscription = ({ data: { voteAdded }, errors }) => { 81 | if (errors && errors.length > 0) { 82 | console.log(errors[0]) 83 | } 84 | if (voteAdded) { 85 | setVote(voteAdded) 86 | } 87 | } 88 | useSubscription( 89 | { 90 | query: VOTE_ADDED, 91 | variables: { voteId: vote.id } 92 | }, 93 | handleSubscription 94 | ) 95 | 96 | const [voteAye] = useMutation(VOTE_AYE) 97 | const [voteNo] = useMutation(VOTE_NO) 98 | 99 | return ( 100 |
  • 110 |

    {vote.title}

    111 |

    Total votes: {vote.ayes + vote.noes}

    112 |
    113 |
    114 |

    Ayes

    115 |

    {vote.ayes}

    116 | 119 |
    120 |
    121 |

    Noes

    122 |

    {vote.noes}

    123 | 126 |
    127 |
    128 |
  • 129 | ) 130 | } 131 | 132 | export default App 133 | -------------------------------------------------------------------------------- /examples/subscription/src/client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { GraphQLClient, ClientContext } from 'graphql-hooks' 4 | import memCache from 'graphql-hooks-memcache' 5 | import { SubscriptionClient } from 'subscriptions-transport-ws' 6 | import App from './App.js' 7 | 8 | const host = window.location.host 9 | 10 | const client = new GraphQLClient({ 11 | url: `http://${host}/graphql`, 12 | cache: memCache(), 13 | subscriptionClient: new SubscriptionClient(`ws://${host}/graphql`, { 14 | reconnect: true 15 | }) 16 | }) 17 | 18 | ReactDOM.render( 19 | 20 | 21 | , 22 | document.getElementById('root') 23 | ) 24 | -------------------------------------------------------------------------------- /examples/subscription/src/server/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | useSubscriptions 8 | 9 | 16 | 17 | 18 |
    19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/subscription/src/server/index.js: -------------------------------------------------------------------------------- 1 | import fastify from 'fastify' 2 | import mercurius from 'mercurius' 3 | import { join, dirname } from 'path' 4 | import fs from 'fs' 5 | import fastifyStatic from '@fastify/static' 6 | import { LowSync } from 'lowdb' 7 | import { JSONFileSync } from 'lowdb/node' 8 | import mq from 'mqemitter-redis' 9 | import { fileURLToPath } from 'url' 10 | import cors from '@fastify/cors' 11 | 12 | const __dirname = dirname(fileURLToPath(import.meta.url)) 13 | 14 | const emitter = mq({ 15 | port: 6379, 16 | host: '127.0.0.1' 17 | }) 18 | 19 | const NUMBER_OF_VOTES = 5 20 | const APP_PORT = parseInt(process.env.APP_PORT || '8000', 10) 21 | 22 | const app = fastify() 23 | const indexHtml = fs.readFileSync(join(__dirname, './index.html'), { 24 | encoding: 'utf8' 25 | }) 26 | 27 | app.register(fastifyStatic, { 28 | root: join(__dirname, '../../build/public'), 29 | prefix: '/public/' 30 | }) 31 | 32 | const votes = [] 33 | for (let i = 1; i <= NUMBER_OF_VOTES; i++) { 34 | votes.push({ id: i, title: `Vote #${i}`, ayes: 0, noes: 0 }) 35 | } 36 | const defaults = { votes } 37 | 38 | const adapter = new JSONFileSync('votes.json') 39 | const db = new LowSync(adapter) 40 | await db.read() 41 | db.data = db.data || defaults 42 | 43 | app.register(cors, { 44 | origin: '*' 45 | }) 46 | 47 | const VOTE_ADDED = 'VOTE_ADDED' 48 | 49 | const schema = ` 50 | type Vote { 51 | id: ID! 52 | title: String! 53 | ayes: Int 54 | noes: Int 55 | } 56 | 57 | type Query { 58 | votes: [Vote] 59 | } 60 | 61 | type Mutation { 62 | voteAye(voteId: ID!): Vote 63 | voteNo(voteId: ID!): Vote 64 | } 65 | 66 | type Subscription { 67 | voteAdded(voteId: ID!): Vote 68 | } 69 | ` 70 | 71 | const resolvers = { 72 | Query: { 73 | votes: async () => db.data.votes 74 | }, 75 | Mutation: { 76 | voteAye: async (_, { voteId }, { pubsub }) => { 77 | const vote = db.data.votes[voteId - 1] 78 | 79 | if (vote) { 80 | const v = { ...vote } 81 | v.ayes++ 82 | db.data.votes[voteId - 1] = v 83 | await db.write() 84 | await pubsub.publish({ 85 | topic: `${VOTE_ADDED}_${voteId}`, 86 | payload: { 87 | voteAdded: v 88 | } 89 | }) 90 | 91 | return v 92 | } 93 | 94 | throw new Error('Invalid vote id') 95 | }, 96 | voteNo: async (_, { voteId }, { pubsub }) => { 97 | const vote = db.data.votes[voteId - 1] 98 | 99 | if (vote) { 100 | const v = { ...vote } 101 | v.noes++ 102 | db.data.votes[voteId - 1] = v 103 | await db.write() 104 | 105 | await pubsub.publish({ 106 | topic: `VOTE_ADDED_${voteId}`, 107 | payload: { 108 | voteAdded: v 109 | } 110 | }) 111 | return v 112 | } 113 | 114 | throw new Error('Invalid vote id') 115 | } 116 | }, 117 | Subscription: { 118 | voteAdded: { 119 | subscribe: async (root, args, context) => { 120 | const { pubsub } = context 121 | return await pubsub.subscribe(`VOTE_ADDED_${args.voteId}`) 122 | } 123 | } 124 | } 125 | } 126 | 127 | app.register(mercurius, { 128 | schema, 129 | resolvers, 130 | subscription: { 131 | emitter 132 | } 133 | }) 134 | 135 | app.get('/', async function (req, reply) { 136 | reply.header('Content-Type', 'text/html').send(indexHtml) 137 | }) 138 | 139 | app.listen({ port: APP_PORT }) 140 | -------------------------------------------------------------------------------- /examples/subscription/webpack.config.js: -------------------------------------------------------------------------------- 1 | import path, { dirname } from 'path' 2 | import { merge } from 'webpack-merge' 3 | import { WebpackManifestPlugin } from 'webpack-manifest-plugin' 4 | import { fileURLToPath } from 'url' 5 | const __dirname = dirname(fileURLToPath(import.meta.url)) 6 | 7 | const PATHS = { 8 | build: path.join(__dirname, 'build'), 9 | src: path.join(__dirname, 'src'), 10 | node_modules: path.join(__dirname, 'node_modules') 11 | } 12 | 13 | const commonConfig = { 14 | entry: { 15 | 'app-shell': path.join(PATHS.src, 'client/index.js') 16 | }, 17 | devtool: 'cheap-module-source-map', 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.(js|jsx)$/, 22 | loader: 'babel-loader', 23 | options: { 24 | babelrc: false, 25 | presets: [ 26 | [ 27 | '@babel/preset-env', 28 | { 29 | targets: '> 5%, not dead' 30 | } 31 | ], 32 | '@babel/preset-react' 33 | ], 34 | plugins: [ 35 | '@babel/plugin-proposal-object-rest-spread', 36 | 'dynamic-import-node' 37 | ] 38 | } 39 | } 40 | ] 41 | }, 42 | resolve: { 43 | extensions: ['.js', '.jsx', '.json'], 44 | modules: [PATHS.src, 'node_modules'], 45 | symlinks: false 46 | }, 47 | output: { 48 | filename: '[name].js', 49 | chunkFilename: '[name].js', 50 | publicPath: '/js/', 51 | path: path.join(PATHS.build, 'public/js') 52 | }, 53 | plugins: [new WebpackManifestPlugin()], 54 | mode: 'development' 55 | } 56 | 57 | const productionConfig = { 58 | mode: 'production', 59 | optimization: { 60 | minimize: false 61 | }, 62 | output: { 63 | filename: '[chunkhash].[name].js', 64 | chunkFilename: '[chunkhash].[name].js', 65 | publicPath: '/js/', 66 | path: path.join(PATHS.build, 'public/js') 67 | } 68 | } 69 | 70 | const developmentConfig = { 71 | mode: 'development' 72 | } 73 | 74 | const config = () => { 75 | if ( 76 | process.env.NODE_ENV === 'production' || 77 | process.env.NODE_ENV === 'staging' 78 | ) { 79 | return merge(commonConfig, productionConfig) 80 | } 81 | 82 | return merge(commonConfig, developmentConfig) 83 | } 84 | 85 | export default config 86 | -------------------------------------------------------------------------------- /examples/typescript/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true 2 | -------------------------------------------------------------------------------- /examples/typescript/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
    10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
    13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
    18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
    23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
    26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | 46 | ### Code Splitting 47 | 48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 49 | 50 | ### Analyzing the Bundle Size 51 | 52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 53 | 54 | ### Making a Progressive Web App 55 | 56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 57 | 58 | ### Advanced Configuration 59 | 60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 61 | 62 | ### Deployment 63 | 64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 65 | 66 | ### `npm run build` fails to minify 67 | 68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 69 | -------------------------------------------------------------------------------- /examples/typescript/codegen.ts: -------------------------------------------------------------------------------- 1 | import { CodegenConfig } from '@graphql-codegen/cli' 2 | 3 | const config: CodegenConfig = { 4 | schema: 'https://create-react-app-server-kqtv5azt3q-ew.a.run.app', 5 | documents: ['src/**/*.tsx'], 6 | ignoreNoDocuments: true, // for better experience with the watcher 7 | generates: { 8 | './src/gql/': { 9 | preset: 'client' 10 | } 11 | } 12 | } 13 | 14 | export default config 15 | -------------------------------------------------------------------------------- /examples/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-example", 3 | "version": "5.1.1", 4 | "private": true, 5 | "dependencies": { 6 | "@types/jest": "^29.0.3", 7 | "@types/node": "^22.10.5", 8 | "@types/react": "^18.0.0", 9 | "@types/react-dom": "^18.0.0", 10 | "graphql-hooks": "^8.2.0", 11 | "graphql-hooks-memcache": "^3.2.0", 12 | "react": "^18.0.0", 13 | "react-dom": "^18.0.0", 14 | "react-scripts": "^5.0.0" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject", 21 | "generate-schema": "graphql-codegen", 22 | "prestart": "npm run generate-schema" 23 | }, 24 | "browserslist": [ 25 | ">0.2%", 26 | "not dead", 27 | "not ie <= 11", 28 | "not op_mini all" 29 | ], 30 | "eslintConfig": { 31 | "extends": [ 32 | "react-app", 33 | "react-app/jest" 34 | ] 35 | }, 36 | "devDependencies": { 37 | "@graphql-codegen/cli": "^5.0.0", 38 | "@graphql-codegen/client-preset": "^4.1.0", 39 | "graphql": "^16.8.1", 40 | "graphql-codegen-typescript-client": "0.18.2", 41 | "typescript": "^4.9.5" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/typescript/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nearform/graphql-hooks/5a124588a3ce684601712bb7076ba2162322c937/examples/typescript/public/favicon.ico -------------------------------------------------------------------------------- /examples/typescript/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 25 | Postie.ts | A GraphQL Hooks Example 26 | 27 | 28 | 29 |
    30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /examples/typescript/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Postie.ts", 3 | "name": "Postie.ts | A GraphQL Hooks Example", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /examples/typescript/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | APIError, 3 | ClientContext, 4 | GraphQLClient, 5 | useManualQuery, 6 | useMutation, 7 | useQuery 8 | } from 'graphql-hooks' 9 | import { useState } from 'react' 10 | import { graphql } from './gql' 11 | import { GetAllPostsQuery } from './gql/graphql' 12 | 13 | interface PostData { 14 | id: string 15 | title: string 16 | url: string 17 | } 18 | 19 | const client = new GraphQLClient({ 20 | url: 'https://create-react-app-server-kqtv5azt3q-ew.a.run.app' 21 | }) 22 | 23 | export const allPostsQuery = graphql(` 24 | query GetAllPosts { 25 | allPosts { 26 | id 27 | title 28 | url 29 | } 30 | } 31 | `) 32 | 33 | const createPostMutation = graphql(` 34 | mutation CreatePost($title: String!, $url: String!) { 35 | createPost(title: $title, url: $url) { 36 | id 37 | } 38 | } 39 | `) 40 | 41 | const postQuery = graphql(` 42 | query Post($id: ID!) { 43 | Post(id: $id) { 44 | id 45 | url 46 | title 47 | } 48 | } 49 | `) 50 | 51 | function AddPost() { 52 | const [title, setTitle] = useState('') 53 | const [url, setUrl] = useState('') 54 | const [createPost, { loading, error }] = useMutation(createPostMutation) 55 | 56 | async function handleSubmit(e: any) { 57 | e.preventDefault() 58 | await createPost({ variables: { title, url } }) 59 | } 60 | 61 | return ( 62 |
    63 | 64 | setTitle(e.currentTarget.value)} 69 | /> 70 | 71 | setUrl(e.currentTarget.value)} 77 | /> 78 | 81 | {error &&

    There was an error!

    } 82 |
    83 | ) 84 | } 85 | 86 | function Posts() { 87 | const { loading, error, data, refetch } = useQuery(allPostsQuery, { 88 | refetchAfterMutations: createPostMutation 89 | }) 90 | 91 | return ( 92 | <> 93 |

    Add post

    94 | 95 |

    Posts

    96 | 97 | 98 | 99 | ) 100 | } 101 | 102 | function PostList({ 103 | loading, 104 | error, 105 | data 106 | }: { 107 | loading: boolean 108 | error?: APIError 109 | data?: GetAllPostsQuery 110 | }) { 111 | if (loading) return

    Loading...

    112 | if (error) return

    Error!

    113 | if (!data || !data.allPosts || !data.allPosts.length) return

    No posts

    114 | 115 | return ( 116 |
      117 | {data.allPosts.map((post: PostData | null) => { 118 | if (!post) return undefined 119 | return ( 120 |
    • 121 | {post.title} 122 | (id: {post.id}) 123 |
    • 124 | ) 125 | })} 126 |
    127 | ) 128 | } 129 | 130 | function Post() { 131 | const [id, setId] = useState('') 132 | const [getPosts, { error, data }] = useManualQuery(postQuery) 133 | 134 | async function handleSubmit(e: any) { 135 | e.preventDefault() 136 | await getPosts({ variables: { id } }) 137 | } 138 | 139 | return ( 140 | <> 141 |

    Search by ID

    142 |
    143 | 144 | setId(e.currentTarget.value)} 149 | /> 150 | 151 | {error &&

    There was en error!

    } 152 |
    153 | {data && data.Post && {data.Post.title}} 154 | 155 | ) 156 | } 157 | 158 | export default function App() { 159 | return ( 160 | 161 |

    Postie.ts

    162 | 163 | 164 |
    165 | ) 166 | } 167 | -------------------------------------------------------------------------------- /examples/typescript/src/gql/fragment-masking.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core'; 3 | import { FragmentDefinitionNode } from 'graphql'; 4 | import { Incremental } from './graphql'; 5 | 6 | 7 | export type FragmentType> = TDocumentType extends DocumentTypeDecoration< 8 | infer TType, 9 | any 10 | > 11 | ? [TType] extends [{ ' $fragmentName'?: infer TKey }] 12 | ? TKey extends string 13 | ? { ' $fragmentRefs'?: { [key in TKey]: TType } } 14 | : never 15 | : never 16 | : never; 17 | 18 | // return non-nullable if `fragmentType` is non-nullable 19 | export function useFragment( 20 | _documentNode: DocumentTypeDecoration, 21 | fragmentType: FragmentType> 22 | ): TType; 23 | // return nullable if `fragmentType` is nullable 24 | export function useFragment( 25 | _documentNode: DocumentTypeDecoration, 26 | fragmentType: FragmentType> | null | undefined 27 | ): TType | null | undefined; 28 | // return array of non-nullable if `fragmentType` is array of non-nullable 29 | export function useFragment( 30 | _documentNode: DocumentTypeDecoration, 31 | fragmentType: ReadonlyArray>> 32 | ): ReadonlyArray; 33 | // return array of nullable if `fragmentType` is array of nullable 34 | export function useFragment( 35 | _documentNode: DocumentTypeDecoration, 36 | fragmentType: ReadonlyArray>> | null | undefined 37 | ): ReadonlyArray | null | undefined; 38 | export function useFragment( 39 | _documentNode: DocumentTypeDecoration, 40 | fragmentType: FragmentType> | ReadonlyArray>> | null | undefined 41 | ): TType | ReadonlyArray | null | undefined { 42 | return fragmentType as any; 43 | } 44 | 45 | 46 | export function makeFragmentData< 47 | F extends DocumentTypeDecoration, 48 | FT extends ResultOf 49 | >(data: FT, _fragment: F): FragmentType { 50 | return data as FragmentType; 51 | } 52 | export function isFragmentReady( 53 | queryNode: DocumentTypeDecoration, 54 | fragmentNode: TypedDocumentNode, 55 | data: FragmentType, any>> | null | undefined 56 | ): data is FragmentType { 57 | const deferredFields = (queryNode as { __meta__?: { deferredFields: Record } }).__meta__ 58 | ?.deferredFields; 59 | 60 | if (!deferredFields) return true; 61 | 62 | const fragDef = fragmentNode.definitions[0] as FragmentDefinitionNode | undefined; 63 | const fragName = fragDef?.name?.value; 64 | 65 | const fields = (fragName && deferredFields[fragName]) || []; 66 | return fields.length > 0 && fields.every(field => data && field in data); 67 | } 68 | -------------------------------------------------------------------------------- /examples/typescript/src/gql/gql.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import * as types from './graphql'; 3 | import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; 4 | 5 | /** 6 | * Map of all GraphQL operations in the project. 7 | * 8 | * This map has several performance disadvantages: 9 | * 1. It is not tree-shakeable, so it will include all operations in the project. 10 | * 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle. 11 | * 3. It does not support dead code elimination, so it will add unused operations. 12 | * 13 | * Therefore it is highly recommended to use the babel or swc plugin for production. 14 | */ 15 | const documents = { 16 | "\n query GetAllPosts {\n allPosts {\n id\n title\n url\n }\n }\n": types.GetAllPostsDocument, 17 | "\n mutation CreatePost($title: String!, $url: String!) {\n createPost(title: $title, url: $url) {\n id\n }\n }\n": types.CreatePostDocument, 18 | "\n query Post($id: ID!) {\n Post(id: $id) {\n id\n url\n title\n }\n }\n": types.PostDocument, 19 | }; 20 | 21 | /** 22 | * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. 23 | * 24 | * 25 | * @example 26 | * ```ts 27 | * const query = graphql(`query GetUser($id: ID!) { user(id: $id) { name } }`); 28 | * ``` 29 | * 30 | * The query argument is unknown! 31 | * Please regenerate the types. 32 | */ 33 | export function graphql(source: string): unknown; 34 | 35 | /** 36 | * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. 37 | */ 38 | export function graphql(source: "\n query GetAllPosts {\n allPosts {\n id\n title\n url\n }\n }\n"): (typeof documents)["\n query GetAllPosts {\n allPosts {\n id\n title\n url\n }\n }\n"]; 39 | /** 40 | * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. 41 | */ 42 | export function graphql(source: "\n mutation CreatePost($title: String!, $url: String!) {\n createPost(title: $title, url: $url) {\n id\n }\n }\n"): (typeof documents)["\n mutation CreatePost($title: String!, $url: String!) {\n createPost(title: $title, url: $url) {\n id\n }\n }\n"]; 43 | /** 44 | * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. 45 | */ 46 | export function graphql(source: "\n query Post($id: ID!) {\n Post(id: $id) {\n id\n url\n title\n }\n }\n"): (typeof documents)["\n query Post($id: ID!) {\n Post(id: $id) {\n id\n url\n title\n }\n }\n"]; 47 | 48 | export function graphql(source: string) { 49 | return (documents as any)[source] ?? {}; 50 | } 51 | 52 | export type DocumentType> = TDocumentNode extends DocumentNode< infer TType, any> ? TType : never; -------------------------------------------------------------------------------- /examples/typescript/src/gql/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./fragment-masking"; 2 | export * from "./gql"; -------------------------------------------------------------------------------- /examples/typescript/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 5 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 13 | monospace; 14 | } 15 | -------------------------------------------------------------------------------- /examples/typescript/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import './index.css' 4 | import App from './App' 5 | 6 | ReactDOM.render(, document.getElementById('root')) 7 | -------------------------------------------------------------------------------- /examples/typescript/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react-jsx", 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const commonConfig = { 2 | testEnvironment: 'jsdom' 3 | } 4 | 5 | const projects = [ 6 | { 7 | ...commonConfig, 8 | ...{ 9 | displayName: 'graphql-hooks-jsdom', 10 | roots: ['./packages/graphql-hooks/test-jsdom'], 11 | setupFiles: ['/packages/graphql-hooks/test-jsdom/setup.js'], 12 | automock: false 13 | } 14 | }, 15 | { 16 | ...commonConfig, 17 | ...{ 18 | displayName: 'graphql-hooks-node', 19 | roots: ['./packages/graphql-hooks/test-node'], 20 | automock: false, 21 | testEnvironment: 'node' 22 | } 23 | }, 24 | { 25 | ...commonConfig, 26 | ...{ 27 | roots: ['./packages/graphql-hooks-memcache'], 28 | displayName: 'graphql-hooks-memcache' 29 | } 30 | }, 31 | { 32 | ...commonConfig, 33 | ...{ 34 | roots: ['./packages/graphql-hooks-ssr'], 35 | displayName: 'graphql-hooks-ssr', 36 | testEnvironment: 'node' 37 | } 38 | }, 39 | { 40 | ...commonConfig, 41 | ...{ 42 | roots: ['./examples/create-react-app'], 43 | coveragePathIgnorePatterns: ['/node_modules/', '/packages/*'], 44 | transform: { 45 | '\\.[jt]sx?$': 'babel-jest' 46 | }, 47 | displayName: 'cra-example', 48 | testEnvironment: 'jsdom', 49 | setupFiles: ['/examples/create-react-app/test/setup.js'] 50 | } 51 | } 52 | ] 53 | module.exports = { 54 | projects 55 | } 56 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": ["packages/*", "examples/*", "examples/persisted-queries/*"], 3 | "version": "independent", 4 | "command": { 5 | "publish": { 6 | "conventionalCommits": true, 7 | "message": "chore(release): :rocket:" 8 | }, 9 | "version": { 10 | "conventionalCommits": true, 11 | "createRelease": "github" 12 | } 13 | }, 14 | "$schema": "node_modules/lerna/schemas/lerna-schema.json" 15 | } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "scripts": { 5 | "test": "jest", 6 | "test:coverage": "jest --coverage", 7 | "test:acceptance": "cypress open", 8 | "build": "lerna run build", 9 | "build:packages": "lerna run build --no-private", 10 | "build:site": "lerna run build --scope=create-react-app", 11 | "postinstall": "npm run build:packages", 12 | "lint": "eslint .", 13 | "check-types": "lerna run check-types", 14 | "prettier": "prettier '**/*.js' '**/*.md' '**/*.ts' --write", 15 | "release": "lerna publish", 16 | "clean": "lerna run clean && lerna clean && rm -rf ./node_modules", 17 | "prepare": "husky" 18 | }, 19 | "devDependencies": { 20 | "@babel/core": "^7.16.12", 21 | "@babel/eslint-parser": "^7.16.5", 22 | "@babel/plugin-proposal-object-rest-spread": "^7.20.7", 23 | "@babel/plugin-transform-object-rest-spread": "^7.22.5", 24 | "@babel/preset-env": "^7.14.1", 25 | "@babel/preset-react": "^7.13.13", 26 | "@babel/preset-typescript": "^7.18.6", 27 | "@commitlint/cli": "^19.3.0", 28 | "@commitlint/config-conventional": "^19.2.2", 29 | "@rollup/plugin-babel": "^6.0.4", 30 | "@rollup/plugin-commonjs": "^26.0.1", 31 | "@rollup/plugin-node-resolve": "^15.2.3", 32 | "@rollup/plugin-terser": "^0.4.4", 33 | "@testing-library/cypress": "^10.0.2", 34 | "@types/jest": "^29.0.3", 35 | "babel-jest": "^29.0.1", 36 | "cross-env": "^7.0.3", 37 | "cypress": "^13.11.0", 38 | "esbuild": "^0.23.1", 39 | "eslint": "^8.7.0", 40 | "eslint-config-prettier": "^9.0.0", 41 | "eslint-config-react-app": "^7.0.0", 42 | "eslint-plugin-prettier": "^4.0.0", 43 | "eslint-plugin-react": "^7.23.2", 44 | "eslint-plugin-react-hooks": "^4.0.0", 45 | "husky": "^9.0.11", 46 | "jest": "^29.0.1", 47 | "jest-environment-jsdom": "^29.0.1", 48 | "jest-environment-node": "^29.0.1", 49 | "lerna": "^8.1.2", 50 | "lint-staged": "^15.2.0", 51 | "prettier": "^2.7.1", 52 | "rollup": "^2.47.0", 53 | "rollup-plugin-esbuild": "^5.0.0", 54 | "rollup-plugin-replace": "^2.2.0", 55 | "rollup-plugin-size-snapshot": "^0.12.0", 56 | "ts-jest": "^29.0.1", 57 | "typescript": "^4.9.5" 58 | }, 59 | "lint-staged": { 60 | "**/*.js": [ 61 | "eslint", 62 | "prettier --write" 63 | ] 64 | }, 65 | "engines": { 66 | "node": ">=16" 67 | }, 68 | "workspaces": [ 69 | "packages/*", 70 | "examples/*", 71 | "examples/persisted-queries/*" 72 | ] 73 | } 74 | -------------------------------------------------------------------------------- /packages/babel-plugin-extract-gql/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/env" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /packages/babel-plugin-extract-gql/.gitignore: -------------------------------------------------------------------------------- 1 | gql-queries.json 2 | -------------------------------------------------------------------------------- /packages/babel-plugin-extract-gql/README.md: -------------------------------------------------------------------------------- 1 | # babel-plugin-extract-gql 2 | 3 | Extracts all graphql queries into a file 4 | 5 | ## Usage 6 | 7 | #### CLI 8 | 9 | ```sh 10 | npx babel ./proj-a/ --out-dir ./out --plugins=extract-gql --presets=@babel/env,@babel/react 11 | ``` 12 | 13 | ## Output 14 | 15 | All input files are analyzed and transformed when a graphql query is found 16 | 17 | **Example** 18 | 19 | ```js 20 | import { useQuery, persist } from 'graphql-hooks' 21 | 22 | const SOME_QUERY = persist` 23 | query Add($x: Int!, $y: Int!) { 24 | add(x: $x, y: $y) 25 | } 26 | ` 27 | 28 | const MyCompA = () => { 29 | const { data } = useQuery(SOME_QUERY) 30 | } 31 | ``` 32 | 33 | Becomes 34 | 35 | ```js 36 | import { useQuery, persist } from 'graphql-hooks' 37 | const SOME_QUERY = 38 | 'd012f1a214f5e2117ec9d9aa16d5b8b5ef95f755252cb8488c3a742eebcc6db9' 39 | 40 | const MyCompA = () => { 41 | const { data } = useQuery(SOME_QUERY, { 42 | persisted: true 43 | }) 44 | } 45 | ``` 46 | 47 | Note there are two changes. 48 | 49 | - Query const is set to the hash of the query text 50 | - `useQuery` is added with a second param containing `persisted: true` 51 | 52 | All the queries extracted are saved on disk in one file. The file is saved in the project root and looks like following 53 | 54 | ```js 55 | { 56 | "d012f1a214f5e2117ec9d9aa16d5b8b5ef95f755252cb8488c3a742eebcc6db9": "query Add($x: Int!, $y: Int!) {\n add(x: $x, y: $y)\n }", 57 | "3384eca8378f1d49f0ccd3d6bb2e394c0dd143c19bb1717138b206cd181c5dd7": "query Sub($x: Int!, $y: Int!) {\n sub(x: $x, y: $y)\n }", 58 | "3f0587e447e0eee76801682a24ce1b44b68c5d298ed24a010ce259b0361b1ef2": "query Div($x: Int!, $y: Int!) {\n div(x: $x, y: $y)\n }" 59 | } 60 | ``` 61 | 62 | Transforming all queries to their hashes tells graphql-hooks to make GET requests with `query=&persisted=true`. Following is how a request looks like 63 | 64 | ``` 65 | http://localhost:5000/graphql?query=248eb276edb4f22aced0a2848c539810b55f79d89abc531b91145e76838f5602&persisted=true 66 | ``` 67 | 68 | Or with variables 69 | 70 | ``` 71 | http://localhost:5000/graphql?query=495ccd73abc8436544cfeedd65f24beee660d2c7be2c32536e3fbf911f935ddf&persisted=true&variables=\{"x":3,"y":6\} 72 | ``` 73 | -------------------------------------------------------------------------------- /packages/babel-plugin-extract-gql/examples/mercurius/README.md: -------------------------------------------------------------------------------- 1 | # mercurius example 2 | 3 | This is a sample project to demonstrate how [mercurius](https://github.com/mercurius-js/mercurius) uses the output of babel-plugin-extract-gql. 4 | 5 | ```sh 6 | npm i 7 | node . 8 | ``` 9 | 10 | and hit the /graphql endpoint with the query hash as shown bellow 11 | 12 | ```sh 13 | curl 'http://localhost:5000/graphql?query=248eb276edb4f22aced0a2848c539810b55f79d89abc531b91145e76838f5602&persisted=true' 14 | ``` 15 | 16 | with variables 17 | 18 | ```sh 19 | curl 'http://localhost:5000/graphql?query=495ccd73abc8436544cfeedd65f24beee660d2c7be2c32536e3fbf911f935ddf&persisted=true&variables=\{"x":3,"y":6\}' 20 | ``` 21 | -------------------------------------------------------------------------------- /packages/babel-plugin-extract-gql/examples/mercurius/index.js: -------------------------------------------------------------------------------- 1 | const Fastify = require('fastify') 2 | import mercurius from 'mercurius' 3 | const persistedQueries = require('./gql-queries.json') 4 | 5 | const app = Fastify() 6 | 7 | const schema = ` 8 | type Query { 9 | add(x: Int, y: Int): Int 10 | } 11 | ` 12 | 13 | const resolvers = { 14 | Query: { 15 | add: async (_, { x, y }) => x + y 16 | } 17 | } 18 | 19 | app.register(require('@fastify/cors')) 20 | 21 | app.register(mercurius, { 22 | schema, 23 | resolvers, 24 | persistedQueries 25 | }) 26 | 27 | app.listen(5000) 28 | -------------------------------------------------------------------------------- /packages/babel-plugin-extract-gql/examples/mercurius/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mercurius-example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node ." 8 | }, 9 | "author": "Salman Mitha ", 10 | "license": "MIT", 11 | "dependencies": { 12 | "fastify": "^4.2.0", 13 | "@fastify/cors": "^8.0.0", 14 | "mercurius": "^10.1.1" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/babel-plugin-extract-gql/examples/proj-a/README.md: -------------------------------------------------------------------------------- 1 | # project-a 2 | 3 | Use the following command to create the gql-queries.json file 4 | 5 | ```sh 6 | npx babel ./src --out-dir ./dist --plugins=../../src --presets=@babel/env,@babel/react 7 | ``` 8 | -------------------------------------------------------------------------------- /packages/babel-plugin-extract-gql/examples/proj-a/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { GraphQLClient, ClientContext } from 'graphql-hooks' 3 | 4 | import MyComp from './components/MyComp' 5 | 6 | const client = new GraphQLClient({ 7 | url: 'http://localhost:5000/graphql', 8 | useGETForQueries: true 9 | }) 10 | 11 | function App() { 12 | return ( 13 | 14 |
    15 | 16 |
    17 |
    18 | ) 19 | } 20 | 21 | export default App 22 | -------------------------------------------------------------------------------- /packages/babel-plugin-extract-gql/examples/proj-a/src/components/MyComp/MyComp.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useQuery, persist } from 'graphql-hooks' 3 | 4 | const ADD_QUERY = persist` 5 | query Add($x: Int!, $y: Int!) { 6 | add(x: $x, y: $y) 7 | } 8 | ` 9 | 10 | function MyComp() { 11 | const { loading, error, data } = useQuery(ADD_QUERY, { 12 | variables: { 13 | x: 2, 14 | y: 4 15 | } 16 | }) 17 | 18 | if (loading) return 'Loading...' 19 | if (error) return 'Something Bad Happened' 20 | 21 | return
    Result: {data.add}
    22 | } 23 | 24 | export default MyComp 25 | -------------------------------------------------------------------------------- /packages/babel-plugin-extract-gql/examples/proj-a/src/components/MyComp/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './MyComp' 2 | -------------------------------------------------------------------------------- /packages/babel-plugin-extract-gql/examples/proj-a/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | 5 | ReactDOM.render(, document.getElementById('root')) 6 | -------------------------------------------------------------------------------- /packages/babel-plugin-extract-gql/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babel-plugin-extract-gql", 3 | "version": "4.0.1", 4 | "description": "", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "clean": "rm -rf lib", 8 | "build": "babel src --out-dir lib", 9 | "lint": "eslint", 10 | "test": "mocha --require @babel/register ./test/unit", 11 | "test:watch": "npm run test -- --watch" 12 | }, 13 | "devDependencies": { 14 | "@babel/cli": "^7.0.0", 15 | "@babel/core": "^7.4.0", 16 | "@babel/preset-env": "^7.9.6", 17 | "@babel/preset-react": "^7.9.4", 18 | "@babel/register": "^7.0.0", 19 | "glob": "^11.0.0", 20 | "mercurius": "^14.0.0", 21 | "mocha": "^10.0.0" 22 | }, 23 | "author": "Salman Mitha ", 24 | "license": "MIT", 25 | "keywords": [ 26 | "babel", 27 | "plugin", 28 | "extract", 29 | "strings" 30 | ], 31 | "dependencies": { 32 | "@fastify/cors": "^9.0.1", 33 | "fastify": "^4.2.0", 34 | "pkg-dir": "^8.0.0" 35 | }, 36 | "repository": { 37 | "type": "git", 38 | "url": "git://github.com/nearform/graphql-hooks.git" 39 | }, 40 | "bugs": { 41 | "url": "https://github.com/nearform/graphql-hooks/issues" 42 | }, 43 | "homepage": "https://github.com/nearform/graphql-hooks/tree/master/packages/babel-plugin-extract-gql#readme" 44 | } 45 | -------------------------------------------------------------------------------- /packages/babel-plugin-extract-gql/src/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const crypto = require('crypto') 4 | const pkgDir = require('pkg-dir') 5 | 6 | const pkgRoot = pkgDir.sync() 7 | const outputFile = path.join(pkgRoot, 'gql-queries.json') 8 | 9 | function appendJSON(json) { 10 | let data = {} 11 | if (fs.existsSync(outputFile)) { 12 | try { 13 | data = JSON.parse(fs.readFileSync(outputFile, 'utf8') || '') 14 | } catch (e) { 15 | console.log(e) 16 | } 17 | } 18 | Object.assign(data, json) 19 | fs.writeFileSync(outputFile, JSON.stringify(data, null, 2)) 20 | } 21 | 22 | function extractGql({ types: t }) { 23 | const queries = {} 24 | const varsTransformed = {} 25 | 26 | return { 27 | pre() { 28 | this.queries = {} 29 | }, 30 | visitor: { 31 | TaggedTemplateExpression(path) { 32 | const { tag, quasi } = path.node 33 | 34 | if (tag.name !== 'persist') return // if not the gql query, ignore 35 | if (quasi.quasis.length > 1) return // if dynamic string, ignore 36 | 37 | const queryText = quasi.quasis[0].value.cooked.trim() 38 | const queryHash = crypto 39 | .createHash('sha256') 40 | .update(queryText) 41 | .digest('hex') 42 | const queryHashNode = t.stringLiteral(queryHash) 43 | 44 | this.queries[queryHash] = queryText 45 | queries[queryHash] = queryText 46 | 47 | const varName = path.parentPath.node.id.name 48 | varsTransformed[varName] = true 49 | 50 | path.replaceWith(queryHashNode) 51 | }, 52 | CallExpression(path) { 53 | const { callee, arguments: args } = path.node 54 | const [varNameArg, optionsArg] = args 55 | const varName = varNameArg.name 56 | 57 | // if not a call to useQuery OR if is useQuery but not with a persisted query, ignore 58 | if (callee.name !== 'useQuery' || !varsTransformed[varName]) return 59 | 60 | const optionsObj = optionsArg || t.objectExpression([]) 61 | 62 | optionsObj.properties.push( 63 | t.objectProperty(t.stringLiteral('persisted'), t.booleanLiteral(true)) 64 | ) 65 | 66 | if (!optionsArg) args.push(optionsObj) 67 | } 68 | }, 69 | post() { 70 | appendJSON(this.queries) 71 | } 72 | } 73 | } 74 | 75 | module.exports = extractGql 76 | -------------------------------------------------------------------------------- /packages/babel-plugin-extract-gql/test/unit/fixtures/actual/.4.js: -------------------------------------------------------------------------------- 1 | import { useQuery, persist } from 'graphql-hooks' 2 | 3 | const SOME_QUERY = persist` 4 | query Add($x: Int!, $y: Int!) { 5 | add(x: $x, y: $y) 6 | } 7 | ` 8 | 9 | const MyCompA = () => { 10 | const queryOpts = { other: 'options', remain: true } 11 | const { data } = useQuery(SOME_QUERY, queryOpts) 12 | } 13 | -------------------------------------------------------------------------------- /packages/babel-plugin-extract-gql/test/unit/fixtures/actual/1.js: -------------------------------------------------------------------------------- 1 | import { useQuery, persist } from 'graphql-hooks' 2 | 3 | const SOME_QUERY = persist` 4 | query Add($x: Int!, $y: Int!) { 5 | add(x: $x, y: $y) 6 | } 7 | ` 8 | 9 | const MyCompA = () => { 10 | const { data } = useQuery(SOME_QUERY) 11 | } 12 | -------------------------------------------------------------------------------- /packages/babel-plugin-extract-gql/test/unit/fixtures/actual/2.js: -------------------------------------------------------------------------------- 1 | import { useQuery, persist } from 'graphql-hooks' 2 | 3 | const SOME_QUERY = persist` 4 | query Sub($x: Int!, $y: Int!) { 5 | sub(x: $x, y: $y) 6 | } 7 | ` 8 | 9 | const MyCompA = () => { 10 | const { data } = useQuery(SOME_QUERY, { other: 'options', remain: true }) 11 | } 12 | -------------------------------------------------------------------------------- /packages/babel-plugin-extract-gql/test/unit/fixtures/actual/3.js: -------------------------------------------------------------------------------- 1 | import { useQuery, persist } from 'graphql-hooks' 2 | 3 | const SOME_QUERY = persist` 4 | query Div($x: Int!, $y: Int!) { 5 | div(x: $x, y: $y) 6 | } 7 | ` 8 | 9 | const SOME_OTHER_QUERY = ` 10 | query Mul($x: Int!, $y: Int!) { 11 | mul(x: $x, y: $y) 12 | } 13 | ` 14 | 15 | const MyCompA = () => { 16 | const { data: data1 } = useQuery(SOME_QUERY, { 17 | other: 'options', 18 | remain: true 19 | }) 20 | const { data: data2 } = useQuery(SOME_OTHER_QUERY) 21 | } 22 | -------------------------------------------------------------------------------- /packages/babel-plugin-extract-gql/test/unit/fixtures/expected/1.js: -------------------------------------------------------------------------------- 1 | import { useQuery, persist } from 'graphql-hooks' 2 | const SOME_QUERY = 3 | 'd012f1a214f5e2117ec9d9aa16d5b8b5ef95f755252cb8488c3a742eebcc6db9' 4 | 5 | const MyCompA = () => { 6 | const { data } = useQuery(SOME_QUERY, { 7 | persisted: true 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /packages/babel-plugin-extract-gql/test/unit/fixtures/expected/2.js: -------------------------------------------------------------------------------- 1 | import { useQuery, persist } from 'graphql-hooks' 2 | const SOME_QUERY = 3 | '3384eca8378f1d49f0ccd3d6bb2e394c0dd143c19bb1717138b206cd181c5dd7' 4 | 5 | const MyCompA = () => { 6 | const { data } = useQuery(SOME_QUERY, { 7 | other: 'options', 8 | remain: true, 9 | persisted: true 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /packages/babel-plugin-extract-gql/test/unit/fixtures/expected/3.js: -------------------------------------------------------------------------------- 1 | import { useQuery, persist } from 'graphql-hooks' 2 | const SOME_QUERY = 3 | '3f0587e447e0eee76801682a24ce1b44b68c5d298ed24a010ce259b0361b1ef2' 4 | const SOME_OTHER_QUERY = ` 5 | query Mul($x: Int!, $y: Int!) { 6 | mul(x: $x, y: $y) 7 | } 8 | ` 9 | 10 | const MyCompA = () => { 11 | const { data: data1 } = useQuery(SOME_QUERY, { 12 | other: 'options', 13 | remain: true, 14 | persisted: true 15 | }) 16 | const { data: data2 } = useQuery(SOME_OTHER_QUERY) 17 | } 18 | -------------------------------------------------------------------------------- /packages/babel-plugin-extract-gql/test/unit/fixtures/expected/4.js: -------------------------------------------------------------------------------- 1 | import { useQuery, persist } from 'graphql-hooks' 2 | const SOME_QUERY = 3 | 'd012f1a214f5e2117ec9d9aa16d5b8b5ef95f755252cb8488c3a742eebcc6db9' 4 | 5 | const MyCompA = () => { 6 | const queryOpts = { 7 | other: 'options', 8 | remain: true 9 | } 10 | const { data } = useQuery(SOME_QUERY, { ...queryOpts, persisted: true }) 11 | } 12 | -------------------------------------------------------------------------------- /packages/babel-plugin-extract-gql/test/unit/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | import path from 'path' 4 | import glob from 'glob' 5 | import fs from 'fs' 6 | import assert from 'assert' 7 | import { transformFileSync } from '@babel/core' 8 | 9 | import plugin from '../../src' 10 | 11 | const babelOptions = { 12 | presets: [['@babel/react']], 13 | plugins: [[plugin, {}]], 14 | babelrc: false 15 | } 16 | 17 | describe('Exracts gql queries', () => { 18 | const inDir = path.join(__dirname, 'fixtures', 'actual') 19 | const outDir = path.join(__dirname, 'fixtures', 'expected') 20 | 21 | glob 22 | .sync('*', { cwd: inDir }) 23 | .filter(d => !d.startsWith('.')) 24 | .map(file => { 25 | it(`should transform ${file}`, () => { 26 | const actualPath = path.join(inDir, file) 27 | const expectedPath = path.join(outDir, file) 28 | 29 | const actual = transformFileSync(actualPath, babelOptions).code 30 | const expected = fs.readFileSync(expectedPath, 'utf8') 31 | 32 | assert.strictEqual(actual.trim(), expected.trim()) 33 | }) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /packages/graphql-hooks-memcache/README.md: -------------------------------------------------------------------------------- 1 | # graphql-hooks-memcache 2 | 3 | In-memory caching implementation for graphql-hooks 4 | 5 | ## Install 6 | 7 | `npm install graphql-hooks-memcache` 8 | 9 | or 10 | 11 | `yarn add graphql-hooks-memcache` 12 | 13 | ## Quick Start 14 | 15 | This is intended to be used as the `cache` option when calling `createClient` from `graphql-hooks`. 16 | 17 | ```js 18 | import { GraphQLClient } from 'graphql-hooks' 19 | import memCache from 'graphql-hooks-memcache' 20 | 21 | const client = new GraphQLClient({ 22 | url: '/graphql', 23 | cache: memCache() 24 | }) 25 | ``` 26 | 27 | ### Options 28 | 29 | `memCache(options)`: Option object properties 30 | 31 | - `size`: The number of items to store in the cache 32 | - `ttl`: Milliseconds an item will remain in cache. The default behaviour will only evict items when the `size` limit has been reached 33 | - `initialState`: The value from `cache.getInitialState()` used for rehydrating the cache after SSR 34 | 35 | ### API 36 | 37 | - `cache.get(key)`: Find the item in the cache that matches `key` 38 | - `cache.set(key, value)`: Set an item in the cache 39 | - `cache.delete(key)`: Delete an item from the cache 40 | - `cache.clear()`: Clear all items from the cache 41 | - `cache.keys()`: Returns an array of keys, useful when you need to iterate over the cache items 42 | - `cache.getInitialState()`: A serialisable version of the cache - used during SSR 43 | -------------------------------------------------------------------------------- /packages/graphql-hooks-memcache/babel.config.js: -------------------------------------------------------------------------------- 1 | const { NODE_ENV } = process.env 2 | 3 | module.exports = { 4 | presets: [ 5 | [ 6 | '@babel/env', 7 | { 8 | targets: { 9 | browsers: ['ie >= 11'] 10 | }, 11 | modules: false, 12 | loose: true 13 | } 14 | ] 15 | ], 16 | plugins: [ 17 | // don't use `loose` mode here - need to copy symbols when spreading 18 | '@babel/plugin-transform-object-rest-spread', 19 | NODE_ENV === 'test' && '@babel/transform-modules-commonjs' 20 | ].filter(Boolean) 21 | } 22 | -------------------------------------------------------------------------------- /packages/graphql-hooks-memcache/index.d.ts: -------------------------------------------------------------------------------- 1 | export = MemCacheFunction 2 | 3 | declare function MemCacheFunction(options?: { 4 | size?: number 5 | ttl?: number 6 | initialState?: object 7 | }): MemCache 8 | 9 | interface MemCache { 10 | get(keyObject: object): object 11 | set(keyObject: object, data: object): void 12 | delete(keyObject: object): void 13 | clear(): void 14 | keys(): void 15 | getInitialState(): object 16 | } 17 | -------------------------------------------------------------------------------- /packages/graphql-hooks-memcache/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-hooks-memcache", 3 | "version": "3.2.0", 4 | "description": "In memory cache for graphql-hooks", 5 | "main": "lib/graphql-hooks-memcache.js", 6 | "module": "es/graphql-hooks-memcache.js", 7 | "unpkg": "dist/graphql-hooks-memcache.min.js", 8 | "types": "index.d.ts", 9 | "scripts": { 10 | "clean": "rm -rf ./dist ./es ./lib", 11 | "test": "echo \"Error: no test specified\" && exit 1", 12 | "build": "cross-env NODE_OPTIONS=--openssl-legacy-provider rollup -c", 13 | "prepublishOnly": "npm run build && cp ../../LICENSE ." 14 | }, 15 | "files": [ 16 | "dist", 17 | "es", 18 | "lib", 19 | "index.d.ts" 20 | ], 21 | "keywords": [ 22 | "graphql", 23 | "hooks", 24 | "react", 25 | "graphql-hooks", 26 | "cache", 27 | "ssr", 28 | "server-side rendering" 29 | ], 30 | "author": "Brian Mullan ", 31 | "license": "Apache-2.0", 32 | "dependencies": { 33 | "tiny-lru": "^11.0.1" 34 | }, 35 | "repository": { 36 | "type": "git", 37 | "url": "git://github.com/nearform/graphql-hooks.git" 38 | }, 39 | "bugs": { 40 | "url": "https://github.com/nearform/graphql-hooks/issues" 41 | }, 42 | "homepage": "https://github.com/nearform/graphql-hooks/blob/master/packages/graphql-hooks-memcache#readme", 43 | "devDependencies": { 44 | "cross-env": "^7.0.3" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/graphql-hooks-memcache/rollup.config.js: -------------------------------------------------------------------------------- 1 | import nodeResolve from '@rollup/plugin-node-resolve' 2 | import babel from '@rollup/plugin-babel' 3 | import commonjs from '@rollup/plugin-commonjs' 4 | import { sizeSnapshot } from 'rollup-plugin-size-snapshot' 5 | import esbuild from 'rollup-plugin-esbuild' // Used for TS transpiling 6 | import generateRollupConfig from '../../config/rollup.config' 7 | 8 | const pkg = require('./package.json') 9 | const externalPeerDeps = [...Object.keys(pkg.peerDependencies || {})] 10 | 11 | const overrides = { 12 | external: [...externalPeerDeps], 13 | plugins: [ 14 | commonjs(), 15 | nodeResolve({ 16 | jsnext: true 17 | }), 18 | esbuild(), 19 | babel(), 20 | sizeSnapshot() 21 | ] 22 | } 23 | 24 | export default generateRollupConfig({ 25 | name: 'GraphQLHooksMemcache', 26 | overrides 27 | }) 28 | -------------------------------------------------------------------------------- /packages/graphql-hooks-memcache/src/fnv1a.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // MIT License 4 | // 5 | // Copyright (c) Sindre Sorhus (sindresorhus.com) 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a 8 | // copy of this software and associated documentation files (the "Software"), 9 | // to deal in the Software without restriction, including without limitation 10 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 11 | // and/or sell copies of the Software, and to permit persons to whom the Software 12 | // is furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 18 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 19 | // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | const OFFSET_BASIS_32 = 2166136261 25 | 26 | function fnv1aString(string) { 27 | let hash = OFFSET_BASIS_32 28 | 29 | for (let i = 0; i < string.length; i++) { 30 | hash ^= string.charCodeAt(i) 31 | 32 | // 32-bit FNV prime: 2**24 + 2**8 + 0x93 = 16777619 33 | // Using bitshift for accuracy and performance. Numbers in JS suck. 34 | hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24) 35 | } 36 | 37 | return hash >>> 0 38 | } 39 | 40 | export default function fnv1a(input) { 41 | if (typeof input === 'string') { 42 | return fnv1aString(input) 43 | } else { 44 | throw new Error('input must be a string') 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/graphql-hooks-memcache/src/index.js: -------------------------------------------------------------------------------- 1 | import { lru as LRU } from 'tiny-lru' 2 | import fnv1a from './fnv1a' 3 | 4 | function generateKey(keyObj) { 5 | return fnv1a(JSON.stringify(keyObj)).toString(36) 6 | } 7 | 8 | export default function memCache({ 9 | size = 100, 10 | ttl = 0, 11 | debug = false, 12 | initialState 13 | } = {}) { 14 | const lru = LRU(size, ttl) 15 | const debugLru = debug ? LRU(size, ttl) : undefined 16 | 17 | if (initialState) { 18 | Object.keys(initialState).map(k => { 19 | lru.set(k, initialState[k]) 20 | }) 21 | } 22 | 23 | return { 24 | get: keyObj => lru.get(generateKey(keyObj)), 25 | rawGet: rawKey => lru.get(rawKey), 26 | rawToKey: rawKey => { 27 | if (!debugLru) { 28 | throw 'not allowed unless in debug mode' 29 | } 30 | return debugLru.get(rawKey) 31 | }, 32 | set: (keyObj, data) => { 33 | const key = generateKey(keyObj) 34 | lru.set(key, data) 35 | if (debugLru) { 36 | debugLru.set(key, keyObj) 37 | } 38 | }, 39 | delete: keyObj => lru.delete(generateKey(keyObj)), 40 | clear: () => lru.clear(), 41 | keys: () => [...lru.keys()], 42 | getInitialState: () => 43 | [...lru.keys()].reduce( 44 | (initialState, key) => ({ 45 | ...initialState, 46 | [key]: lru.get(key) 47 | }), 48 | {} 49 | ) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/graphql-hooks-memcache/test/fnv1a.test.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // MIT License 4 | // 5 | // Copyright (c) Sindre Sorhus (sindresorhus.com) 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a 8 | // copy of this software and associated documentation files (the "Software"), 9 | // to deal in the Software without restriction, including without limitation 10 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 11 | // and/or sell copies of the Software, and to permit persons to whom the Software 12 | // is furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 18 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 19 | // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | import fnv1a from '../src/fnv1a' 25 | 26 | describe('fnv1a tests', () => { 27 | it('works for string', () => { 28 | expect(fnv1a('')).toEqual(2166136261) 29 | expect(fnv1a('🦄🌈')).toEqual(582881315) 30 | expect(fnv1a('h')).toEqual(3977000791) 31 | expect(fnv1a('he')).toEqual(1547363254) 32 | expect(fnv1a('hel')).toEqual(179613742) 33 | expect(fnv1a('hell')).toEqual(477198310) 34 | expect(fnv1a('hello')).toEqual(1335831723) 35 | expect(fnv1a('hello ')).toEqual(3801292497) 36 | expect(fnv1a('hello w')).toEqual(1402552146) 37 | expect(fnv1a('hello wo')).toEqual(3611200775) 38 | expect(fnv1a('hello wor')).toEqual(1282977583) 39 | expect(fnv1a('hello worl')).toEqual(2767971961) 40 | expect(fnv1a('hello world')).toEqual(3582672807) 41 | expect( 42 | fnv1a( 43 | 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium.' 44 | ) 45 | ).toEqual(2964896417) 46 | }) 47 | 48 | it("can't hash objects", () => { 49 | expect(() => fnv1a({})).toThrow(Error) 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /packages/graphql-hooks-memcache/test/index.test.ts: -------------------------------------------------------------------------------- 1 | import memCache from '../src' 2 | 3 | describe('memcache', () => { 4 | let cache 5 | beforeEach(() => { 6 | cache = memCache() 7 | }) 8 | it('sets and gets key with string key', () => { 9 | cache.set('foo', 'bar') 10 | expect(cache.get('foo')).toEqual('bar') 11 | }) 12 | it('sets and gets key with object key', () => { 13 | cache.set({ foo: 'foo' }, 'baz') 14 | expect(cache.get({ foo: 'foo' })).toEqual('baz') 15 | }) 16 | it('gets value from a hashed key', () => { 17 | cache.set('foo', 'bar') 18 | const rawKey = cache.keys().pop() 19 | expect(cache.rawGet(rawKey)).toEqual('bar') 20 | }) 21 | it('deletes a key', () => { 22 | cache.set('foo', 'baz') 23 | expect(cache.get('foo')).toEqual('baz') 24 | cache.delete('foo') 25 | expect(cache.get('foo')).toBe(undefined) 26 | }) 27 | it('lists all keys', () => { 28 | cache.set('foo', 'bar') 29 | cache.set('bar', 'baz') 30 | expect(cache.keys().length).toEqual(2) 31 | }) 32 | it('clears all keys', () => { 33 | cache.set('foo', 'bar') 34 | cache.set('bar', 'baz') 35 | expect(cache.keys().length).toEqual(2) 36 | cache.clear() 37 | expect(cache.keys().length).toEqual(0) 38 | }) 39 | it('returns initial state', () => { 40 | cache = memCache({ initialState: { foo: 'bar' } }) 41 | expect(cache.getInitialState()).toEqual({ foo: 'bar' }) 42 | }) 43 | it('throws an error if rawToKey is used', () => { 44 | expect(() => cache.rawToKey('test')).toThrow() 45 | }) 46 | }) 47 | 48 | describe('memcache (debug)', () => { 49 | let cache 50 | beforeEach(() => { 51 | cache = memCache({ initialState: {}, debug: true }) 52 | }) 53 | it('returns the original key', () => { 54 | cache.set('foo', 'bar') 55 | const rawKey = cache.keys().pop() 56 | expect(cache.rawToKey(rawKey)).toEqual('foo') 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /packages/graphql-hooks-ssr/README.md: -------------------------------------------------------------------------------- 1 | # graphql-hooks-ssr 2 | 3 | Server-side rendering utils for `graphql-hooks` 4 | 5 | ## Install 6 | 7 | `npm install graphql-hooks-ssr` 8 | 9 | or 10 | 11 | `yarn add graphql-hooks-ssr` 12 | 13 | ## Quick Start 14 | 15 | The below example is for `fastify` but the same principles apply for `express` & `hapi`. 16 | 17 | ```js 18 | const { GraphQLClient, ClientContext } = require('graphql-hooks') 19 | const memCache = require('graphql-hooks-memcache') 20 | const { getInitialState } = require('graphql-hooks-ssr') 21 | const { ServerLocation } = require('@reach/router') 22 | // NOTE: use can use any 'fetch' polyfill 23 | const fetch = require('isomorphic-unfetch') 24 | 25 | app.get('/', async (req, reply) => { 26 | // Step 1: Create the client inside the request handler 27 | const client = new GraphQLClient({ 28 | url: 'https://domain.com/graphql', 29 | cache: memCache(), // NOTE: a cache is required for SSR 30 | fetch 31 | }) 32 | 33 | // Step 2: Provide the `client` 34 | // Optional: If your app contains a router, you'll need to tell it which route the user is on 35 | // based on the request.. this example uses @reach/router 36 | const App = ( 37 | 38 | 39 | {/* Your App component goes here */} 40 | 41 | 42 | ) 43 | 44 | // Step 3: Use the getInitialState method from graphql-hooks-ssr 45 | // Pass in App + GraphQL client 46 | const initialState = await getInitialState({ App, client }) 47 | 48 | // Step 4: Render the your App - all queries will now be cached 49 | const content = ReactDOMServer.renderToString(App) 50 | 51 | // Step 5: Serialise the initialState object + include it in the html payload 52 | const html = ` 53 | 54 | 55 | 56 |
    ${content}
    57 | 63 | 64 | 65 | ` 66 | 67 | reply.type('text/html').send(html) 68 | }) 69 | ``` 70 | 71 | ### API 72 | 73 | #### `getInitialState(options)` 74 | 75 | Returns the serialisable cache after fetching all queries. 76 | 77 | - `options.App`: The react component to render 78 | - `options.client`: An instance of `GraphQLClient` from `graphql-hooks` 79 | - `options.render`: A custom render function; defaults to `ReactDOMServer.renderToStaticMarkup` 80 | -------------------------------------------------------------------------------- /packages/graphql-hooks-ssr/index.d.ts: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react' 2 | 3 | export function getInitialState(options: Options): Promise 4 | 5 | interface Options { 6 | App: ReactElement 7 | client: Client 8 | render?(element: ReactElement): string 9 | } 10 | 11 | interface Client { 12 | cache: Cache 13 | ssrPromises?: Promise[] 14 | } 15 | 16 | interface Cache { 17 | getInitialState(): object 18 | } 19 | -------------------------------------------------------------------------------- /packages/graphql-hooks-ssr/index.js: -------------------------------------------------------------------------------- 1 | const ReactDOMServer = require('react-dom/server') 2 | 3 | async function getInitialState(opts) { 4 | const { App, client, render = ReactDOMServer.renderToStaticMarkup } = opts 5 | 6 | if (!client.cache) { 7 | throw new Error( 8 | 'A cache implementation must be provided for SSR, please pass one to `GraphQLClient` via `options`.' 9 | ) 10 | } 11 | 12 | // ensure ssrMode is set: 13 | client.ssrMode = true 14 | render(App) 15 | 16 | if (client.ssrPromises.length) { 17 | await Promise.all(client.ssrPromises) 18 | // clear promises 19 | client.ssrPromises = [] 20 | // recurse there may be dependant queries 21 | return getInitialState(opts) 22 | } else { 23 | return client.cache.getInitialState() 24 | } 25 | } 26 | 27 | module.exports = { 28 | getInitialState 29 | } 30 | -------------------------------------------------------------------------------- /packages/graphql-hooks-ssr/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { getInitialState } from './index' 3 | 4 | describe('getInitialState', () => { 5 | it('runs all promises and returns state from cache', async () => { 6 | const MockApp =

    hello world

    7 | 8 | let resolvedPromises = 0 9 | const promiseCounter = jest.fn().mockImplementation(() => { 10 | resolvedPromises++ 11 | return Promise.resolve() 12 | }) 13 | 14 | const ssrPromises = [promiseCounter(), promiseCounter()] 15 | 16 | const mockClient = { 17 | ssrPromises, 18 | cache: { 19 | getInitialState: jest.fn().mockReturnValue({ foo: 'bar' }) 20 | } 21 | } 22 | 23 | const result = await getInitialState({ 24 | App: MockApp, 25 | client: mockClient 26 | }) 27 | 28 | expect(result).toEqual({ foo: 'bar' }) 29 | expect(resolvedPromises).toBe(2) 30 | }) 31 | 32 | it("throws if a cache hasn't been provided", async () => { 33 | const MockApp =

    hello world

    34 | 35 | const mockClient = { 36 | ssrPromises: [] 37 | } 38 | 39 | expect( 40 | getInitialState({ 41 | App: MockApp, 42 | //@ts-ignore 43 | client: mockClient 44 | }) 45 | ).rejects.toEqual( 46 | new Error( 47 | 'A cache implementation must be provided for SSR, please pass one to `GraphQLClient` via `options`.' 48 | ) 49 | ) 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /packages/graphql-hooks-ssr/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-hooks-ssr", 3 | "version": "3.0.1", 4 | "description": "Server side rendering utils for graphql-hooks", 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "scripts": { 8 | "clean": "rm -rf ./dist ./es ./lib", 9 | "test": "echo \"Error: no test specified\" && exit 0", 10 | "prepublishOnly": "cp ../../LICENSE ." 11 | }, 12 | "keywords": [ 13 | "graphql", 14 | "hooks", 15 | "react", 16 | "graphql-hooks", 17 | "cache", 18 | "ssr", 19 | "server-side rendering" 20 | ], 21 | "author": "Brian Mullan ", 22 | "license": "Apache-2.0", 23 | "peerDependencies": { 24 | "react": "^18.0.0", 25 | "react-dom": "^18.0.0" 26 | }, 27 | "devDependencies": { 28 | "@types/react": "^18.0.0", 29 | "react": "^18.0.0", 30 | "react-dom": "^18.0.0" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "git://github.com/nearform/graphql-hooks.git" 35 | }, 36 | "bugs": { 37 | "url": "https://github.com/nearform/graphql-hooks/issues" 38 | }, 39 | "homepage": "https://github.com/nearform/graphql-hooks/blob/master/packages/graphql-hooks-ssr#readme" 40 | } 41 | -------------------------------------------------------------------------------- /packages/graphql-hooks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-hooks", 3 | "version": "8.2.0", 4 | "description": "Graphql Hooks", 5 | "main": "lib/graphql-hooks.js", 6 | "module": "es/graphql-hooks.js", 7 | "unpkg": "dist/graphql-hooks.min.js", 8 | "types": "lib/index.d.ts", 9 | "scripts": { 10 | "clean": "rm -rf ./dist ./es ./lib", 11 | "build": "npm run build:code && npm run build:types", 12 | "build:code": "cross-env NODE_OPTIONS=--openssl-legacy-provider rollup -c", 13 | "build:types": "tsc --emitDeclarationOnly --declaration", 14 | "check-types": "tsc --noEmit", 15 | "prepublishOnly": "npm run build && cp ../../README.md . && cp ../../LICENSE ." 16 | }, 17 | "files": [ 18 | "dist", 19 | "es", 20 | "lib", 21 | "index.d.ts" 22 | ], 23 | "keywords": [ 24 | "graphql", 25 | "hooks", 26 | "graphql-hooks", 27 | "gql", 28 | "gql-hooks", 29 | "react-graphql-hooks", 30 | "react", 31 | "apollo" 32 | ], 33 | "author": "Brian Mullan ", 34 | "license": "Apache-2.0", 35 | "peerDependencies": { 36 | "react": "^17.0.0 || ^18.0.0 || ^19.0.0" 37 | }, 38 | "dependencies": { 39 | "@0no-co/graphql.web": "^1.0.7", 40 | "@aws-crypto/sha256-browser": "^5.2.0", 41 | "buffer": "^6.0.3", 42 | "events": "^3.3.0", 43 | "extract-files": "^11.0.0", 44 | "use-deep-compare-effect": "^1.8.1" 45 | }, 46 | "devDependencies": { 47 | "@testing-library/react": "^16.0.1", 48 | "@types/extract-files": "^8.1.1", 49 | "cross-env": "^7.0.3", 50 | "formdata-node": "^6.0.3", 51 | "graphql": "^16.8.1", 52 | "graphql-hooks-memcache": "^3.2.0", 53 | "graphql-hooks-ssr": "^3.0.1", 54 | "graphql-tag": "^2.12.6", 55 | "graphql-ws": "^5.5.5", 56 | "jest-fetch-mock": "^3.0.0", 57 | "react": "^18.0.0", 58 | "react-dom": "^18.0.0", 59 | "react-test-renderer": "^18.0.0", 60 | "subscriptions-transport-ws": "^0.11.0" 61 | }, 62 | "repository": { 63 | "type": "git", 64 | "url": "git://github.com/nearform/graphql-hooks.git" 65 | }, 66 | "bugs": { 67 | "url": "https://github.com/nearform/graphql-hooks/issues" 68 | }, 69 | "homepage": "https://github.com/nearform/graphql-hooks#readme" 70 | } 71 | -------------------------------------------------------------------------------- /packages/graphql-hooks/rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from '@rollup/plugin-babel' 2 | import esbuild from 'rollup-plugin-esbuild' // Used for TS transpiling 3 | import { sizeSnapshot } from 'rollup-plugin-size-snapshot' 4 | 5 | import generateRollupConfig from '../../config/rollup.config' 6 | 7 | export default generateRollupConfig({ 8 | name: 'GraphQLHooks', 9 | entryPoint: 'src/index.ts' 10 | }).concat({ 11 | input: 'src/middlewares/apqMiddleware.ts', 12 | output: { 13 | file: 'lib/middlewares/apqMiddleware.js', 14 | format: 'cjs', 15 | indent: false 16 | }, 17 | plugins: [esbuild(), babel(), sizeSnapshot()] 18 | }) 19 | -------------------------------------------------------------------------------- /packages/graphql-hooks/src/.babelrc.js: -------------------------------------------------------------------------------- 1 | const { NODE_ENV } = process.env 2 | 3 | module.exports = { 4 | presets: [ 5 | [ 6 | '@babel/env', 7 | { 8 | targets: { 9 | browsers: ['ie >= 11'] 10 | }, 11 | modules: false, 12 | loose: true 13 | } 14 | ] 15 | ], 16 | plugins: [ 17 | // don't use `loose` mode here - need to copy symbols when spreading 18 | '@babel/proposal-object-rest-spread', 19 | NODE_ENV === 'test' && '@babel/transform-modules-commonjs' 20 | ].filter(Boolean) 21 | } 22 | -------------------------------------------------------------------------------- /packages/graphql-hooks/src/ClientContext.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import type GraphQLClient from './GraphQLClient' 4 | 5 | const ClientContext = React.createContext(null) 6 | 7 | ClientContext.displayName = 'ClientContext' 8 | 9 | export default ClientContext 10 | -------------------------------------------------------------------------------- /packages/graphql-hooks/src/LocalGraphQLClient.ts: -------------------------------------------------------------------------------- 1 | import GraphQLClient, { applyResponseReducer } from './GraphQLClient' 2 | import LocalGraphQLError from './LocalGraphQLError' 3 | import { 4 | LocalClientOptions, 5 | LocalQueries, 6 | Operation, 7 | RequestOptions, 8 | Result 9 | } from './types/common-types' 10 | 11 | /** Local version of the GraphQLClient which only returns specified queries 12 | * Meant to be used as a way to easily mock and test queries during development. This client never contacts any actual server. 13 | * Queries are given in the form of an object of functions. 14 | * Example: 15 | ``` 16 | const localQueries = { 17 | [allPostsQuery]: () => ({ 18 | allPosts: [ 19 | { 20 | id: 1, 21 | title: 'Test', 22 | url: 'https://example.com' 23 | } 24 | ] 25 | }), 26 | [createPostMutation]: () => ({ createPost: { id: 1 } }), 27 | } 28 | const client = new LocalGraphQLClient({ localQueries }) 29 | ``` 30 | */ 31 | class LocalGraphQLClient extends GraphQLClient { 32 | localQueries: LocalQueries 33 | // Delay before sending responses in miliseconds for simulating latency 34 | requestDelayMs: number 35 | constructor(config: LocalClientOptions) { 36 | super({ url: 'http://localhost', ...config }) 37 | this.localQueries = config.localQueries 38 | this.requestDelayMs = config.requestDelayMs || 0 39 | if (!this.localQueries) { 40 | throw new Error( 41 | 'LocalGraphQLClient: `localQueries` object required in the constructor options' 42 | ) 43 | } 44 | } 45 | 46 | verifyConfig() { 47 | // Skips all config verification from the parent class because we're mocking the client 48 | } 49 | 50 | requestViaHttp( 51 | operation: Operation, 52 | options: RequestOptions = {} 53 | ): Promise> { 54 | return timeoutPromise(this.requestDelayMs).then(() => { 55 | if (!operation.query || !this.localQueries[operation.query]) { 56 | throw new Error( 57 | `LocalGraphQLClient: no query match for: ${operation.query}` 58 | ) 59 | } 60 | 61 | const data = this.localQueries[operation.query]( 62 | operation.variables, 63 | operation.operationName 64 | ) 65 | 66 | return applyResponseReducer(options.responseReducer, data, new Response()) 67 | }) 68 | } 69 | 70 | request( 71 | operation: Operation, 72 | options?: RequestOptions 73 | ): Promise> { 74 | return super 75 | .request(operation, options) 76 | .then(result => { 77 | if (result instanceof LocalGraphQLError) { 78 | return { error: result } 79 | } 80 | const { data, errors } = collectErrors(result) 81 | if (errors && errors.length > 0) { 82 | return { 83 | data, 84 | error: new LocalGraphQLError({ 85 | graphQLErrors: errors as TGraphQLError[] 86 | }) 87 | } 88 | } else { 89 | return { data } 90 | } 91 | }) 92 | } 93 | } 94 | function timeoutPromise(delayInMs) { 95 | return new Promise(resolve => { 96 | setTimeout(resolve, delayInMs) 97 | }) 98 | } 99 | 100 | function isObject(o: unknown): o is object { 101 | return o === Object(o) 102 | } 103 | 104 | function collectErrorsFromObject(objectIn: object): { 105 | data: object | null 106 | errors: Error[] 107 | } { 108 | const data: object = {} 109 | const errors: Error[] = [] 110 | 111 | for (const [key, value] of Object.entries(objectIn)) { 112 | const child = collectErrors(value) 113 | data[key] = child.data 114 | if (child.errors != null) { 115 | errors.push(...child.errors) 116 | } 117 | } 118 | 119 | return { data, errors } 120 | } 121 | 122 | function collectErrorsFromArray(arrayIn: object[]): { 123 | data: (object | null)[] 124 | errors: Error[] 125 | } { 126 | const data: (object | null)[] = Array(arrayIn.length) 127 | const errors: Error[] = [] 128 | 129 | for (const [idx, entry] of arrayIn.entries()) { 130 | const child = collectErrors(entry) 131 | data[idx] = child.data 132 | if (child.errors != null) { 133 | errors.push(...child.errors) 134 | } 135 | } 136 | 137 | return { data, errors } 138 | } 139 | 140 | function collectErrors(entry: object) { 141 | if (entry instanceof Error) { 142 | return { data: null, errors: [entry] } 143 | } else if (Array.isArray(entry)) { 144 | return collectErrorsFromArray(entry) 145 | } else if (isObject(entry)) { 146 | return collectErrorsFromObject(entry) 147 | } else { 148 | return { data: entry, errors: null } 149 | } 150 | } 151 | 152 | export default LocalGraphQLClient 153 | -------------------------------------------------------------------------------- /packages/graphql-hooks/src/LocalGraphQLError.ts: -------------------------------------------------------------------------------- 1 | import { APIError, HttpError } from './types/common-types' 2 | 3 | /** Used to easily mock a query returning an error when using the `LocalGraphQLClient`. 4 | * This is a class so that the local mock client can use `instanceof` to detect it. 5 | */ 6 | class LocalGraphQLError 7 | implements APIError 8 | { 9 | fetchError?: Error 10 | httpError?: HttpError 11 | graphQLErrors?: TGraphQLError[] 12 | 13 | constructor(error: APIError) { 14 | this.fetchError = error.fetchError 15 | this.httpError = error.httpError 16 | this.graphQLErrors = error.graphQLErrors 17 | } 18 | } 19 | 20 | export default LocalGraphQLError 21 | -------------------------------------------------------------------------------- /packages/graphql-hooks/src/Middleware.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generic Middleware function that 3 | * will run object through all provided functions 4 | * 5 | * 6 | * Minimal example: 7 | * const MyMiddleware = ({ operation }, next) => { 8 | * operation.variables.user = 'admin' 9 | * next() 10 | * } 11 | * 12 | * All parameters provided are written in run function JSDoc 13 | */ 14 | export default class Middleware { 15 | constructor(fns) { 16 | if (fns.length === 0) { 17 | // Pass through 18 | fns.push((_, next) => next()) 19 | } 20 | 21 | for (const fn of fns) { 22 | if (typeof fn !== 'function') { 23 | throw new Error( 24 | 'GraphQLClient Middleware: middleware has to be of type `function`' 25 | ) 26 | } 27 | 28 | this.run = (stack => (opts, next) => { 29 | stack(opts, () => { 30 | fn.apply(this, [opts, next.bind.apply(next, [null, opts])]) 31 | }) 32 | })(this.run) 33 | } 34 | } 35 | 36 | /** 37 | * Run middleware 38 | * @param {opts.client} GraphQLClient instance 39 | * @param {opts.operation} Operation object with properties such as query and variables 40 | * @param {opts.resolve} Used to early resolve the request 41 | * @param {opts.addResponseHook} Hook that accepts a function that will be run after response is fetched 42 | * @param {opts.reject} User to early reject the request 43 | * @param {function} next 44 | */ 45 | run(opts, next) { 46 | next.apply(this, opts) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/graphql-hooks/src/canUseDOM.js: -------------------------------------------------------------------------------- 1 | export default () => 2 | typeof window !== 'undefined' && 3 | typeof window.document !== 'undefined' && 4 | typeof window.document.createElement !== 'undefined' 5 | -------------------------------------------------------------------------------- /packages/graphql-hooks/src/createRefetchMutationsMap.ts: -------------------------------------------------------------------------------- 1 | import type { TypedDocumentNode } from './types/typedDocumentNode' 2 | 3 | import { 4 | RefetchAfterMutationItem, 5 | RefetchAfterMutationsData 6 | } from './types/common-types' 7 | import { stringifyDocumentNode } from './utils' 8 | 9 | function isRefetchAfterMutationItem( 10 | item: unknown 11 | ): item is RefetchAfterMutationItem { 12 | return typeof item === 'object' && item != null && 'mutation' in item 13 | } 14 | 15 | function isTypedDocumentNode(item: unknown): item is TypedDocumentNode { 16 | return typeof item === 'object' && item != null && 'kind' in item 17 | } 18 | 19 | export default function createRefetchMutationsMap( 20 | refetchAfterMutations?: RefetchAfterMutationsData 21 | ) { 22 | if (!refetchAfterMutations) return {} 23 | 24 | const mutations = Array.isArray(refetchAfterMutations) 25 | ? refetchAfterMutations 26 | : [refetchAfterMutations] 27 | const result: Record< 28 | string, 29 | Pick 30 | > = {} 31 | 32 | mutations.forEach(mutationInfo => { 33 | if (mutationInfo == null) return 34 | 35 | if (typeof mutationInfo === 'string') { 36 | result[mutationInfo] = {} 37 | } else if (isRefetchAfterMutationItem(mutationInfo)) { 38 | const { filter, mutation, refetchOnMutationError = true } = mutationInfo 39 | 40 | result[mutation] = { 41 | filter, 42 | refetchOnMutationError 43 | } 44 | } else if (isTypedDocumentNode(mutationInfo)) { 45 | result[stringifyDocumentNode(mutationInfo)] = {} 46 | } 47 | }) 48 | 49 | return result 50 | } 51 | -------------------------------------------------------------------------------- /packages/graphql-hooks/src/events.ts: -------------------------------------------------------------------------------- 1 | export enum Events { 2 | DATA_INVALIDATED = 'DATA_INVALIDATED', 3 | DATA_UPDATED = 'DATA_UPDATED' 4 | } 5 | -------------------------------------------------------------------------------- /packages/graphql-hooks/src/index.ts: -------------------------------------------------------------------------------- 1 | import ClientContext from './ClientContext' 2 | import GraphQLClient from './GraphQLClient' 3 | import LocalGraphQLClient from './LocalGraphQLClient' 4 | import LocalGraphQLError from './LocalGraphQLError' 5 | import useClientRequest from './useClientRequest' 6 | import useQuery from './useQuery' 7 | import useQueryClient from './useQueryClient' 8 | import useSubscription from './useSubscription' 9 | import useMutation from './useMutation' 10 | import useManualQuery from './useManualQuery' 11 | 12 | export * from './types/common-types' 13 | 14 | 15 | export { 16 | ClientContext, 17 | GraphQLClient, 18 | LocalGraphQLClient, 19 | LocalGraphQLError, 20 | useClientRequest, 21 | useQuery, 22 | useQueryClient, 23 | useSubscription, 24 | useManualQuery, 25 | // alias 26 | useMutation 27 | } 28 | -------------------------------------------------------------------------------- /packages/graphql-hooks/src/isExtractableFileEnhanced.ts: -------------------------------------------------------------------------------- 1 | import { ExtractableFile, isExtractableFile } from 'extract-files' 2 | 3 | // Support streams for NodeJS compatibility 4 | const isExtractableFileEnhanced = ( 5 | value: any 6 | ): value is TFile => 7 | isExtractableFile(value) || 8 | // Check if stream 9 | // https://github.com/sindresorhus/is-stream/blob/3750505b0727f6df54324784fe369365ef78841e/index.js#L3 10 | (value !== null && 11 | typeof value === 'object' && 12 | typeof value.pipe === 'function') || 13 | // Check if formdata-node File 14 | // https://github.com/octet-stream/form-data/blob/14a6708f0ae28a5ffded8b6f8156394ba1d1244e/lib/File.ts#L29 15 | (value !== null && 16 | typeof value === 'object' && 17 | typeof value.stream === 'function') 18 | 19 | export default isExtractableFileEnhanced 20 | -------------------------------------------------------------------------------- /packages/graphql-hooks/src/middlewares/README.md: -------------------------------------------------------------------------------- 1 | # GraphQLClient Middleware 2 | 3 | A middleware gives you the option to access and intercept requests before and after they are performed. 4 | 5 | In GraphQLClient the middleware functions are passed in the GraphQLClient's config under the `middleware` key. They are executed in the given order. 6 | 7 | ## Minimal example 8 | 9 | ```js 10 | const LoggerMiddleware = ({ operation }, next) => { 11 | console.log('Starting request:', operation) 12 | next() 13 | } 14 | } 15 | ``` 16 | 17 | ## Parameters 18 | 19 | Middleware is a function with 2 parameters: 20 | 21 | - `options` object: 22 | - `operation` - GraphQL operation object - includes `query`, `variables` etc. 23 | - `addResponseHook` - a function that can be called from within the middleware to add a response hook - it accepts a handler to read/transform and return the data - passthrough ex.: `addResponseHook(response => response)` 24 | - `client` - the GraphQLClient instance 25 | - `resolve`, `reject` - advanced usage only, these would be used only when we want to resolve/reject the request `Promise` early without doing the usual fetch (see [example](examples/cacheMiddleware.ts)) 26 | - `next` function - calls the next middleware function in line. Generally it should always be called, unless we want to change the control flow 27 | 28 | ## Automatic Persisted Queries (APQ) middleware 29 | 30 | This library also includes an Automatic Persisted Queries middleware that can improve network performance by sending smaller requests. The implementation should match the Apollo back-end functionality that is [further described here](https://www.apollographql.com/docs/apollo-server/v2/performance/apq/). 31 | 32 | **Usage**: 33 | 34 | ```js 35 | import { GraphQLClient } from 'graphql-hooks' 36 | import { APQMiddleware } from 'graphql-hooks/lib/middlewares/apqMiddleware' 37 | 38 | const client = new GraphQLClient({ 39 | middleware: [APQMiddleware], 40 | url: 'localhost:3000/graphql' 41 | }) 42 | ``` 43 | 44 | ## More examples 45 | 46 | See [examples folder](examples/) 47 | 48 | ### Update response post-request 49 | 50 | ```js 51 | // Convert response object keys to camelCase 52 | const camelCaseMiddleware = ({ operation, addResponseHook }, next) => { 53 | addResponseHook(response => { 54 | // Need to return 55 | return toCamelCaseDeep(response) 56 | }) 57 | // Continue executing the next middleware 58 | next() 59 | } 60 | ``` 61 | 62 | ### Change the control flow (async) 63 | 64 | ```js 65 | // Check pre-request if API is up and running 66 | const healthCheckMiddleware = async ({ operation, client, reject }, next) => { 67 | const isWorking = await client.request('/health') 68 | if (isWorking) { 69 | // Everything's good, fire the request 70 | next() 71 | } else { 72 | // Server is down, don't continue in the middleware execution and fail the req early 73 | reject({ message: 'Server is down' }) 74 | } 75 | } 76 | ``` 77 | -------------------------------------------------------------------------------- /packages/graphql-hooks/src/middlewares/apqMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { Sha256 } from '@aws-crypto/sha256-browser' 2 | import { Buffer } from 'buffer' 3 | import { 4 | APIError, 5 | MiddlewareFunction, 6 | GraphQLResponseError 7 | } from '../types/common-types' 8 | 9 | export async function sha256(query) { 10 | const hash = new Sha256() 11 | hash.update(query, 'utf8') 12 | const hashUint8Array = await hash.digest() 13 | 14 | const hashBuffer = Buffer.from(hashUint8Array) 15 | return hashBuffer.toString('hex') 16 | } 17 | 18 | type APQExtension = { 19 | persistedQuery: { 20 | version: number 21 | sha256Hash: string 22 | } 23 | } 24 | 25 | function isPersistedQueryNotFound(error: APIError) { 26 | if ((error?.fetchError as any)?.type === 'PERSISTED_QUERY_NOT_FOUND') { 27 | return true 28 | } 29 | 30 | let errors = error?.graphQLErrors ?? [] 31 | 32 | if (error.httpError) { 33 | try { 34 | const body = JSON.parse(error.httpError.body) 35 | errors = errors.concat(body.errors ?? []) 36 | } catch { 37 | return false 38 | } 39 | } 40 | 41 | return errors.some(e => e.message === 'PersistedQueryNotFound') 42 | } 43 | 44 | /** 45 | * AutomaticPersistedQueryMiddleware - must be last in the middleware list 46 | * @param {function} makeRequest 47 | * @returns Promise 48 | */ 49 | export const APQMiddleware: MiddlewareFunction = async ( 50 | { operation, client, resolve, reject }, 51 | next 52 | ) => { 53 | try { 54 | operation.extensions = { 55 | ...operation.extensions, 56 | persistedQuery: { 57 | version: 1, 58 | sha256Hash: await sha256(operation.query) 59 | } 60 | } 61 | 62 | // Try to send just the hash 63 | const res = await client.requestViaHttp( 64 | { ...operation, query: null }, 65 | { 66 | fetchOptionsOverrides: { method: 'GET' } 67 | } 68 | ) 69 | 70 | // Data fetched successfully -> resolve early 71 | if (!res.error) { 72 | return resolve(res) 73 | } 74 | 75 | const { error } = res 76 | 77 | // If a server has not recognized the hash, send both query and hash 78 | if (isPersistedQueryNotFound(error)) { 79 | next() 80 | } else { 81 | throw error 82 | } 83 | } catch (err: any) { 84 | reject(err) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /packages/graphql-hooks/src/middlewares/examples/cacheMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { MiddlewareFunction } from '../../types/common-types' 2 | 3 | /** 4 | * CacheMiddleware - just an extremely naive example 5 | * @param {function} makeRequest 6 | * @returns Promise 7 | */ 8 | const CacheMiddleware = (): MiddlewareFunction => { 9 | const cache = new Map() 10 | 11 | return ({ operation, addResponseHook, resolve }, next) => { 12 | if (cache.get(operation.query)) { 13 | return resolve(cache.get(operation.query)) 14 | } 15 | 16 | addResponseHook(res => { 17 | cache.set(operation.query, res) 18 | return res 19 | }) 20 | 21 | next() 22 | } 23 | } 24 | 25 | export default CacheMiddleware 26 | -------------------------------------------------------------------------------- /packages/graphql-hooks/src/middlewares/examples/debugMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { MiddlewareFunction } from '../../types/common-types' 2 | 3 | /** 4 | * DebugMiddleware - example 5 | * @param {function} logger 6 | */ 7 | const DebugMiddleware = 8 | (logger = console.log): MiddlewareFunction => 9 | (opts, next) => { 10 | logger('Start request:', opts.operation) 11 | opts.addResponseHook(res => { 12 | logger('End request:', res) 13 | return res 14 | }) 15 | next() 16 | } 17 | 18 | export default DebugMiddleware 19 | -------------------------------------------------------------------------------- /packages/graphql-hooks/src/types/typedDocumentNode.ts: -------------------------------------------------------------------------------- 1 | import { DocumentNode } from '@0no-co/graphql.web' 2 | 3 | export interface DocumentTypeDecoration { 4 | /** 5 | * This type is used to ensure that the variables you pass in to the query are assignable to Variables 6 | * and that the Result is assignable to whatever you pass your result to. The method is never actually 7 | * implemented, but the type is valid because we list it as optional 8 | */ 9 | __apiType?: (variables: TVariables) => TResult 10 | } 11 | export interface TypedDocumentNode< 12 | TResult = { 13 | [key: string]: any 14 | }, 15 | TVariables = { 16 | [key: string]: any 17 | } 18 | > extends DocumentNode, 19 | DocumentTypeDecoration {} 20 | -------------------------------------------------------------------------------- /packages/graphql-hooks/src/useDeepCompareCallback.ts: -------------------------------------------------------------------------------- 1 | import { useDeepCompareMemoize } from 'use-deep-compare-effect' 2 | import React from 'react' 3 | 4 | type UseCallbackParameters = Parameters 5 | type UseCallbackCallback = UseCallbackParameters[0] 6 | type DependencyList = UseCallbackParameters[1] 7 | 8 | export function useDeepCompareCallback( 9 | callback: T, 10 | deps: DependencyList 11 | ) { 12 | return React.useCallback(callback, useDeepCompareMemoize(deps)) 13 | } 14 | -------------------------------------------------------------------------------- /packages/graphql-hooks/src/useManualQuery.ts: -------------------------------------------------------------------------------- 1 | import type { TypedDocumentNode } from './types/typedDocumentNode' 2 | 3 | import { 4 | FetchData, 5 | ResetFunction, 6 | UseClientRequestOptions, 7 | UseClientRequestResult 8 | } from './types/common-types' 9 | import useClientRequest from './useClientRequest' 10 | 11 | const useManualQuery = < 12 | ResponseData = any, 13 | Variables = object, 14 | TGraphQLError = object 15 | >( 16 | query: string | TypedDocumentNode, 17 | options: Omit< 18 | UseClientRequestOptions, 19 | 'useCache' | 'isManual' 20 | > = {} 21 | ): [ 22 | FetchData, 23 | UseClientRequestResult, 24 | ResetFunction 25 | ] => 26 | useClientRequest(query, { useCache: true, isManual: true, ...options }) as any 27 | 28 | export default useManualQuery 29 | -------------------------------------------------------------------------------- /packages/graphql-hooks/src/useMutation.ts: -------------------------------------------------------------------------------- 1 | import type { TypedDocumentNode } from './types/typedDocumentNode' 2 | 3 | import useClientRequest from './useClientRequest' 4 | import { 5 | UseClientRequestOptions, 6 | FetchData, 7 | UseClientRequestResult, 8 | ResetFunction 9 | } from './types/common-types' 10 | 11 | const useMutation = < 12 | ResponseData = any, 13 | Variables = object, 14 | TGraphQLError = object 15 | >( 16 | query: string | TypedDocumentNode, 17 | options: Omit< 18 | UseClientRequestOptions, 19 | 'isMutation' 20 | > = {} 21 | ): [ 22 | FetchData, 23 | UseClientRequestResult, 24 | ResetFunction 25 | ] => 26 | useClientRequest(query, { 27 | isMutation: true, 28 | ...options 29 | }) as any 30 | 31 | export default useMutation 32 | -------------------------------------------------------------------------------- /packages/graphql-hooks/src/useQuery.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import type { TypedDocumentNode } from './types/typedDocumentNode' 3 | 4 | import ClientContext from './ClientContext' 5 | import createRefetchMutationsMap from './createRefetchMutationsMap' 6 | import useClientRequest from './useClientRequest' 7 | 8 | import { 9 | GraphQLResponseError, 10 | UseQueryOptions, 11 | UseQueryResult 12 | } from './types/common-types' 13 | 14 | const defaultOpts = { 15 | useCache: true, 16 | skip: false, 17 | throwErrors: false 18 | } 19 | 20 | function useQuery< 21 | ResponseData = any, 22 | Variables = object, 23 | TGraphQLError extends GraphQLResponseError = GraphQLResponseError, 24 | TRefetchData = any, 25 | TRefetchVariables = object 26 | >( 27 | query: string | TypedDocumentNode, 28 | opts: UseQueryOptions = {} 29 | ): UseQueryResult { 30 | const allOpts = { 31 | ...defaultOpts, 32 | ...opts 33 | } 34 | const contextClient = React.useContext(ClientContext) 35 | const client = opts.client || contextClient 36 | const [calledDuringSSR, setCalledDuringSSR] = React.useState(false) 37 | const [queryReq, state] = useClientRequest< 38 | ResponseData, 39 | Variables, 40 | TGraphQLError 41 | >(query, allOpts) 42 | 43 | if (!client) { 44 | throw new Error( 45 | 'useQuery() requires a client to be passed in the options or as a context value' 46 | ) 47 | } 48 | 49 | if ( 50 | client.ssrMode && 51 | opts.ssr !== false && 52 | !calledDuringSSR && 53 | !opts.skipCache && 54 | !opts.skip 55 | ) { 56 | // result may already be in the cache from previous SSR iterations 57 | if (!state.data && !state.error) { 58 | const p = queryReq() 59 | client.ssrPromises.push(p) 60 | } 61 | setCalledDuringSSR(true) 62 | } 63 | 64 | const { client: clientFromOpts, ...allOptsToStringify } = allOpts 65 | const stringifiedAllOpts = JSON.stringify(allOptsToStringify) 66 | React.useEffect(() => { 67 | if (allOpts.skip) { 68 | return 69 | } 70 | 71 | queryReq() 72 | }, [query, stringifiedAllOpts]) // eslint-disable-line react-hooks/exhaustive-deps 73 | 74 | React.useEffect(() => { 75 | if (state.error && allOpts.throwErrors) { 76 | throw state.error 77 | } 78 | }, [state.error, allOpts.throwErrors]) 79 | 80 | const refetch = React.useCallback( 81 | (options = {}) => 82 | queryReq({ 83 | skipCache: true, 84 | // don't call the updateData that has been passed into useQuery here 85 | // reset to the default behaviour of returning the raw query result 86 | // this can be overridden in refetch options 87 | updateData: (_, data) => data, 88 | ...options 89 | }), 90 | [queryReq] 91 | ) 92 | 93 | React.useEffect( 94 | function subscribeToMutationsAndRefetch() { 95 | const mutationsMap = createRefetchMutationsMap(opts.refetchAfterMutations) 96 | const mutations = Object.keys(mutationsMap) 97 | 98 | const afterConditionsCheckRefetch = ({ mutation, variables, result }) => { 99 | const { filter, refetchOnMutationError } = mutationsMap[mutation] 100 | 101 | const hasValidFilterOrNoFilter = 102 | !filter || (variables && filter(variables)) 103 | 104 | const shouldRefetch = refetchOnMutationError || !result.error 105 | 106 | if (hasValidFilterOrNoFilter && shouldRefetch) { 107 | refetch() 108 | } 109 | } 110 | 111 | mutations.forEach(mutation => { 112 | // this event is emitted from useClientRequest 113 | client.mutationsEmitter.on(mutation, afterConditionsCheckRefetch) 114 | }) 115 | 116 | return () => { 117 | mutations.forEach(mutation => { 118 | client.mutationsEmitter.removeListener( 119 | mutation, 120 | afterConditionsCheckRefetch 121 | ) 122 | }) 123 | } 124 | }, 125 | [opts.refetchAfterMutations, refetch, client.mutationsEmitter] 126 | ) 127 | 128 | return { 129 | ...state, 130 | refetch 131 | } 132 | } 133 | 134 | export default useQuery 135 | -------------------------------------------------------------------------------- /packages/graphql-hooks/src/useQueryClient.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | 3 | import ClientContext from './ClientContext' 4 | 5 | function useQueryClient() { 6 | return useContext(ClientContext) 7 | } 8 | 9 | export default useQueryClient 10 | -------------------------------------------------------------------------------- /packages/graphql-hooks/src/useSubscription.ts: -------------------------------------------------------------------------------- 1 | import { useContext, useRef } from 'react' 2 | import useDeepCompareEffect from 'use-deep-compare-effect' 3 | 4 | import ClientContext from './ClientContext' 5 | 6 | import { UseSubscriptionOperation } from './types/common-types' 7 | 8 | function useSubscription< 9 | ResponseData = any, 10 | Variables extends object = object, 11 | TGraphQLError = object 12 | >( 13 | options: UseSubscriptionOperation & { skip?: boolean }, 14 | callback: (response: { 15 | data?: ResponseData 16 | errors?: TGraphQLError[] 17 | }) => void 18 | ): void { 19 | const callbackRef = useRef(callback) 20 | callbackRef.current = callback 21 | 22 | const contextClient = useContext(ClientContext) 23 | const client = options.client || contextClient 24 | 25 | if (!client) { 26 | throw new Error( 27 | 'useSubscription() requires a client to be passed in the options or as a context value' 28 | ) 29 | } 30 | 31 | useDeepCompareEffect(() => { 32 | if (options.skip) { 33 | return 34 | } 35 | 36 | const request = { 37 | query: options.query, 38 | variables: options.variables 39 | } 40 | 41 | const observable = client.createSubscription(request) 42 | 43 | const subscription = observable.subscribe({ 44 | next: result => { 45 | callbackRef.current(result as any) 46 | }, 47 | error: errors => { 48 | callbackRef.current({ errors } as any) 49 | }, 50 | complete: () => { 51 | subscription.unsubscribe() 52 | } 53 | }) 54 | 55 | return () => { 56 | subscription.unsubscribe() 57 | } 58 | }, [options.query, options.variables, options.skip]) 59 | } 60 | 61 | export default useSubscription 62 | -------------------------------------------------------------------------------- /packages/graphql-hooks/src/utils.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | DocumentNode, 3 | OperationDefinitionNode 4 | } from 'graphql/language/ast' 5 | import { Kind, print } from '@0no-co/graphql.web' 6 | 7 | /** 8 | * Pipe with support for async functions 9 | * @param {array} array of functions 10 | * @returns single function 11 | */ 12 | export const pipeP = (fns: (() => any)[]) => (arg: any) => 13 | fns.reduce((p, f) => p.then(f), Promise.resolve(arg)) 14 | 15 | export function extractOperationName(document: DocumentNode | string): string | undefined { 16 | let operationName: string | undefined = undefined 17 | 18 | if (typeof document !== 'string') { 19 | const operationDefinitions = document.definitions.filter( 20 | definition => definition.kind === Kind.OPERATION_DEFINITION 21 | ) as OperationDefinitionNode[] 22 | 23 | if (operationDefinitions.length === 1) { 24 | operationName = operationDefinitions[0]?.name?.value 25 | } 26 | } 27 | return operationName 28 | } 29 | 30 | export function stringifyDocumentNode( 31 | document: TNode 32 | ): string { 33 | if (typeof document === 'string') { 34 | return document 35 | } 36 | 37 | return print(document) 38 | } 39 | -------------------------------------------------------------------------------- /packages/graphql-hooks/test-jsdom/setup.js: -------------------------------------------------------------------------------- 1 | global.fetch = require('jest-fetch-mock') 2 | 3 | if (typeof global.TextEncoder === 'undefined') { 4 | const { TextEncoder } = require('util') 5 | global.TextEncoder = TextEncoder 6 | } 7 | 8 | global.structuredClone = jest.fn(function (obj) { 9 | return JSON.parse(JSON.stringify(obj)) 10 | }) 11 | -------------------------------------------------------------------------------- /packages/graphql-hooks/test-jsdom/unit/Middleware.test.ts: -------------------------------------------------------------------------------- 1 | import Middleware from '../../src/Middleware' 2 | 3 | describe('Middleware', () => { 4 | const dateCreatedMiddie = ({ operation }, next) => { 5 | operation.variables.dateCreated = '2025-02-02' 6 | next() 7 | } 8 | const sideEffectMiddie = 9 | logger => 10 | ({ operation }, next) => { 11 | setTimeout(() => { 12 | Object.entries(operation.variables).forEach(([key, value]) => 13 | logger('VARIABLE IS', key, value) 14 | ) 15 | next() 16 | }, 20) 17 | } 18 | const initialOperation = { 19 | variables: { 20 | age: 25, 21 | name: 'Jon Snow' 22 | } 23 | } 24 | 25 | it('throws error when middleware provided is not a function', () => { 26 | expect(() => new Middleware([initialOperation])).toThrow( 27 | 'GraphQLClient Middleware: middleware has to be of type `function`' 28 | ) 29 | }) 30 | 31 | it('pipes data through a single middleware', done => { 32 | const m = new Middleware([dateCreatedMiddie]) 33 | m.run({ operation: initialOperation }, results => { 34 | expect(results).toEqual({ 35 | operation: { 36 | variables: { 37 | ...initialOperation.variables, 38 | dateCreated: '2025-02-02' 39 | } 40 | } 41 | }) 42 | done() 43 | }) 44 | }) 45 | 46 | it('pipes data through multiple middlewares', done => { 47 | const logger = jest.fn() 48 | const m = new Middleware([dateCreatedMiddie, sideEffectMiddie(logger)]) 49 | m.run({ operation: initialOperation }, results => { 50 | expect(results).toEqual({ 51 | operation: { 52 | variables: { 53 | ...initialOperation.variables, 54 | dateCreated: '2025-02-02' 55 | } 56 | } 57 | }) 58 | expect(logger.mock.calls).toEqual([ 59 | ['VARIABLE IS', 'age', 25], 60 | ['VARIABLE IS', 'name', 'Jon Snow'], 61 | ['VARIABLE IS', 'dateCreated', '2025-02-02'] 62 | ]) 63 | done() 64 | }) 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /packages/graphql-hooks/test-jsdom/unit/createRefetchMutationsMap.test.ts: -------------------------------------------------------------------------------- 1 | import createRefetchMutationsMap from '../../src/createRefetchMutationsMap' 2 | 3 | describe('createRefetchMutationsMap', () => { 4 | it('accepts a string parameter', () => { 5 | const result = createRefetchMutationsMap('my-mutation') 6 | 7 | expect(result).toEqual({ 8 | 'my-mutation': {} 9 | }) 10 | }) 11 | 12 | it('accepts an object parameter', () => { 13 | const result = createRefetchMutationsMap({ mutation: 'my-mutation' }) 14 | 15 | expect(result).toEqual({ 16 | 'my-mutation': { refetchOnMutationError: true } 17 | }) 18 | }) 19 | 20 | it('accepts an array parameter', () => { 21 | const filter = () => {} 22 | const result = createRefetchMutationsMap([ 23 | { mutation: 'my-mutation' }, 24 | 'my-mutation-2', 25 | { mutation: 'my-mutation-3', filter } 26 | ]) 27 | 28 | expect(result).toEqual({ 29 | 'my-mutation': { refetchOnMutationError: true }, 30 | 'my-mutation-2': {}, 31 | 'my-mutation-3': { filter, refetchOnMutationError: true } 32 | }) 33 | }) 34 | 35 | it('filters out invalid parameters', () => { 36 | const result = createRefetchMutationsMap([ 37 | { mutation: 'my-mutation' }, 38 | 2, 39 | null, 40 | undefined 41 | ]) 42 | 43 | expect(result).toEqual({ 44 | 'my-mutation': { refetchOnMutationError: true } 45 | }) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /packages/graphql-hooks/test-jsdom/unit/useDeepCompareCallback.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react' 2 | import { useDeepCompareCallback } from '../../src/useDeepCompareCallback' 3 | 4 | function renderHookWithDeps(callback: (props: T) => U, props: T) { 5 | return renderHook(callback, { 6 | initialProps: props 7 | }) 8 | } 9 | 10 | describe('useDeepCompareCallback', () => { 11 | it('should get the same instance if the deps is an empty array and rerender an empty array', () => { 12 | const { result, rerender } = renderHookWithDeps( 13 | ({ deps }) => 14 | useDeepCompareCallback(() => { 15 | return deps 16 | }, deps), 17 | { 18 | deps: [] 19 | } 20 | ) 21 | const firstResult = result.current 22 | rerender({ deps: [] }) 23 | expect(result.current).toEqual(firstResult) 24 | }) 25 | 26 | it('should get the same instance if the deps is defined and rerender with a clone of the deps', () => { 27 | const deps = [{ a: 1 }, { b: 1 }] 28 | const { result, rerender } = renderHookWithDeps( 29 | ({ deps }) => 30 | useDeepCompareCallback(() => { 31 | return deps 32 | }, deps), 33 | { 34 | deps 35 | } 36 | ) 37 | const firstResult = result.current 38 | rerender({ deps: structuredClone(deps) }) 39 | expect(result.current).toEqual(firstResult) 40 | }) 41 | 42 | it('should get a new instance if the deps is changed', () => { 43 | const { result, rerender } = renderHookWithDeps( 44 | ({ deps }) => 45 | useDeepCompareCallback(() => { 46 | return deps 47 | }, deps), 48 | { 49 | deps: [{ a: 1 }] 50 | } 51 | ) 52 | const firstResult = result.current 53 | rerender({ deps: [{ a: 2 }] }) 54 | expect(result.current).not.toEqual(firstResult) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /packages/graphql-hooks/test-jsdom/unit/useManualQuery.test.ts: -------------------------------------------------------------------------------- 1 | import { useManualQuery, useClientRequest } from '../../src' 2 | 3 | jest.mock('../../src/useClientRequest') 4 | 5 | const TEST_QUERY = `query Test($limit: Int) { 6 | tests(limit: $limit) { 7 | id 8 | } 9 | }` 10 | 11 | describe('useManualQuery', () => { 12 | it('calls useClientRequest with useCache set to true & options', () => { 13 | useManualQuery(TEST_QUERY, { isMutation: true }) 14 | expect(useClientRequest).toHaveBeenCalledWith(TEST_QUERY, { 15 | useCache: true, 16 | isMutation: true, 17 | isManual: true 18 | }) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /packages/graphql-hooks/test-jsdom/unit/useMutation.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { act, render, renderHook, screen, fireEvent } from '@testing-library/react' 3 | import { useMutation, useClientRequest, GraphQLClient, ClientContext } from '../../src' 4 | 5 | jest.mock('../../src/useMutation.ts') 6 | 7 | const TEST_QUERY = `query Test($limit: Int) { 8 | tests(limit: $limit) { 9 | id 10 | } 11 | }` 12 | 13 | const useMutationMock = useMutation as jest.MockedFunction< 14 | typeof useMutation 15 | > 16 | 17 | const client = new GraphQLClient({ url: 'http://localhost:8080' }) 18 | 19 | const TestComponent = ({ onSuccess }) => { 20 | const [callMutation] = useMutation(TEST_QUERY, { onSuccess }) || [] 21 | 22 | const handleButtonClick = () => callMutation ? callMutation({ variables: { hello: 'World' }}) : null 23 | 24 | return ( 25 | 26 | ) 27 | } 28 | 29 | describe('useMutation', () => { 30 | const useClientRequestMock = jest.fn() 31 | 32 | it('calls useClientRequest with options and isMutation set to true', () => { 33 | 34 | useMutationMock.mockImplementationOnce(useClientRequestMock) 35 | 36 | renderHook(() => useMutation(TEST_QUERY, { isManual: true })) 37 | 38 | expect(useClientRequestMock).toHaveBeenCalledWith(TEST_QUERY, { 39 | isManual: true 40 | }) 41 | }) 42 | 43 | it('should call onSuccess function when the request finish successfully', async () => { 44 | const resultMock = { data: 'It works!' } 45 | const onSuccessMock = jest.fn() 46 | const variablesMock = { hello: 'World' } 47 | 48 | useMutationMock.mockImplementationOnce(useClientRequest) 49 | client.request = jest.fn(() => Promise.resolve(resultMock)) as any 50 | 51 | render( 52 | 53 | 54 | 55 | ) 56 | 57 | await act(async () => { 58 | await fireEvent.click(screen.getByTestId('btn-test-me')) 59 | }) 60 | 61 | expect(onSuccessMock).toHaveBeenCalledTimes(1) 62 | expect(onSuccessMock).toHaveBeenCalledWith(resultMock, variablesMock) 63 | }) 64 | 65 | it('should not call onSuccess function when the request finish with an error', async () => { 66 | const resultMock = { error: 'Request error', data: 'It works!' } 67 | const onSuccessMock = jest.fn() 68 | const variablesMock = { hello: 'World' } 69 | 70 | useMutationMock.mockImplementationOnce(useClientRequest) 71 | client.request = jest.fn(() => Promise.resolve(resultMock)) as any 72 | 73 | render( 74 | 75 | 76 | 77 | ) 78 | 79 | await act(async () => { 80 | await fireEvent.click(screen.getByTestId('btn-test-me')) 81 | }) 82 | 83 | expect(onSuccessMock).toHaveBeenCalledTimes(0) 84 | }) 85 | }) 86 | -------------------------------------------------------------------------------- /packages/graphql-hooks/test-jsdom/unit/useQueryClient.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { renderHook } from '@testing-library/react' 3 | import { ClientContext, useQueryClient, GraphQLClient } from '../../src' 4 | 5 | const client = new GraphQLClient({ url: 'http://localhost:8080' }) 6 | 7 | const Wrapper = ({ children }) => ( 8 | 9 | {children} 10 | 11 | ) 12 | 13 | describe('useQueryClient', () => { 14 | it('should return the graphql client provided to the ClientContext', () => { 15 | const { result } = renderHook(() => useQueryClient(), { wrapper: Wrapper }) 16 | expect(result.current).toBe(client) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /packages/graphql-hooks/test-jsdom/unit/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { pipeP } from '../../src/utils' 2 | 3 | describe('GraphQLClient utils', () => { 4 | describe('pipeP', () => { 5 | it('pipes through sync functions in order', async () => { 6 | const mock = jest.fn() 7 | const fnA = () => { 8 | mock('fnA') 9 | } 10 | const fnB = () => mock('fnB') 11 | await pipeP([fnA, fnB])({}) 12 | expect(mock).toHaveBeenCalledTimes(2) 13 | expect(mock.mock.calls).toEqual([['fnA'], ['fnB']]) 14 | }) 15 | 16 | it('pipes through async functions in order', async () => { 17 | const mock = jest.fn() 18 | const fnA = async () => { 19 | await new Promise(res => setTimeout(res, 20)) 20 | mock('fnA') 21 | } 22 | const fnB = async () => { 23 | await new Promise(res => setTimeout(res, 30)) 24 | mock('fnB') 25 | } 26 | await pipeP([fnA, fnB])({}) 27 | expect(mock).toHaveBeenCalledTimes(2) 28 | expect(mock.mock.calls).toEqual([['fnA'], ['fnB']]) 29 | }) 30 | 31 | it('pipes through async and sync functions in order', async () => { 32 | const mock = jest.fn() 33 | const fnA = async () => { 34 | await new Promise(res => setTimeout(res, 20)) 35 | mock('fnA') 36 | } 37 | const fnB = () => mock('fnB') 38 | const fnC = async () => { 39 | await new Promise(res => setTimeout(res, 30)) 40 | mock('fnC') 41 | } 42 | await pipeP([fnA, fnB, fnC])({}) 43 | expect(mock).toHaveBeenCalledTimes(3) 44 | expect(mock.mock.calls).toEqual([['fnA'], ['fnB'], ['fnC']]) 45 | }) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /packages/graphql-hooks/test-jsdom/utils.ts: -------------------------------------------------------------------------------- 1 | import { SubscriptionClient } from 'subscriptions-transport-ws' 2 | import { Cache } from '../src' 3 | 4 | export function createMockResponse(options: Partial = {}): Response { 5 | const response = new Response() 6 | return { 7 | ...response, 8 | ...options 9 | } 10 | } 11 | 12 | export function createMockCache(): Cache { 13 | return { 14 | get: () => ({}), 15 | set: () => {}, 16 | delete: () => {}, 17 | clear: () => {}, 18 | keys: () => null, 19 | getInitialState: () => ({}) 20 | } 21 | } 22 | 23 | export function createMockSubscriptionClient(): SubscriptionClient { 24 | return new SubscriptionClient('ws://localhost') 25 | } 26 | -------------------------------------------------------------------------------- /packages/graphql-hooks/test-node/sample.txt: -------------------------------------------------------------------------------- 1 | sample -------------------------------------------------------------------------------- /packages/graphql-hooks/test-node/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { DefinitionNode, DocumentNode, Kind } from 'graphql' 2 | import { extractOperationName } from '../src/utils' 3 | 4 | describe('extractOperationName', () => { 5 | it('should return the operation name from a DocumentNode', () => { 6 | const document = { 7 | kind: Kind.DOCUMENT, 8 | definitions: [ 9 | { 10 | kind: Kind.OPERATION_DEFINITION, 11 | name: { 12 | kind: Kind.NAME, 13 | value: 'MyQuery' 14 | } 15 | } as DefinitionNode 16 | ] 17 | } as DocumentNode 18 | 19 | const operationName = extractOperationName(document) 20 | expect(operationName).toBe('MyQuery') 21 | }) 22 | 23 | it('should return undefined if the DocumentNode does not have an operation name', () => { 24 | const document = { 25 | kind: Kind.DOCUMENT, 26 | definitions: [ 27 | { 28 | kind: Kind.OPERATION_DEFINITION 29 | } as DefinitionNode 30 | ] 31 | } as DocumentNode 32 | 33 | const operationName = extractOperationName(document) 34 | expect(operationName).toBeUndefined() 35 | }) 36 | 37 | it('should return undefined if the DocumentNode does not have an operation definition', () => { 38 | const document = { 39 | kind: Kind.DOCUMENT, 40 | definitions: [ 41 | { 42 | kind: Kind.FRAGMENT_DEFINITION 43 | } as DefinitionNode 44 | ] 45 | } as DocumentNode 46 | 47 | const operationName = extractOperationName(document) 48 | expect(operationName).toBeUndefined() 49 | }) 50 | 51 | it('should return undefined if the input is not a DocumentNode', () => { 52 | const document = 'query MyQuery { ... }' 53 | 54 | const operationName = extractOperationName(document) 55 | expect(operationName).toBeUndefined() 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /packages/graphql-hooks/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../config/tsconfig.base", 3 | "include": ["src"], 4 | "compilerOptions": { 5 | "outDir": "lib" 6 | } 7 | } 8 | --------------------------------------------------------------------------------