├── .eslintignore
├── .eslintrc.js
├── .github
├── dependabot.yml
└── workflows
│ ├── ci.yml
│ └── codeql-analysis.yml
├── .gitignore
├── .prettierrc
├── MIT-LICENSE.txt
├── README.md
├── babel.config.js
├── docs
├── .gitignore
├── README.md
├── babel.config.js
├── docs
│ ├── api-reference
│ │ ├── _category_.json
│ │ ├── createCacheUpdaterLink.md
│ │ ├── usePagination.md
│ │ └── withCacheUpdater.md
│ ├── directives
│ │ ├── _category_.json
│ │ ├── appendNode-prependNode.md
│ │ ├── argumentDefinitions.md
│ │ ├── arguments.md
│ │ ├── deleteRecord.md
│ │ ├── pagination.md
│ │ └── refetchable.md
│ ├── getting-started
│ │ ├── _category_.json
│ │ ├── basic-usage
│ │ │ ├── _category_.json
│ │ │ ├── pagination.md
│ │ │ └── updating-data.md
│ │ ├── configuration.md
│ │ └── installation.md
│ ├── graphql-code-generator
│ │ ├── _category_.json
│ │ └── graphql-codegen-preset.md
│ ├── guides
│ │ ├── _category_.json
│ │ ├── ide-settings.md
│ │ ├── local-variables.md
│ │ ├── pagination.md
│ │ ├── relay-spec.md
│ │ └── updating-data.md
│ └── introduction.md
├── docusaurus.config.js
├── package.json
├── sidebars.js
├── src
│ ├── components
│ │ └── HomepageFeatures
│ │ │ ├── index.tsx
│ │ │ └── styles.module.css
│ ├── css
│ │ └── custom.css
│ └── pages
│ │ ├── index.module.css
│ │ └── index.tsx
├── static
│ ├── .nojekyll
│ └── img
│ │ ├── favicon.ico
│ │ └── logo.svg
├── tsconfig.json
└── yarn.lock
├── examples
└── app
│ ├── .env.development
│ ├── .prettierignore
│ ├── README.md
│ ├── apollo.config.js
│ ├── codegen.yml
│ ├── graphql.config.js
│ ├── package.json
│ ├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
│ ├── schema.graphql
│ ├── server-tsconfig.json
│ ├── src
│ ├── App.css
│ ├── App.tsx
│ ├── List.tsx
│ ├── ListItem.tsx
│ ├── generated
│ │ ├── graphql.ts
│ │ └── introspection-result.json
│ ├── index.css
│ ├── index.tsx
│ ├── logo.svg
│ ├── react-app-env.d.ts
│ └── server
│ │ └── index.ts
│ └── tsconfig.json
├── jest.config.js
├── lerna.json
├── package.json
├── packages
├── config
│ ├── README.md
│ ├── package.json
│ ├── scripts
│ │ └── generateClientSchema.js
│ ├── src
│ │ ├── __tests__
│ │ │ └── clientSchema.test.ts
│ │ ├── apolloConfig.ts
│ │ ├── clientDirective.ts
│ │ ├── clientSchema.ts
│ │ ├── dependencies
│ │ │ ├── apollo.ts
│ │ │ └── graphql.ts
│ │ ├── index.ts
│ │ └── validationRules
│ │ │ ├── __tests__
│ │ │ └── paginationDirective.test.ts
│ │ │ ├── index.ts
│ │ │ ├── paginationDirective.ts
│ │ │ └── validationRule.ts
│ └── tsconfig.build.json
├── core
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ ├── hooks
│ │ │ ├── __tests__
│ │ │ │ ├── usePagination.mock.ts
│ │ │ │ └── usePagination.test.ts
│ │ │ ├── index.ts
│ │ │ └── usePagination.ts
│ │ ├── index.ts
│ │ ├── links
│ │ │ ├── __tests__
│ │ │ │ └── cacheUpdaterLink.test.ts
│ │ │ ├── cacheUpdaterLink.ts
│ │ │ ├── index.ts
│ │ │ └── utils.ts
│ │ ├── policies
│ │ │ ├── cacheUpdater
│ │ │ │ ├── __tests__
│ │ │ │ │ ├── deleteRecord.mock.ts
│ │ │ │ │ ├── deleteRecord.test.ts
│ │ │ │ │ └── pagination
│ │ │ │ │ │ ├── appendNode.mock.ts
│ │ │ │ │ │ ├── appendNode.test.ts
│ │ │ │ │ │ ├── connectionId.mock.ts
│ │ │ │ │ │ ├── connectionId.test.ts
│ │ │ │ │ │ ├── mock.ts
│ │ │ │ │ │ ├── prependNode.mock.ts
│ │ │ │ │ │ └── prependNode.test.ts
│ │ │ │ ├── deleteRecord.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── pagination.ts
│ │ │ │ ├── util.ts
│ │ │ │ └── withCacheUpdater.ts
│ │ │ └── index.ts
│ │ └── utils
│ │ │ ├── directiveName.ts
│ │ │ ├── getNodes.ts
│ │ │ ├── graphqlAST.ts
│ │ │ ├── index.ts
│ │ │ └── testing
│ │ │ ├── backendNodeIdGenerator.ts
│ │ │ └── mockedWrapperComponent.tsx
│ └── tsconfig.build.json
└── graphql-codegen-preset
│ ├── README.md
│ ├── package.json
│ ├── src
│ ├── __tests__
│ │ ├── __snapshots__
│ │ │ └── preset.test.ts.snap
│ │ ├── fixtures
│ │ │ └── exampleFile.ts
│ │ └── preset.test.ts
│ ├── index.ts
│ ├── plugins
│ │ └── cache-updater-support
│ │ │ ├── __tests__
│ │ │ └── index.test.ts
│ │ │ ├── config.ts
│ │ │ ├── index.ts
│ │ │ └── visitor.ts
│ ├── preset.ts
│ ├── presetConfig.ts
│ ├── schemaTransforms
│ │ ├── __tests__
│ │ │ └── addConnectionId.test.ts
│ │ ├── addClientDirective.ts
│ │ └── addConnectionId.ts
│ ├── transforms
│ │ ├── __tests__
│ │ │ ├── addFieldsForAddingNode.test.ts
│ │ │ ├── addPaginationFields.test.ts
│ │ │ ├── fixVariableNotDefinedInRoot.test.ts
│ │ │ ├── generateRefetchQuery.test.ts
│ │ │ ├── passArgumentValueToFragment.test.ts
│ │ │ └── removeCustomDirective.test.ts
│ │ ├── addFieldsForAddingNode.ts
│ │ ├── addPaginationFields.ts
│ │ ├── fixVariableNotDefinedInRoot.ts
│ │ ├── generateRefetchQuery.ts
│ │ ├── passArgumentValueToFragment.ts
│ │ ├── removeCustomDirective.ts
│ │ └── util.ts
│ └── utils
│ │ ├── graphqlAST.ts
│ │ ├── graphqlSchema.ts
│ │ ├── nonNullable.ts
│ │ └── testing
│ │ ├── example.graphql
│ │ └── utils.ts
│ └── tsconfig.build.json
├── setupJest.js
├── tsconfig.build.json
├── tsconfig.json
└── yarn.lock
/.eslintignore:
--------------------------------------------------------------------------------
1 | */backend/*
2 | **/*.js
3 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: [
3 | 'plugin:@typescript-eslint/eslint-recommended',
4 | 'plugin:@typescript-eslint/recommended',
5 | 'plugin:@typescript-eslint/recommended-requiring-type-checking',
6 | 'plugin:prettier/recommended',
7 | 'prettier',
8 | ],
9 | env: {
10 | es6: true,
11 | node: true,
12 | },
13 | parser: '@typescript-eslint/parser',
14 | plugins: ['@typescript-eslint/eslint-plugin', 'prettier'],
15 | parserOptions: {
16 | project: './tsconfig.json',
17 | tsconfigRootDir: __dirname,
18 | },
19 | rules: {
20 | '@typescript-eslint/no-unused-vars': 'error',
21 | '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
22 | },
23 | };
24 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "npm"
4 | directory: "/"
5 | schedule:
6 | interval: "daily"
7 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | paths-ignore:
8 | - '**.md*'
9 | pull_request:
10 | paths-ignore:
11 | - '**.md*'
12 | schedule:
13 | # Check peerDependency packages changes do not break this library
14 | - cron: '0 0 * * 1'
15 |
16 | jobs:
17 | ci:
18 | runs-on: ubuntu-latest
19 | strategy:
20 | matrix:
21 | node-version:
22 | - 14.x
23 | - 16.x
24 | command:
25 | - 'lint'
26 | - 'test'
27 | steps:
28 | - name: Checkout repo
29 | uses: actions/checkout@v2
30 |
31 | - name: Use Node.js ${{ matrix.node-version }}
32 | uses: actions/setup-node@v2
33 | with:
34 | node-version: ${{ matrix.node-version }}
35 |
36 | - name: Install dependencies
37 | run: yarn && yarn run lerna bootstrap
38 |
39 | - name: Build packages
40 | run: yarn build
41 |
42 | - name: Run ${{ matrix.command }}
43 | run: yarn ${{ matrix.command }}
44 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ "main" ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ "main" ]
20 | schedule:
21 | - cron: '44 2 * * 1'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ubuntu-latest
27 | permissions:
28 | actions: read
29 | contents: read
30 | security-events: write
31 |
32 | strategy:
33 | fail-fast: false
34 | matrix:
35 | language: [ 'javascript' ]
36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
38 |
39 | steps:
40 | - name: Checkout repository
41 | uses: actions/checkout@v3
42 |
43 | # Initializes the CodeQL tools for scanning.
44 | - name: Initialize CodeQL
45 | uses: github/codeql-action/init@v2
46 | with:
47 | languages: ${{ matrix.language }}
48 | # If you wish to specify custom queries, you can do so here or in a config file.
49 | # By default, queries listed here will override any specified in a config file.
50 | # Prefix the list here with "+" to use these queries and those in the config file.
51 |
52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
53 | # queries: security-extended,security-and-quality
54 |
55 |
56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
57 | # If this step fails, then you should remove it and run the build manually (see below)
58 | - name: Autobuild
59 | uses: github/codeql-action/autobuild@v2
60 |
61 | # ℹ️ Command-line programs to run using the OS shell.
62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
63 |
64 | # If the Autobuild fails above, remove it and uncomment the following three lines.
65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
66 |
67 | # - run: |
68 | # echo "Run, Build Application using script"
69 | # ./location_of_script_within_repo/buildscript.sh
70 |
71 | - name: Perform CodeQL Analysis
72 | uses: github/codeql-action/analyze@v2
73 | with:
74 | category: "/language:${{matrix.language}}"
75 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | */node_modules/
43 | jspm_packages/
44 |
45 | # Snowpack dependency directory (https://snowpack.dev/)
46 | web_modules/
47 |
48 | # TypeScript cache
49 | *.tsbuildinfo
50 |
51 | # Optional npm cache directory
52 | .npm
53 |
54 | # Optional eslint cache
55 | .eslintcache
56 |
57 | # Microbundle cache
58 | .rpt2_cache/
59 | .rts2_cache_cjs/
60 | .rts2_cache_es/
61 | .rts2_cache_umd/
62 |
63 | # Optional REPL history
64 | .node_repl_history
65 |
66 | # Output of 'npm pack'
67 | *.tgz
68 |
69 | # Yarn Integrity file
70 | .yarn-integrity
71 |
72 | # dotenv environment variables file
73 | .env
74 | .env.test
75 |
76 | # parcel-bundler cache (https://parceljs.org/)
77 | .cache
78 | .parcel-cache
79 |
80 | # TernJS port file
81 | .tern-port
82 |
83 | # Stores VSCode versions used for testing VSCode extensions
84 | .vscode-test
85 |
86 | # yarn v2
87 | .yarn/cache
88 | .yarn/unplugged
89 | .yarn/build-state.yml
90 | .yarn/install-state.gz
91 | .pnp.*
92 |
93 | # build output
94 | dist/
95 |
96 | # Others
97 | .DS_Store
98 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 120,
3 | "singleQuote": true,
4 | "trailingComma": "all"
5 | }
6 |
--------------------------------------------------------------------------------
/MIT-LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 | Copyright © 2021 Kyohei Nakamori
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5 |
6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7 |
8 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | Nau
4 |
5 |
6 | Nau is a tool that makes [Apollo Client](https://github.com/apollographql/apollo-client) more productive for users using [Relay GraphQL Server Specification](https://relay.dev/docs/guides/graphql-server-specification) compliant backends.
7 |
8 | - Make cache operations very easy
9 | - Provide custom directives to write declaratively and improve productivity
10 | - Support co-location of components and fragments by allowing a query to split into the fragments
11 | - Support subscriptions
12 |
13 | The tool aims to help frontend developers build frontend applications more quickly, with fewer bugs, and more efficiently.
14 |
15 | Nau is a perfect fit with React, Apollo Client, GraphQL Code Generator, and TypeScript.
16 |
17 |
18 | ## Installation
19 | ```
20 | yarn add @kazekyo/nau
21 | yarn add --dev @kazekyo/nau-graphql-codegen-preset
22 | ```
23 |
24 | > ⚠️ The packages are currently under development. All version updates may have breaking changes.
25 |
26 | ## Documentation
27 | https://www.naugraphql.com
28 |
29 |
30 | ## Example
31 | 
32 |
33 | See the [example code](https://github.com/kazekyo/nau/tree/main/examples/app).
34 |
35 |
36 | You can write pagination and cache updates more easily using some GraphQL directives. :rocket:
37 | ```src/List.tsx
38 | import { gql, useMutation, useSubscription } from '@apollo/client';
39 | import { usePagination } from '@kazekyo/nau';
40 | import * as React from 'react';
41 | import {
42 | AddItemMutationDocument,
43 | ItemAddedSubscriptionDocument,
44 | ItemRemovedSubscriptionDocument,
45 | List_PaginationQueryDocument,
46 | List_UserFragment
47 | } from './generated/graphql';
48 | import ListItem from './ListItem';
49 |
50 | gql`
51 | fragment List_user on User
52 | @argumentDefinitions(count: { type: "Int", defaultValue: 2 }, cursor: { type: "String" })
53 | @refetchable(queryName: "List_PaginationQuery") {
54 | items(first: $count, after: $cursor) @pagination {
55 | edges {
56 | node {
57 | ...ListItem_item
58 | }
59 | }
60 | }
61 | ...ListItem_user
62 | }
63 |
64 | mutation AddItemMutation($input: AddItemInput!, $connections: [String!]!) {
65 | addItem(input: $input) {
66 | item @prependNode(connections: $connections) {
67 | ...ListItem_item
68 | }
69 | }
70 | }
71 |
72 | subscription ItemAddedSubscription($connections: [String!]!) {
73 | itemAdded {
74 | item @prependNode(connections: $connections) {
75 | ...ListItem_item
76 | }
77 | }
78 | }
79 |
80 | subscription ItemRemovedSubscription {
81 | itemRemoved {
82 | id @deleteRecord(typename: "Item")
83 | }
84 | }
85 | `;
86 |
87 | const List: React.FC<{ user: List_UserFragment }> = ({ user }) => {
88 | useSubscription(ItemAddedSubscriptionDocument, {
89 | variables: {
90 | connections: [user.items._connectionId],
91 | },
92 | });
93 | useSubscription(ItemRemovedSubscriptionDocument);
94 | const [addItem] = useMutation(AddItemMutationDocument);
95 | const { nodes, hasNext, loadNext, isLoading } = usePagination(List_PaginationQueryDocument, {
96 | id: user.id,
97 | connection: user.items,
98 | });
99 |
100 | return (
101 | <>
102 |
103 |
115 |
116 |
117 | {nodes.map((node) => {
118 | return (
119 |
120 |
121 |
122 | );
123 | })}
124 |
125 | {hasNext && (
126 |
129 | )}
130 | >
131 | );
132 | };
133 |
134 | export default List;
135 |
136 | ```
137 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function (api) {
2 | api.cache(true);
3 | return {
4 | presets: ['@babel/env', '@babel/typescript'],
5 | plugins: ['@babel/proposal-class-properties', '@babel/proposal-object-rest-spread'],
6 | };
7 | };
8 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | # Dependencies
2 | /node_modules
3 |
4 | # Production
5 | /build
6 |
7 | # Generated files
8 | .docusaurus
9 | .cache-loader
10 |
11 | # Misc
12 | .DS_Store
13 | .env.local
14 | .env.development.local
15 | .env.test.local
16 | .env.production.local
17 |
18 | npm-debug.log*
19 | yarn-debug.log*
20 | yarn-error.log*
21 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # Website
2 |
3 | This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator.
4 |
5 | ### Installation
6 |
7 | ```
8 | $ yarn
9 | ```
10 |
11 | ### Local Development
12 |
13 | ```
14 | $ yarn start
15 | ```
16 |
17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
18 |
19 | ### Build
20 |
21 | ```
22 | $ yarn build
23 | ```
24 |
25 | This command generates static content into the `build` directory and can be served using any static contents hosting service.
26 |
27 | ### Deployment
28 |
29 | Using SSH:
30 |
31 | ```
32 | $ USE_SSH=true yarn deploy
33 | ```
34 |
35 | Not using SSH:
36 |
37 | ```
38 | $ GIT_USER= yarn deploy
39 | ```
40 |
41 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
42 |
--------------------------------------------------------------------------------
/docs/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
3 | };
4 |
--------------------------------------------------------------------------------
/docs/docs/api-reference/_category_.json:
--------------------------------------------------------------------------------
1 | {
2 | "label": "API Reference",
3 | "position": 4
4 | }
5 |
--------------------------------------------------------------------------------
/docs/docs/api-reference/createCacheUpdaterLink.md:
--------------------------------------------------------------------------------
1 | # createCacheUpdaterLink
2 |
3 | `createCacheUpdaterLink` creates an Apollo Link for Nau to manipulate the cache.
4 |
5 | ```tsx
6 | const httpLink = new HttpLink({ uri: 'http://localhost:4000/graphql' });
7 | // highlight-next-line
8 | const cacheUpdaterLink = createCacheUpdaterLink();
9 |
10 | const client = new ApolloClient({
11 | cache: new InMemoryCache({
12 | addTypename: true,
13 | possibleTypes: introspectionResult.possibleTypes,
14 | typePolicies: withCacheUpdater({}),
15 | }),
16 | // highlight-next-line
17 | link: from([cacheUpdaterLink, httpLink]),
18 | });
19 | ```
20 |
--------------------------------------------------------------------------------
/docs/docs/api-reference/usePagination.md:
--------------------------------------------------------------------------------
1 | # usePagination
2 |
3 | The `usePagination` is Hook for easy handling of pagination.
4 |
5 | ```tsx
6 | const { nodes, hasNext, loadNext, isLoading } = usePagination(List_PaginationQueryDocument, {
7 | id: user.id,
8 | connection: user.items,
9 | });
10 | ```
11 |
12 | ## Arguments
13 | - `document`: A GraphQL query document.
14 | - `options`:
15 | - `id`: A id of an object having Connection.
16 | - `connection`: A connection field.
17 | - `variables`: Variables required for query.
18 |
19 |
20 | ## Result
21 | - `nodes`: Array of nodes retrieved from a connection.
22 | - `loadNext`: A function used to fetch items on the next page in a connection.
23 | - `loadPrevious`: A function used to fetch items on the previous page in a connection.
24 | - `hasNext`: A value indicating whether item exists on the next page.
25 | - `hasPrevious`: A value indicating whether item exists on the previous page.
26 | - `isLoading`: value indicating whether the next/previous page is currently loading.
27 |
--------------------------------------------------------------------------------
/docs/docs/api-reference/withCacheUpdater.md:
--------------------------------------------------------------------------------
1 | # withCacheUpdater
2 |
3 | The `withCacheUpdater` allows you to use some tools for updating the cache, such as `@appendNode`.
4 |
5 | ```tsx
6 | new InMemoryCache({
7 | addTypename: true,
8 | possibleTypes: introspectionResult.possibleTypes,
9 | // highlight-start
10 | typePolicies: withCacheUpdater({
11 | User: {
12 | fields: {
13 | items: relayStylePagination(),
14 | },
15 | },
16 | }),
17 | // highlight-end
18 | }),
19 | ```
20 |
21 |
22 | ## Arguments
23 | - `typePolicies`: A `TypePolicy` object. Set `relayStylePagination()` for fields to which `@pagination` is attached.
24 |
--------------------------------------------------------------------------------
/docs/docs/directives/_category_.json:
--------------------------------------------------------------------------------
1 | {
2 | "label": "Directives",
3 | "position": 5
4 | }
5 |
--------------------------------------------------------------------------------
/docs/docs/directives/appendNode-prependNode.md:
--------------------------------------------------------------------------------
1 | # @appendNode / @prependNode
2 |
3 | `@appendNode` and `@prependNode` will automatically add data from a mutation result to a connection on the cache.
4 |
5 | The `@appendNode` will add an element to the bottom of a list, and the `@prependNode` will add it to the top.
6 | ### `@appendNode`
7 | ```graphql
8 | mutation AddBar($input: AddBarInput!, $connections: [String!]!) {
9 | addBar(input: $input) {
10 | bar @appendNode(connections: $connections) {
11 | name
12 | }
13 | }
14 | }
15 | ```
16 |
17 | ### `@prependNode`
18 | ```graphql
19 | mutation AddBar($input: AddBarInput!, $connections: [String!]!) {
20 | addBar(input: $input) {
21 | bar @appendNode(connections: $connections) {
22 | name
23 | }
24 | }
25 | }
26 | ```
27 |
--------------------------------------------------------------------------------
/docs/docs/directives/argumentDefinitions.md:
--------------------------------------------------------------------------------
1 | # @argumentDefinitions
2 |
3 | The `@argumentDefinitions` allows you to define fragment-specific variables. You can also set default values for the variables.
4 |
5 | ```graphql
6 | fragment foo on Foo @argumentDefinitions(arg1: { type: "String!" }, arg2: { type: "Int!", defaultValue: 0 }) {
7 | foo(a: $arg1, b: $arg2) {
8 | id
9 | name
10 | }
11 | }
12 | `;
13 | ```
14 |
--------------------------------------------------------------------------------
/docs/docs/directives/arguments.md:
--------------------------------------------------------------------------------
1 | # @arguments
2 |
3 | Use the `@arguments` directive to pass variables to fragments.
4 |
5 | ```graphql
6 | query MyQuery($myName: String!) {
7 | foo {
8 | ...bar1 @arguments(name: $myName)
9 | ...bar2 @arguments(name: "your name")
10 | }
11 | }
12 |
13 | fragment bar1 on Bar @argumentDefinitions(name: { type: "String!" }) {
14 | field1(name: $name) {
15 | id
16 | }
17 | }
18 |
19 | fragment bar2 on Bar @argumentDefinitions(name: { type: "String!" }) {
20 | field2(name: $name) {
21 | id
22 | }
23 | }
24 | ```
25 |
--------------------------------------------------------------------------------
/docs/docs/directives/deleteRecord.md:
--------------------------------------------------------------------------------
1 | # @deleteRecord
2 |
3 | Use the `@deleteRecord` directive to delete a item in the cache based on a mutation result.
4 |
5 | You can put this directive on only a `id` field. The `typename` argument is the type of data to be deleted.
6 |
7 | ```graphql
8 | mutation RemoveItemMutation($input: RemoveItemInput!) {
9 | removeItem(input: $input) {
10 | removedItem {
11 | id @deleteRecord(typename: "Item")
12 | }
13 | }
14 | }
15 | ```
16 |
17 | Using this directive will delete the data from the cache. So if you have the item in connections A and B, `@deleteRecord` will remove the data from both connections.
18 |
--------------------------------------------------------------------------------
/docs/docs/directives/pagination.md:
--------------------------------------------------------------------------------
1 | # @pagination
2 |
3 | By attaching the `pagination` directive, you indicate to Nau that this is a connection for pagination.
4 |
5 | Nau will determine fields you have not written in your query (e.g. the `pageInfo` field) and add them if necessary. It also generates some TypeScript code for caching.
6 |
7 | For example, let's look at a following query:
8 | ```graphql
9 | query TestQuery($cursor: String) {
10 | viewer {
11 | // highlight-next-line
12 | items(first: 1, after: $cursor) @pagination {
13 | edges {
14 | node {
15 | name
16 | }
17 | }
18 | }
19 | }
20 | }
21 | ```
22 | Nau will rewrite the query as follows:
23 | ```graphql
24 | query TestQuery($cursor: String) {
25 | viewer {
26 | items(first: 1, after: $cursor) @pagination {
27 | edges {
28 | node {
29 | name
30 | // highlight-start
31 | id
32 | __typename
33 | // highlight-end
34 | }
35 | // highlight-next-line
36 | cursor
37 | }
38 | // highlight-start
39 | pageInfo {
40 | hasNextPage
41 | endCursor
42 | hasPreviousPage
43 | startCursor
44 | }
45 | _connectionId @client
46 | // highlight-end
47 | }
48 | // highlight-start
49 | id
50 | __typename
51 | // highlight-end
52 | }
53 | }
54 | ```
55 |
--------------------------------------------------------------------------------
/docs/docs/directives/refetchable.md:
--------------------------------------------------------------------------------
1 | # @refetchable
2 |
3 | Using the `@refetchable` directive, Nau can automatically create a query containing only a fragment. You need to set `queryName` to a name of the query to refetch.
4 |
5 | For example, let's look at a following fragment:
6 | ```graphql
7 | fragment List_user on User
8 | @refetchable(queryName: "List_PaginationQuery") {
9 | id
10 | name
11 | }
12 | ```
13 |
14 | Nau will generate a query as follows:
15 | ```graphql
16 | query List_PaginationQuery($id: ID!) {
17 | node(id: $id) {
18 | id
19 | __typename
20 | ...List_user
21 | }
22 | }
23 |
24 | fragment List_user on User
25 | @refetchable(queryName: "List_PaginationQuery") {
26 | id
27 | name
28 | }
29 | ```
30 |
--------------------------------------------------------------------------------
/docs/docs/getting-started/_category_.json:
--------------------------------------------------------------------------------
1 | {
2 | "label": "Getting Started",
3 | "position": 2
4 | }
5 |
--------------------------------------------------------------------------------
/docs/docs/getting-started/basic-usage/_category_.json:
--------------------------------------------------------------------------------
1 | {
2 | "label": "Basic Usage",
3 | "position": 3
4 | }
5 |
--------------------------------------------------------------------------------
/docs/docs/getting-started/basic-usage/pagination.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 1
3 | ---
4 |
5 | # Pagination
6 |
7 | By using some directives and hooks, you can paginate with less code while making efficient API requests.
8 |
9 | Here is an example component. Let's implement it using this component.
10 |
11 | ```tsx title="src/List.tsx"
12 | import { List_PaginationQueryDocument, List_UserFragment } from './generated/graphql';
13 |
14 | gql` /* GraphQL */
15 | fragment List_user on User {
16 | id
17 | items(first: $count, after: $cursor) {
18 | edges {
19 | node {
20 | id
21 | name
22 | }
23 | cursor
24 | }
25 | pageInfo {
26 | hasNextPage
27 | endCursor
28 | }
29 | }
30 | }
31 | `;
32 |
33 | const List: React.FC<{ user: List_UserFragment; }> = ({ user }) => {
34 | // Retrieve nodes from user
35 | // ...
36 | return (
37 | <>
38 |
39 | {nodes.map((node, i) => {
40 | return (
41 |
{node.name}
42 | );
43 | })}
44 |
45 | {/* ... */}
46 | >
47 | );
48 | };
49 | ```
50 |
51 | First, add the following directives: `@argumentDefinitions`, `@refetchable`, and `@pagination`. You may also remove some fields only for pagination, such as `pageInfo`, from the fragment, as Nau will automatically complete them for you.
52 |
53 | ```graphql
54 | fragment List_user on User
55 | // highlight-start
56 | @argumentDefinitions(count: { type: "Int", defaultValue: 2 }, cursor: { type: "String" })
57 | @refetchable(queryName: "List_PaginationQuery") {
58 | // highlight-end
59 | id
60 | // highlight-next-line
61 | items(first: $count, after: $cursor) @pagination {
62 | edges {
63 | node {
64 | name
65 | }
66 | }
67 | }
68 | }
69 | ```
70 |
71 | Run `graphql-codegen`.
72 | ```bash
73 | yarn graphql-codegen
74 | ```
75 |
76 | You can easily implement pagination using the generated `List_PaginationQueryDocument` with `usePagination`.
77 |
78 | ```tsx title="src/List.tsx"
79 | // highlight-start
80 | import { usePagination } from '@kazekyo/nau';
81 | import { List_PaginationQueryDocument, List_UserFragment } from './generated/graphql';
82 | // highlight-end
83 |
84 | gql` /* GraphQL */
85 | fragment List_user on User
86 | @argumentDefinitions(count: { type: "Int", defaultValue: 2 }, cursor: { type: "String" })
87 | @refetchable(queryName: "List_PaginationQuery") {
88 | id
89 | items(first: $count, after: $cursor) @pagination {
90 | edges {
91 | node {
92 | name
93 | }
94 | }
95 | }
96 | }
97 | `;
98 |
99 | const List: React.FC<{ user: List_UserFragment; }> = ({ user }) => {
100 | // highlight-start
101 | const { nodes, hasNext, loadNext, isLoading } = usePagination(List_PaginationQueryDocument, {
102 | id: user.id,
103 | connection: user.items,
104 | });
105 | // highlight-end
106 |
107 | return (
108 | <>
109 |
110 | {nodes.map((node, i) => {
111 | return (
112 |
{node.name}
113 | );
114 | })}
115 |
116 | // highlight-start
117 | {hasNext && (
118 |
125 | )}
126 | // highlight-end
127 | >
128 | );
129 | };
130 | ```
131 |
132 | Note that whether you are using Nau or not, remember that you must have pagination configured in your cache.
133 |
134 | ```tsx title="src/index.tsx"
135 | // highlight-next-line
136 | import { relayStylePagination } from '@apollo/client/utilities';
137 |
138 | const client = new ApolloClient({
139 | cache: new InMemoryCache({
140 | addTypename: true,
141 | possibleTypes: introspectionResult.possibleTypes,
142 | typePolicies: withCacheUpdater({
143 | // highlight-start
144 | User: {
145 | fields: {
146 | items: relayStylePagination(),
147 | },
148 | },
149 | // highlight-end
150 | }),
151 | }),
152 | link: splitLink,
153 | });
154 | ```
155 |
--------------------------------------------------------------------------------
/docs/docs/getting-started/basic-usage/updating-data.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 2
3 | ---
4 |
5 | # Updating Data
6 |
7 | Nau can automatically add objects to a list (connection) on cache based on mutation results. Using directives such as @appendNode will automatically add a element to a connection on the cache based on mutation results.
8 |
9 | Let's look at an example mutation.
10 | ```graphql
11 | mutation AddItemMutation($input: AddItemInput!) {
12 | addItem(input: $input) {
13 | item {
14 | name
15 | }
16 | }
17 | }
18 | ```
19 |
20 | Attach @prependNode to the `item` field. Using `@prependNode` you can add the element to the top of the list.
21 |
22 | ```graphql
23 | // highlight-next-line
24 | mutation AddItemMutation($input: AddItemInput!, $connections: [String!]!) {
25 | // highlight-next-line
26 | addItem(input: $input) @prependNode(connections: $connections) {
27 | item {
28 | name
29 | }
30 | }
31 | }
32 | ```
33 |
34 | Run `graphql-codegen`.
35 | ```bash
36 | yarn graphql-codegen
37 | ```
38 |
39 | Let's try to use this mutation.
40 |
41 | ```tsx title="src/List.tsx"
42 | import {
43 | // highlight-next-line
44 | AddItemMutationDocument,
45 | List_PaginationQueryDocument,
46 | List_UserFragment,
47 | } from './generated/graphql';
48 |
49 | gql` /* GraphQL */
50 | fragment List_user on User
51 | @argumentDefinitions(count: { type: "Int", defaultValue: 2 }, cursor: { type: "String" })
52 | @refetchable(queryName: "List_PaginationQuery") {
53 | id
54 | items(first: $count, after: $cursor) @pagination {
55 | edges {
56 | node {
57 | name
58 | }
59 | }
60 | }
61 | }
62 |
63 | // highlight-start
64 | mutation AddItemMutation($input: AddItemInput!, $connections: [String!]!) {
65 | addItem(input: $input) {
66 | item @prependNode(connections: $connections) {
67 | name
68 | }
69 | }
70 | }
71 | // highlight-end
72 | `;
73 |
74 | const List: React.FC<{ user: List_UserFragment }> = ({ user }) => {
75 | // ...
76 |
77 | // highlight-next-line
78 | const [addItem] = useMutation(AddItemMutationDocument);
79 |
80 | return (
81 | <>
82 | {/* ... */}
83 |
97 | {/* ... */}
98 | >
99 | );
100 | };
101 | ```
102 |
103 | Now you can add the element to the top of the list based on the mutation result.
104 |
105 | Using [`@appendNode`](/docs/directives/appendNode-prependNode), you can do the opposite, adding the element to the bottom of the list. Other directives such as [`@deleteRecord`](/docs/directives/deleteRecord) delete data based on mutation results.
106 |
--------------------------------------------------------------------------------
/docs/docs/getting-started/configuration.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 2
3 | ---
4 |
5 | # Configuration
6 |
7 | You should be familiar with [React](https://reactjs.org/), [Apollo Client](https://www.apollographql.com/docs/react/), and [GraphQL Code Generator](https://www.graphql-code-generator.com/) as a prerequisite. If you have never used them, you will need to read their documentation first.
8 |
9 | # Configration for Nau
10 | First, add `@kazekyo/nau-graphql-codegen-preset` to your `codegen.yml`.
11 |
12 | ```yaml title="./codegen.yml"
13 | schema: http://localhost:4000/graphql
14 | # Do not use the root document. For example, do not write `documents: src/**/*.graphql` this line.
15 | generates:
16 | src/generated/graphql.ts:
17 | // highlight-start
18 | preset: '@kazekyo/nau-graphql-codegen-preset'
19 | presetConfig:
20 | generateTypeScriptCode: true
21 | documents:
22 | - src/**/*.tsx
23 | - src/**/*.graphql
24 | // highlight-end
25 | plugins:
26 | - typescript
27 | - typescript-operations
28 | - typed-document-node
29 | src/generated/introspection-result.json:
30 | plugins:
31 | - fragment-matcher
32 | ```
33 |
34 | As above, you will need to add this preset to processes that read `documents` and use it. In most cases, that means you add the preset to processes of generating files that output TypeScript code.
35 |
36 | :::info
37 | We recommend not using root `documents`. It means that you should **not** write the following:
38 |
39 | ```yaml title="./codegen.yml"
40 | schema: http://localhost:4000/graphql
41 | // highlight-start
42 | documents:
43 | - src/**/*.tsx
44 | - src/**/*.graphql
45 | // highlight-end
46 | generates:
47 | # ...
48 | ```
49 |
50 | Using root `documents` requires using the preset in all file generation process to parse directives provided by Nau.
51 |
52 | :::
53 |
54 |
55 | Run GraphQL Code Generator.
56 | ```bash
57 | yarn graphql-codegen
58 | ```
59 |
60 | And Configure your cache settings for Apollo Client. Import and use `withCacheUpdater` from the generated file, and also use some functions of `@kazekyo/nau`.
61 | ```tsx title="src/index.tsx"
62 | import { ApolloClient, ApolloProvider, from, HttpLink, InMemoryCache, split } from '@apollo/client';
63 | import { WebSocketLink } from '@apollo/client/link/ws';
64 | // highlight-next-line
65 | import { createCacheUpdaterLink, isSubscriptionOperation } from '@kazekyo/nau';
66 | // highlight-next-line
67 | import { withCacheUpdater } from './generated/graphql';
68 | // highlight-next-line
69 | import introspectionResult from './generated/introspection-result.json';
70 |
71 | const wsLink = new WebSocketLink({
72 | uri: 'ws://localhost:4000/subscriptions',
73 | options: {
74 | reconnect: true,
75 | },
76 | });
77 | const httpLink = new HttpLink({ uri: 'http://localhost:4000/graphql' });
78 | // highlight-next-line
79 | const cacheUpdaterLink = createCacheUpdaterLink();
80 |
81 | const splitLink = split(
82 | ({ query }) => isSubscriptionOperation(query),
83 | // highlight-start
84 | from([cacheUpdaterLink, wsLink]),
85 | from([cacheUpdaterLink, httpLink]),
86 | // highlight-end
87 | );
88 |
89 | const client = new ApolloClient({
90 | cache: new InMemoryCache({
91 | // highlight-start
92 | addTypename: true, // Do not set false
93 | possibleTypes: introspectionResult.possibleTypes,
94 | typePolicies: withCacheUpdater({}),
95 | // highlight-end
96 | }),
97 | link: splitLink,
98 | });
99 | ```
100 |
101 | That's it for the configuration! If you want to set up your IDE, see also [here](/docs/guides/ide-settings).
102 |
--------------------------------------------------------------------------------
/docs/docs/getting-started/installation.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 1
3 | title: Installation
4 | ---
5 |
6 | ```mdx-code-block
7 | import Tabs from '@theme/Tabs';
8 | import TabItem from '@theme/TabItem';
9 | ```
10 |
11 | You can install packages.
12 |
13 | ````mdx-code-block
14 |
15 |
16 |
17 | ```bash
18 | npm install @kazekyo/nau
19 | npm install --save-dev @kazekyo/nau-graphql-codegen-preset
20 | ```
21 |
22 |
23 |
24 |
25 | ```bash
26 | yarn add @kazekyo/nau
27 | yarn add --dev @kazekyo/nau-graphql-codegen-preset
28 | ```
29 |
30 |
31 |
32 | ````
33 |
34 |
35 | You need **Apollo Client** and **GraphQL Code Generator** to use Nau. And GraphQL Code Generator plugins such as `typescript`, `typescript-operations`, `typed-document-node`, `fragment-matcher`, and `schema-ast` are strongly recommended.
36 |
37 | If you have not installed them, do so.
38 |
39 | ````mdx-code-block
40 |
41 |
42 |
43 | ```bash
44 | npm install @apollo/client graphql
45 | npm install --save-dev @graphql-codegen/cli @graphql-codegen/fragment-matcher @graphql-codegen/schema-ast @graphql-codegen/typed-document-node @graphql-codegen/typescript @graphql-codegen/typescript-operations
46 | ```
47 |
48 |
49 |
50 |
51 | ```bash
52 | yarn add @apollo/client graphql
53 | yarn add --dev @graphql-codegen/cli @graphql-codegen/fragment-matcher @graphql-codegen/schema-ast @graphql-codegen/typed-document-node @graphql-codegen/typescript @graphql-codegen/typescript-operations
54 | ```
55 |
56 |
57 |
58 | ````
59 |
--------------------------------------------------------------------------------
/docs/docs/graphql-code-generator/_category_.json:
--------------------------------------------------------------------------------
1 | {
2 | "label": "Graphql Code Generator",
3 | "position": 6
4 | }
5 |
--------------------------------------------------------------------------------
/docs/docs/graphql-code-generator/graphql-codegen-preset.md:
--------------------------------------------------------------------------------
1 | # @kazekyo/nau-graphql-codegen-preset
2 |
3 | This preset extends your schema required to run Nau, modifies GraphQL documents you have written, and generates TypeScript Code.
4 | ```yaml
5 | schema: http://localhost:4000/graphql
6 | documents:
7 | - src/**/*.tsx
8 | generates:
9 | src/generated/graphql.ts:
10 | // highlight-start
11 | preset: '@kazekyo/nau-graphql-codegen-preset'
12 | presetConfig:
13 | generateTypeScriptCode: true
14 | // highlight-end
15 | plugins:
16 | - typescript
17 | - typescript-operations
18 | - typed-document-node
19 | src/generated/introspection-result.json:
20 | // highlight-next-line
21 | preset: '@kazekyo/nau-graphql-codegen-preset'
22 | plugins:
23 | - fragment-matcher
24 | ./schema.graphql:
25 | // highlight-next-line
26 | preset: '@kazekyo/nau-graphql-codegen-preset'
27 | plugins:
28 | - schema-ast
29 | ```
30 |
31 | ## Preset Config
32 |
33 | ### generateTypeScriptCode
34 | type: `boolean` default: `false`
35 |
36 | If set to `true`, It generates TypeScript code. For example, `withCacheUpdater` is generated by enabling this option. The default is `false`.
37 |
--------------------------------------------------------------------------------
/docs/docs/guides/_category_.json:
--------------------------------------------------------------------------------
1 | {
2 | "label": "Guides",
3 | "position": 3
4 | }
5 |
--------------------------------------------------------------------------------
/docs/docs/guides/ide-settings.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: IDE Settings
3 | ---
4 |
5 |
6 | ```mdx-code-block
7 | import Tabs from '@theme/Tabs';
8 | import TabItem from '@theme/TabItem';
9 | ```
10 |
11 |
12 | Nau supports [Apollo Config](https://www.apollographql.com/docs/devtools/apollo-config/) and [GraphQL Config](https://www.graphql-config.com/). We are testing works with VS Code extensions [Apollo GraphQL Extension](https://marketplace.visualstudio.com/items?itemName=apollographql.vscode-apollo) and [GraphQL Extension](https://marketplace.visualstudio.com/items?itemName=GraphQL.vscode-graphql).
13 |
14 | `@kazekyo/nau-config` provides directives knowledge to your IDE.
15 |
16 | # Installation
17 | ````mdx-code-block
18 |
19 |
20 |
21 | ```bash
22 | npm install --save-dev @kazekyo/nau-config
23 | ```
24 |
25 |
26 |
27 |
28 | ```bash
29 | yarn add --dev @kazekyo/nau-config
30 | ```
31 |
32 |
33 |
34 | ````
35 |
36 |
37 | # Apollo Config
38 | ```js title="apollo.config.js"
39 | // highlight-start
40 | const { apolloConfig } = require('@kazekyo/nau-config');
41 | const config = apolloConfig.generateConfig();
42 | // highlight-end
43 | module.exports = {
44 | client: {
45 | // highlight-next-line
46 | ...config.client,
47 | service: {
48 | name: 'example-app',
49 | url: 'http://localhost:4000/graphql',
50 | },
51 | },
52 | };
53 | ```
54 |
55 | See [here](https://www.apollographql.com/docs/devtools/editor-plugins/) for how to set up `apollo.config.js`.
56 |
57 | # GraphQL Config
58 | ```js title="graphql.config.js"
59 | // highlight-start
60 | const { graphqlConfig } = require('@kazekyo/nau-config');
61 | // highlight-end
62 | module.exports = {
63 | // highlight-next-line
64 | schema: ['http://localhost:4000/graphql', graphqlConfig.clientSchemaPath],
65 | documents: 'src/**/*.{graphql,js,ts,jsx,tsx}',
66 | };
67 | ```
68 |
69 | See [here](https://www.graphql-config.com/docs/user/user-introduction) for how to set up `graphql.config.js`.
70 |
--------------------------------------------------------------------------------
/docs/docs/guides/local-variables.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 3
3 | ---
4 |
5 | # Local variables in Fragment
6 |
7 | We cannot define local variables in Fragment, so a variable name must be unique across all fragments used. And if we want to set a default value for a variable used in a fragment, we have to put it as a query variable.
8 |
9 | You can use @arguments directive and @argumentDefinitions directive to solve this issue.
10 |
11 | These can be used to create local variables for fragments.
12 |
13 | ```graphql
14 | query MyQuery($myName: String!) {
15 | foo {
16 | ...bar1 @arguments(name: $myName)
17 | ...bar2 @arguments(name: "your name")
18 | }
19 | }
20 |
21 | fragment bar1 on Bar @argumentDefinitions(name: { type: "String!" }) {
22 | field1(name: $name) {
23 | id
24 | }
25 | }
26 |
27 | fragment bar2 on Bar @argumentDefinitions(name: { type: "String!" }) {
28 | field2(name: $name) {
29 | id
30 | }
31 | }
32 | ```
33 |
--------------------------------------------------------------------------------
/docs/docs/guides/pagination.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 1
3 | ---
4 |
5 | # Pagination
6 | When loading the next edges in a connection, you will get all data that has nothing to do with the connection if you reuse a query used to display the current page. But in most cases, it should be more efficient to request only the connection part.
7 |
8 | By using some directives and hooks, you can paginate with less code while making efficient API requests.
9 |
10 | First, create a fragment in the list component. Then, add some directives to that fragment.
11 | ```graphql
12 | fragment List_user on User
13 | // highlight-start
14 | @argumentDefinitions(count: { type: "Int", defaultValue: 2 }, cursor: { type: "String" })
15 | @refetchable(queryName: "List_PaginationQuery") {
16 | // highlight-end
17 | id
18 | // highlight-next-line
19 | items(first: $count, after: $cursor) @pagination {
20 | edges {
21 | node {
22 | name
23 | }
24 | }
25 | }
26 | }
27 | ```
28 | Attaching `@refetchable` to a fragment generates a query for refetching data using `node(id:)` in your backend API. The `@argumentDefinitions` are needed to define necessary variables when generating the refetch query. Attach `@pagination` to the connection field to indicate to Nau which part of the query is the connection to be paginated.
29 |
30 | Let's use the generated refetch query.
31 |
32 | ```tsx
33 | const { nodes, hasNext, loadNext, isLoading } = usePagination(List_PaginationQueryDocument, {
34 | id: user.id,
35 | connection: user.items,
36 | });
37 | ```
38 | Nau generates the refetch query with the name specified in the `queryName` argument of `@refetchable`. You can use it with `usePagination`.
39 |
40 | The `id` argument of `usePagination` will be the `id` specified in `node(id:)`. The `connection` argument is the field to which `@pagination` is attached.
41 |
42 |
43 | You can use return values of `usePagination` to display data and place a "read more" button.
44 |
45 | ```tsx title="src/List.tsx"
46 | import { usePagination } from '@kazekyo/nau';
47 | import { List_PaginationQueryDocument, List_UserFragment } from './generated/graphql';
48 |
49 | gql` /* GraphQL */
50 | fragment List_user on User
51 | @argumentDefinitions(count: { type: "Int", defaultValue: 2 }, cursor: { type: "String" })
52 | @refetchable(queryName: "List_PaginationQuery") {
53 | id
54 | items(first: $count, after: $cursor) @pagination {
55 | edges {
56 | node {
57 | name
58 | }
59 | }
60 | }
61 | }
62 | `;
63 |
64 | const List: React.FC<{ user: List_UserFragment; }> = ({ user }) => {
65 | const { nodes, hasNext, loadNext, isLoading } = usePagination(List_PaginationQueryDocument, {
66 | id: user.id,
67 | connection: user.items,
68 | });
69 |
70 | return (
71 | <>
72 |
73 | {nodes.map((node, i) => {
74 | return (
75 |
{node.name}
76 | );
77 | })}
78 |
79 | {hasNext && (
80 |
87 | )}
88 | >
89 | );
90 | };
91 | ```
92 |
--------------------------------------------------------------------------------
/docs/docs/guides/relay-spec.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 0
3 | ---
4 |
5 | # About Relay GraphQL Server Specification
6 | [Relay GraphQL Server Specification](https://relay.dev/docs/guides/graphql-server-specification) is a specification of GraphQL Server that makes GraphQL more effective.
7 |
8 | Large parts of the specification are recognized as best practices in GraphQL.
9 |
10 | - [Global Object Identification](https://graphql.org/learn/global-object-identification/)
11 | - [Pagination](https://graphql.org/learn/pagination/)
12 |
13 | Nau does not implement the specification on your server. Nau helps you develop frontend applications efficiently when you use a server that is compliant with the specification.
14 |
--------------------------------------------------------------------------------
/docs/docs/guides/updating-data.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 2
3 | ---
4 |
5 | # Updating Data
6 |
7 | If you want to add or remove an element from a list, you can easily implement it with some directives.
8 |
9 | ## Adding Data
10 | `@prependNode` adds a element to the top of a list, and `@appendNode` adds a element to the bottom of a list.
11 |
12 | ```graphql
13 | mutation AddItemMutation($input: AddItemInput!, $connections: [String!!) {
14 | addItem(input: $input) {
15 | item @prependNode(connections: $connections) {
16 | name
17 | }
18 | }
19 | }
20 | ```
21 |
22 | And you must have attached `@pagination` to the connection field for the following.
23 | ```graphql
24 | fragment List_user on User {
25 | id
26 | items(first: $count, after: $cursor) @pagination {
27 | edges {
28 | node {
29 | name
30 | }
31 | }
32 | }
33 | }
34 | ```
35 | If you attached `@pagination`, Nau will generate a `_connectionId`. You must specify this `_connectionId` to the `connections` argument of `@prependNode` in order for Nau to guess which connection to add the element to.
36 |
37 | Run `yarn graphql-codegen` to generate documents and use `_connectionId`.
38 |
39 | ```tsx title="src/List.tsx"
40 | const List: React.FC<{ user: List_UserFragment }> = ({ user }) => {
41 | // ...
42 | const [addItem] = useMutation(AddItemMutationDocument);
43 | // ...
44 | return (
45 | <>
46 |
58 | {/*...*/}
59 | >
60 | )
61 | }
62 | ```
63 |
64 | ## Removing data
65 | You can also delete data. `@deleteRecord` removes data from the cache based on the id contained in a mutation result. It will also automatically remove edges associated with the data from all connections to which `@pagionation` is attached.
66 |
67 | ```graphql
68 | mutation RemoveItemMutation($input: RemoveItemInput!) {
69 | removeItem(input: $input) {
70 | removedItem {
71 | id @deleteRecord(typename: "Item")
72 | }
73 | }
74 | }
75 | ```
76 | You have to attach the `@deleteRecord` directive only, and no other work is required. Nau will automatically manipulate the cache based on the mutation result.
77 |
78 |
79 | ## Subscription
80 | `@appendNode`/`@appendNode` and `@deleteRecord` also work the same with Subscription as they do with Mutation.
81 |
82 | ```graphql
83 | subscription ItemAddedSubscription($connections: [String!]!) {
84 | itemAdded {
85 | item @prependNode(connections: $connections) {
86 | ...ListItem_item
87 | }
88 | }
89 | }
90 | subscription ItemRemovedSubscription {
91 | itemRemoved {
92 | id @deleteRecord(typename: "Item")
93 | }
94 | }
95 | ```
96 |
--------------------------------------------------------------------------------
/docs/docs/introduction.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 1
3 | ---
4 |
5 | # Introduction
6 |
7 | Nau is a tool that makes [Apollo Client](https://github.com/apollographql/apollo-client) more productive for users using [Relay GraphQL Server Specification](https://relay.dev/docs/guides/graphql-server-specification) compliant backends.
8 |
9 | - Make cache operations very easy
10 | - Provide custom directives to write declaratively and improve productivity
11 | - Support co-location of components and fragments by allowing a query to split into the fragments
12 | - Support subscriptions
13 |
14 | The tool aims to help frontend developers build frontend applications more quickly, with fewer bugs, and more efficiently.
15 |
16 | Nau is inspired by [Relay](https://relay.dev/). However, our goal is not to create a full copy of Relay on Apollo Client. The goal is to make Apollo Client more powerful by integrating Relay GraphQL Server Specification.
17 |
--------------------------------------------------------------------------------
/docs/docusaurus.config.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | // Note: type annotations allow type checking and IDEs autocompletion
3 |
4 | const lightCodeTheme = require('prism-react-renderer/themes/github');
5 | const darkCodeTheme = require('prism-react-renderer/themes/dracula');
6 |
7 | /** @type {import('@docusaurus/types').Config} */
8 | const config = {
9 | title: 'Nau',
10 | tagline: 'Build your frontend apps safely and quickly with Apollo Client',
11 | url: 'https://www.naugraphql.com',
12 | baseUrl: '/',
13 | onBrokenLinks: 'throw',
14 | onBrokenMarkdownLinks: 'warn',
15 | favicon: 'img/favicon.ico',
16 | organizationName: 'kazekyo',
17 | projectName: 'nau',
18 |
19 | presets: [
20 | [
21 | 'classic',
22 | /** @type {import('@docusaurus/preset-classic').Options} */
23 | ({
24 | docs: {
25 | sidebarPath: require.resolve('./sidebars.js'),
26 | editUrl: 'https://github.com/kazekyo/nau/tree/main/docs/',
27 | },
28 | theme: {
29 | customCss: require.resolve('./src/css/custom.css'),
30 | },
31 | gtag: {
32 | trackingID: 'G-2XPHLCBQE5',
33 | },
34 | }),
35 | ],
36 | ],
37 |
38 | themeConfig:
39 | /** @type {import('@docusaurus/preset-classic').ThemeConfig} */
40 | ({
41 | navbar: {
42 | title: 'Nau',
43 | logo: {
44 | alt: 'Nau Logo',
45 | src: 'img/logo.svg',
46 | },
47 | items: [
48 | {
49 | type: 'doc',
50 | docId: 'introduction',
51 | position: 'right',
52 | label: 'Docs',
53 | },
54 | {
55 | href: 'https://github.com/kazekyo/nau',
56 | label: 'GitHub',
57 | position: 'right',
58 | },
59 | ],
60 | },
61 | footer: {
62 | style: 'dark',
63 | links: [
64 | {
65 | title: 'Docs',
66 | items: [
67 | {
68 | label: 'Docs',
69 | to: '/docs/introduction',
70 | },
71 | ],
72 | },
73 | {
74 | title: 'More',
75 | items: [
76 | {
77 | label: 'GitHub',
78 | href: 'https://github.com/kazekyo/nau',
79 | },
80 | ],
81 | },
82 | ],
83 | copyright: `Copyright © ${new Date().getFullYear()} Kyohei Nakamori. All rights reserved.`,
84 | },
85 | prism: {
86 | theme: lightCodeTheme,
87 | darkTheme: darkCodeTheme,
88 | },
89 | }),
90 | };
91 |
92 | module.exports = config;
93 |
--------------------------------------------------------------------------------
/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "docs",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "docusaurus": "docusaurus",
7 | "start": "docusaurus start",
8 | "build": "docusaurus build",
9 | "swizzle": "docusaurus swizzle",
10 | "deploy": "docusaurus deploy",
11 | "clear": "docusaurus clear",
12 | "serve": "docusaurus serve",
13 | "write-translations": "docusaurus write-translations",
14 | "write-heading-ids": "docusaurus write-heading-ids",
15 | "typecheck": "tsc"
16 | },
17 | "dependencies": {
18 | "@docusaurus/core": "^2.0.0",
19 | "@docusaurus/preset-classic": "^2.0.0",
20 | "@mdx-js/react": "^1.6.22",
21 | "clsx": "^1.1.1",
22 | "prism-react-renderer": "^1.3.1",
23 | "react": "^17.0.2",
24 | "react-dom": "^17.0.2"
25 | },
26 | "devDependencies": {
27 | "@docusaurus/module-type-aliases": "^2.0.0",
28 | "@tsconfig/docusaurus": "^1.0.5",
29 | "typescript": "^4.6.3"
30 | },
31 | "browserslist": {
32 | "production": [
33 | ">0.5%",
34 | "not dead",
35 | "not op_mini all"
36 | ],
37 | "development": [
38 | "last 1 chrome version",
39 | "last 1 firefox version",
40 | "last 1 safari version"
41 | ]
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/docs/sidebars.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Creating a sidebar enables you to:
3 | - create an ordered group of docs
4 | - render a sidebar for each doc of that group
5 | - provide next/previous navigation
6 |
7 | The sidebars can be generated from the filesystem, or explicitly defined here.
8 |
9 | Create as many sidebars as you want.
10 | */
11 |
12 | // @ts-check
13 |
14 | /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
15 | const sidebars = {
16 | // By default, Docusaurus generates a sidebar from the docs folder structure
17 | tutorialSidebar: [{type: 'autogenerated', dirName: '.'}],
18 |
19 | // But you can create a sidebar manually
20 | /*
21 | tutorialSidebar: [
22 | {
23 | type: 'category',
24 | label: 'Tutorial',
25 | items: ['hello'],
26 | },
27 | ],
28 | */
29 | };
30 |
31 | module.exports = sidebars;
32 |
--------------------------------------------------------------------------------
/docs/src/components/HomepageFeatures/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import clsx from 'clsx';
3 | import styles from './styles.module.css';
4 |
5 | type FeatureItem = {
6 | title: string;
7 | description: JSX.Element;
8 | };
9 |
10 | const FeatureList: FeatureItem[] = [
11 | {
12 | title: 'Safe and quick',
13 | description: (
14 | <>
15 | Nau simplifies cache operations in the Apollo Client and reduces bugs caused by human inattention. Moreover, you
16 | can write code safely in TypeScript.
17 | >
18 | ),
19 | },
20 | {
21 | title: 'Working on your behalf',
22 | description: (
23 | <>
24 | Nau will guess what you need from the little bit of code you write and automatically do the additional work
25 | required on your behalf.
26 | >
27 | ),
28 | },
29 | {
30 | title: 'Many useful directives',
31 | description: <>Using convenient client directives can help you be highly productive.>,
32 | },
33 | ];
34 |
35 | function Feature({ title, description }: FeatureItem) {
36 | return (
37 |
38 |
39 |
{title}
40 |
{description}
41 |
42 |
43 | );
44 | }
45 |
46 | export default function HomepageFeatures(): JSX.Element {
47 | return (
48 |
49 |
50 |
51 | {FeatureList.map((props, idx) => (
52 |
53 | ))}
54 |
55 |
56 |
57 | );
58 | }
59 |
--------------------------------------------------------------------------------
/docs/src/components/HomepageFeatures/styles.module.css:
--------------------------------------------------------------------------------
1 | .features {
2 | display: flex;
3 | align-items: center;
4 | padding: 2rem 0;
5 | margin-top: 2rem;
6 | width: 100%;
7 | }
8 |
--------------------------------------------------------------------------------
/docs/src/css/custom.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Any CSS included here will be global. The classic template
3 | * bundles Infima by default. Infima is a CSS framework designed to
4 | * work well for content-centric websites.
5 | */
6 |
7 | /* You can override the default Infima variables here. */
8 | :root {
9 | --ifm-color-primary: #38B2AC;
10 | /* --ifm-color-primary-dark: #29784c;
11 | --ifm-color-primary-darker: #277148;
12 | --ifm-color-primary-darkest: #205d3b;
13 | --ifm-color-primary-light: #33925d;
14 | --ifm-color-primary-lighter: #359962;
15 | --ifm-color-primary-lightest: #3cad6e; */
16 | --ifm-code-font-size: 95%;
17 | --docusaurus-highlighted-code-line-bg: #E8EDF2;
18 | }
19 |
20 | /* For readability concerns, you should choose a lighter palette in dark mode. */
21 | [data-theme='dark'] {
22 | --ifm-color-primary: #4FD1C5;
23 | --docusaurus-highlighted-code-line-bg: #363949;
24 | /* --ifm-color-primary-dark: #21af90;
25 | --ifm-color-primary-darker: #1fa588;
26 | --ifm-color-primary-darkest: #1a8870;
27 | --ifm-color-primary-light: #29d5b0;
28 | --ifm-color-primary-lighter: #32d8b4;
29 | --ifm-color-primary-lightest: #4fddbf; */
30 | }
31 |
--------------------------------------------------------------------------------
/docs/src/pages/index.module.css:
--------------------------------------------------------------------------------
1 | /**
2 | * CSS files with the .module.css suffix will be treated as CSS modules
3 | * and scoped locally.
4 | */
5 |
6 | .heroBanner {
7 | padding: 4rem 0;
8 | text-align: center;
9 | position: relative;
10 | overflow: hidden;
11 | }
12 |
13 | @media screen and (max-width: 996px) {
14 | .heroBanner {
15 | padding: 2rem;
16 | }
17 | }
18 |
19 | .buttons {
20 | display: flex;
21 | align-items: center;
22 | justify-content: center;
23 | }
24 |
25 | .getStartedButton {
26 | margin-right: 0.5rem;
27 | }
28 |
--------------------------------------------------------------------------------
/docs/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import clsx from 'clsx';
3 | import Layout from '@theme/Layout';
4 | import Link from '@docusaurus/Link';
5 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
6 | import styles from './index.module.css';
7 | import HomepageFeatures from '@site/src/components/HomepageFeatures';
8 |
9 | function HomepageHeader() {
10 | const { siteConfig } = useDocusaurusContext();
11 | return (
12 |
29 | );
30 | }
31 |
32 | export default function Home(): JSX.Element {
33 | return (
34 |
35 |
36 |
37 |
38 |
39 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/docs/static/.nojekyll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kazekyo/nau/9bf179e12f353e4bcec87c08f2a0e28c654cb763/docs/static/.nojekyll
--------------------------------------------------------------------------------
/docs/static/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kazekyo/nau/9bf179e12f353e4bcec87c08f2a0e28c654cb763/docs/static/img/favicon.ico
--------------------------------------------------------------------------------
/docs/static/img/logo.svg:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/docs/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | // This file is not used in compilation. It is here just for a nice editor experience.
3 | "extends": "@tsconfig/docusaurus/tsconfig.json",
4 | "compilerOptions": {
5 | "baseUrl": "."
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/examples/app/.env.development:
--------------------------------------------------------------------------------
1 | SKIP_PREFLIGHT_CHECK=true
2 |
--------------------------------------------------------------------------------
/examples/app/.prettierignore:
--------------------------------------------------------------------------------
1 | src/generated
2 |
--------------------------------------------------------------------------------
/examples/app/README.md:
--------------------------------------------------------------------------------
1 | 1. `yarn`
2 | 2. Start backend : `yarn run start:server`
3 | 3. Start frontend : `yarn run start:frontend`
4 | 4. Visit http://localhost:3001
5 |
6 |
--------------------------------------------------------------------------------
/examples/app/apollo.config.js:
--------------------------------------------------------------------------------
1 | const { apolloConfig } = require('@kazekyo/nau-config');
2 |
3 | const config = apolloConfig.generateConfig();
4 | module.exports = {
5 | client: {
6 | ...config.client,
7 | service: {
8 | name: 'example-app',
9 | url: 'http://localhost:4000/graphql',
10 | },
11 | },
12 | };
13 |
--------------------------------------------------------------------------------
/examples/app/codegen.yml:
--------------------------------------------------------------------------------
1 | schema: http://localhost:4000/graphql
2 | generates:
3 | src/generated/graphql.ts:
4 | preset: '@kazekyo/nau-graphql-codegen-preset'
5 | presetConfig:
6 | generateTypeScriptCode: true
7 | documents:
8 | - src/**/*.tsx
9 | - src/**/*.graphql
10 | plugins:
11 | - typescript
12 | - typescript-operations
13 | - typed-document-node
14 | src/generated/introspection-result.json:
15 | plugins:
16 | - fragment-matcher
17 | ./schema.graphql:
18 | plugins:
19 | - schema-ast
20 |
--------------------------------------------------------------------------------
/examples/app/graphql.config.js:
--------------------------------------------------------------------------------
1 | const { graphqlConfig } = require('@kazekyo/nau-config');
2 |
3 | module.exports = {
4 | schema: ['http://localhost:4000/graphql', graphqlConfig.clientSchemaPath],
5 | documents: 'src/**/*.{graphql,js,ts,jsx,tsx}',
6 | };
7 |
--------------------------------------------------------------------------------
/examples/app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example-app",
3 | "version": "0.4.5",
4 | "private": true,
5 | "dependencies": {
6 | "@apollo/client": "^3.6.2",
7 | "@babel/cli": "^7.12.13",
8 | "@babel/core": "^7.12.13",
9 | "@babel/plugin-proposal-class-properties": "^7.12.13",
10 | "@babel/preset-env": "^7.12.13",
11 | "@babel/preset-typescript": "^7.12.13",
12 | "@chakra-ui/icons": "^2.0.0",
13 | "@chakra-ui/react": "^2.0.0",
14 | "@emotion/react": "^11",
15 | "@emotion/styled": "^11",
16 | "@kazekyo/nau": "^0.4.4",
17 | "@types/express": "^4.17.13",
18 | "@types/node": "^18.0.0",
19 | "@types/react": "^18.0.8",
20 | "@types/react-dom": "^18.0.3",
21 | "apollo-server-core": "^3.7.0",
22 | "apollo-server-express": "^3.7.0",
23 | "express": "^4.17.1",
24 | "framer-motion": "^9.0.0",
25 | "graphql": "^16.4.0",
26 | "graphql-relay": "^0.10.0",
27 | "graphql-subscriptions": "^2.0.0",
28 | "graphql-ws": "^5.8.1",
29 | "prettier": "^2.2.1",
30 | "react": "^18.1.0",
31 | "react-dom": "^18.1.0",
32 | "react-scripts": "5.0.1",
33 | "web-vitals": "^3.0.1",
34 | "ws": "^8.6.0"
35 | },
36 | "scripts": {
37 | "start:frontend": "PORT=3001 react-scripts start",
38 | "start:server": "tsc --project server-tsconfig.json && node dist/server/index.js",
39 | "start": "yarn start:server & yarn start:frontend",
40 | "build": "react-scripts build",
41 | "eject": "react-scripts eject",
42 | "codegen": "graphql-codegen --config codegen.yml"
43 | },
44 | "browserslist": {
45 | "production": [
46 | ">0.2%",
47 | "not dead",
48 | "not op_mini all"
49 | ],
50 | "development": [
51 | "last 1 chrome version",
52 | "last 1 firefox version",
53 | "last 1 safari version"
54 | ]
55 | },
56 | "devDependencies": {
57 | "@graphql-codegen/cli": "^3.0.0",
58 | "@graphql-codegen/fragment-matcher": "^4.0.0",
59 | "@graphql-codegen/schema-ast": "^3.0.0",
60 | "@graphql-codegen/typed-document-node": "^3.0.0",
61 | "@graphql-codegen/typescript": "^3.0.0",
62 | "@graphql-codegen/typescript-operations": "^3.0.0",
63 | "@kazekyo/nau-config": "^0.4.4",
64 | "@kazekyo/nau-graphql-codegen-preset": "^0.4.5",
65 | "typescript": "^4.6.4"
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/examples/app/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kazekyo/nau/9bf179e12f353e4bcec87c08f2a0e28c654cb763/examples/app/public/favicon.ico
--------------------------------------------------------------------------------
/examples/app/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | Example
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/examples/app/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kazekyo/nau/9bf179e12f353e4bcec87c08f2a0e28c654cb763/examples/app/public/logo192.png
--------------------------------------------------------------------------------
/examples/app/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kazekyo/nau/9bf179e12f353e4bcec87c08f2a0e28c654cb763/examples/app/public/logo512.png
--------------------------------------------------------------------------------
/examples/app/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/examples/app/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.itemstxt.org/itemstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/examples/app/schema.graphql:
--------------------------------------------------------------------------------
1 | input AddItemInput {
2 | clientMutationId: String
3 | itemName: String!
4 | userId: ID!
5 | }
6 |
7 | type AddItemPayload {
8 | clientMutationId: String
9 | item: Item!
10 | }
11 |
12 | type Item implements Node {
13 | """The ID of an object"""
14 | id: ID!
15 |
16 | """The name of the item."""
17 | name: String!
18 | }
19 |
20 | type ItemAddedPayload {
21 | item: Item!
22 | }
23 |
24 | """A connection to a list of items."""
25 | type ItemConnection {
26 | """A list of edges."""
27 | edges: [ItemEdge]
28 |
29 | """Information to aid in pagination."""
30 | pageInfo: PageInfo!
31 | }
32 |
33 | """An edge in a connection."""
34 | type ItemEdge {
35 | """A cursor for use in pagination"""
36 | cursor: String!
37 |
38 | """The item at the end of the edge"""
39 | node: Item
40 | }
41 |
42 | type ItemRemovedPayload {
43 | """The ID of an object"""
44 | id: ID!
45 | }
46 |
47 | type Mutation {
48 | addItem(input: AddItemInput!): AddItemPayload
49 | removeItem(input: RemoveItemInput!): RemoveItemPayload
50 | updateItem(input: UpdateItemInput!): UpdateItemPayload
51 | }
52 |
53 | """An object with an ID"""
54 | interface Node {
55 | """The id of the object."""
56 | id: ID!
57 | }
58 |
59 | """Information about pagination in a connection."""
60 | type PageInfo {
61 | """When paginating forwards, the cursor to continue."""
62 | endCursor: String
63 |
64 | """When paginating forwards, are there more items?"""
65 | hasNextPage: Boolean!
66 |
67 | """When paginating backwards, are there more items?"""
68 | hasPreviousPage: Boolean!
69 |
70 | """When paginating backwards, the cursor to continue."""
71 | startCursor: String
72 | }
73 |
74 | type Query {
75 | items(
76 | """Returns the items in the list that come after the specified cursor."""
77 | after: String
78 |
79 | """Returns the items in the list that come before the specified cursor."""
80 | before: String
81 |
82 | """Returns the first n items from the list."""
83 | first: Int
84 | keyword: String
85 |
86 | """Returns the last n items from the list."""
87 | last: Int
88 | ): ItemConnection!
89 |
90 | """Fetches an object given its ID"""
91 | node(
92 | """The ID of an object"""
93 | id: ID!
94 | ): Node
95 | viewer: User
96 | }
97 |
98 | input RemoveItemInput {
99 | clientMutationId: String
100 | itemId: String!
101 | userId: ID!
102 | }
103 |
104 | type RemoveItemPayload {
105 | clientMutationId: String
106 | removedItem: RemovedItem!
107 | }
108 |
109 | type RemovedItem {
110 | """The ID of an object"""
111 | id: ID!
112 | }
113 |
114 | type Subscription {
115 | itemAdded: ItemAddedPayload!
116 | itemRemoved: ItemRemovedPayload!
117 | }
118 |
119 | input UpdateItemInput {
120 | clientMutationId: String
121 | itemId: ID!
122 | newItemName: String!
123 | }
124 |
125 | type UpdateItemPayload {
126 | clientMutationId: String
127 | item: Item!
128 | }
129 |
130 | type User implements Node {
131 | """The ID of an object"""
132 | id: ID!
133 | items(
134 | """Returns the items in the list that come after the specified cursor."""
135 | after: String
136 |
137 | """Returns the items in the list that come before the specified cursor."""
138 | before: String
139 |
140 | """Returns the first n items from the list."""
141 | first: Int
142 | keyword: String
143 |
144 | """Returns the last n items from the list."""
145 | last: Int
146 | ): ItemConnection!
147 |
148 | """The name of the user."""
149 | name: String
150 | }
--------------------------------------------------------------------------------
/examples/app/server-tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "rootDir": "src",
4 | "outDir": "dist",
5 | "lib": ["es2019"],
6 | "module": "commonjs",
7 | "target": "ES2017",
8 | "moduleResolution": "node",
9 | "esModuleInterop": true,
10 | "skipLibCheck": true
11 | },
12 | "include": ["src/server/**/*"]
13 | }
14 |
--------------------------------------------------------------------------------
/examples/app/src/App.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kazekyo/nau/9bf179e12f353e4bcec87c08f2a0e28c654cb763/examples/app/src/App.css
--------------------------------------------------------------------------------
/examples/app/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { gql, useQuery } from '@apollo/client';
2 | import { Box, Divider } from '@chakra-ui/react';
3 | import React from 'react';
4 | import './App.css';
5 | import { AppQueryDocument } from './generated/graphql';
6 | import List from './List';
7 |
8 | gql`
9 | query AppQuery {
10 | viewer {
11 | id
12 | ...List_user
13 | }
14 | }
15 | `;
16 |
17 | const App: React.FC = () => {
18 | const { loading, error, data } = useQuery(AppQueryDocument);
19 |
20 | if (loading || !data) return Loading...
;
21 | if (error) return Error :(
;
22 |
23 | return (
24 |
25 |
26 |
27 |
28 | My items
29 |
30 |
31 | User ID: {data.viewer?.id}
32 |
33 |
34 |
35 | {data.viewer &&
}
36 |
37 |
38 | );
39 | };
40 |
41 | export default App;
42 |
--------------------------------------------------------------------------------
/examples/app/src/List.tsx:
--------------------------------------------------------------------------------
1 | import { gql, useMutation, useSubscription } from '@apollo/client';
2 | import { AddIcon } from '@chakra-ui/icons';
3 | import { Box, Button } from '@chakra-ui/react';
4 | import { usePagination } from '@kazekyo/nau';
5 | import * as React from 'react';
6 | import {
7 | AddItemMutationDocument,
8 | ItemAddedSubscriptionDocument,
9 | ItemRemovedSubscriptionDocument,
10 | List_PaginationQueryDocument,
11 | List_UserFragment,
12 | } from './generated/graphql';
13 | import ListItem from './ListItem';
14 |
15 | gql`
16 | fragment List_user on User
17 | @argumentDefinitions(count: { type: "Int", defaultValue: 2 }, cursor: { type: "String" })
18 | @refetchable(queryName: "List_PaginationQuery") {
19 | items(first: $count, after: $cursor) @pagination {
20 | edges {
21 | node {
22 | ...ListItem_item
23 | }
24 | }
25 | }
26 | ...ListItem_user
27 | }
28 |
29 | mutation AddItemMutation($input: AddItemInput!, $connections: [String!]!) {
30 | addItem(input: $input) {
31 | item @prependNode(connections: $connections) {
32 | ...ListItem_item
33 | }
34 | }
35 | }
36 |
37 | subscription ItemAddedSubscription($connections: [String!]!) {
38 | itemAdded {
39 | item @prependNode(connections: $connections) {
40 | ...ListItem_item
41 | }
42 | }
43 | }
44 |
45 | subscription ItemRemovedSubscription {
46 | itemRemoved {
47 | id @deleteRecord(typename: "Item")
48 | }
49 | }
50 | `;
51 |
52 | const List: React.FC<{ user: List_UserFragment }> = ({ user }) => {
53 | useSubscription(ItemAddedSubscriptionDocument, {
54 | variables: {
55 | connections: [user.items._connectionId],
56 | },
57 | });
58 | useSubscription(ItemRemovedSubscriptionDocument);
59 | const [addItem] = useMutation(AddItemMutationDocument);
60 | const { nodes, hasNext, loadNext, isLoading } = usePagination(List_PaginationQueryDocument, {
61 | id: user.id,
62 | connection: user.items,
63 | });
64 |
65 | return (
66 | <>
67 |
68 | }
70 | onClick={() =>
71 | void addItem({
72 | variables: {
73 | input: { itemName: 'new item', userId: user.id },
74 | connections: [user.items._connectionId],
75 | },
76 | })
77 | }
78 | >
79 | Add Item
80 |
81 |
82 |
83 | {nodes.map((node) => {
84 | return (
85 |
86 |
87 |
88 | );
89 | })}
90 |
91 | {hasNext && (
92 |
102 | )}
103 | >
104 | );
105 | };
106 |
107 | export default List;
108 |
--------------------------------------------------------------------------------
/examples/app/src/ListItem.tsx:
--------------------------------------------------------------------------------
1 | import { gql, useMutation } from '@apollo/client';
2 | import { DeleteIcon } from '@chakra-ui/icons';
3 | import { Box, Button, Spacer } from '@chakra-ui/react';
4 | import * as React from 'react';
5 | import { FC } from 'react';
6 | import { ListItem_ItemFragment, ListItem_UserFragment, RemoveItemMutationDocument } from './generated/graphql';
7 |
8 | gql`
9 | fragment ListItem_user on User {
10 | id
11 | }
12 |
13 | fragment ListItem_item on Item {
14 | id
15 | name
16 | }
17 |
18 | mutation RemoveItemMutation($input: RemoveItemInput!) {
19 | removeItem(input: $input) {
20 | removedItem {
21 | id @deleteRecord(typename: "Item")
22 | }
23 | }
24 | }
25 | `;
26 |
27 | const ListItem: FC<{
28 | user: ListItem_UserFragment;
29 | item: ListItem_ItemFragment;
30 | }> = ({ user, item }) => {
31 | const [removeItem] = useMutation(RemoveItemMutationDocument);
32 |
33 | return (
34 |
35 |
36 |
37 |
38 | {item.name}
39 |
40 |
41 | Item ID: {item.id}
42 |
43 |
44 | }
47 | aria-label="Delete"
48 | colorScheme="red"
49 | onClick={() => void removeItem({ variables: { input: { itemId: item.id, userId: user.id } } })}
50 | >
51 | Delete
52 |
53 |
54 |
55 |
56 | );
57 | };
58 |
59 | export default ListItem;
60 |
--------------------------------------------------------------------------------
/examples/app/src/generated/introspection-result.json:
--------------------------------------------------------------------------------
1 | {
2 | "possibleTypes": {
3 | "Node": [
4 | "Item",
5 | "User"
6 | ]
7 | }
8 | }
--------------------------------------------------------------------------------
/examples/app/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Itemo', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans',
4 | 'Droid Sans', 'Helvetica Neue', sans-serif;
5 | -webkit-font-smoothing: antialiased;
6 | -moz-osx-font-smoothing: grayscale;
7 | }
8 |
9 | code {
10 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
11 | }
12 |
--------------------------------------------------------------------------------
/examples/app/src/index.tsx:
--------------------------------------------------------------------------------
1 | import { ApolloClient, ApolloProvider, from, HttpLink, InMemoryCache, PossibleTypesMap, split } from '@apollo/client';
2 | import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
3 | import { relayStylePagination } from '@apollo/client/utilities';
4 | import { ChakraProvider } from '@chakra-ui/react';
5 | import { createCacheUpdaterLink, isSubscriptionOperation } from '@kazekyo/nau';
6 | import { createClient } from 'graphql-ws';
7 | import React from 'react';
8 | import { createRoot } from 'react-dom/client';
9 | import App from './App';
10 | import { withCacheUpdater } from './generated/graphql';
11 | import introspection from './generated/introspection-result.json';
12 | import './index.css';
13 | const introspectionResult = introspection as { possibleTypes: PossibleTypesMap };
14 |
15 | const wsLink = new GraphQLWsLink(createClient({ url: 'ws://localhost:4000/subscription' }));
16 | const httpLink = new HttpLink({ uri: 'http://localhost:4000/graphql' });
17 | const cacheUpdaterLink = createCacheUpdaterLink();
18 | const splitLink = split(
19 | ({ query }) => isSubscriptionOperation(query),
20 | from([cacheUpdaterLink, wsLink]),
21 | from([cacheUpdaterLink, httpLink]),
22 | );
23 |
24 | const client = new ApolloClient({
25 | cache: new InMemoryCache({
26 | addTypename: true,
27 | possibleTypes: introspectionResult.possibleTypes,
28 | typePolicies: withCacheUpdater({
29 | User: {
30 | fields: {
31 | items: relayStylePagination(),
32 | },
33 | },
34 | }),
35 | }),
36 | link: splitLink,
37 | });
38 |
39 | const container = document.getElementById('root');
40 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
41 | const root = createRoot(container!);
42 | root.render(
43 |
44 |
45 |
46 |
47 |
48 |
49 | ,
50 | );
51 |
--------------------------------------------------------------------------------
/examples/app/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/app/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/examples/app/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "esModuleInterop": true,
8 | "allowSyntheticDefaultImports": true,
9 | "strict": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "noFallthroughCasesInSwitch": true,
12 | "module": "esnext",
13 | "moduleResolution": "node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "noEmit": true,
17 | "jsx": "react-jsx"
18 | },
19 | "include": ["src/**/*"]
20 | }
21 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | setupFilesAfterEnv: ['/setupJest.js'],
3 | testEnvironment: 'jsdom',
4 | collectCoverageFrom: ['packages/**/*.{ts,tsx}'],
5 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
6 | modulePathIgnorePatterns: ['/examples'],
7 | testMatch: ['**/?(*.)+(spec|test).+(ts|tsx|js)'],
8 | transform: {
9 | '^.+\\.(ts|tsx)$': 'ts-jest',
10 | },
11 | transformIgnorePatterns: ['[/\\\\]node_modules[/\\\\].+\\.(js|jsx)$'],
12 | globals: {
13 | 'ts-jest': {
14 | tsconfig: 'tsconfig.json',
15 | },
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/lerna.json:
--------------------------------------------------------------------------------
1 | {
2 | "packages": ["packages/*"],
3 | "version": "0.4.5",
4 | "npmClient": "yarn",
5 | "useWorkspaces": true,
6 | "registry": "https://registry.npmjs.org/"
7 | }
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nau",
3 | "private": true,
4 | "workspaces": {
5 | "packages": [
6 | "packages/*",
7 | "examples/*"
8 | ]
9 | },
10 | "devDependencies": {
11 | "@babel/cli": "^7.12.13",
12 | "@babel/core": "^7.12.13",
13 | "@babel/plugin-proposal-class-properties": "^7.12.13",
14 | "@babel/preset-env": "^7.12.13",
15 | "@babel/preset-typescript": "^7.12.13",
16 | "@testing-library/jest-dom": "^5.14.1",
17 | "@testing-library/react": "^13.2.0",
18 | "@types/jest": "^28.1.0",
19 | "@typescript-eslint/eslint-plugin": "^5.23.0",
20 | "@typescript-eslint/parser": "^5.23.0",
21 | "babel-jest": "^29.0.2",
22 | "eslint": "^8.15.0",
23 | "eslint-config-prettier": "^8.3.0",
24 | "eslint-plugin-babel": "5.3.1",
25 | "eslint-plugin-prettier": "^4.0.0",
26 | "graphql": "^16.4.0",
27 | "jest": "^28.1.0",
28 | "jest-environment-jsdom": "^29.0.3",
29 | "lerna": "^6.0.0",
30 | "prettier": "^2.2.1",
31 | "ts-jest": "^28.0.2",
32 | "typescript": "^4.6.4"
33 | },
34 | "scripts": {
35 | "prebuild": "yarn lerna exec -- rimraf dist --ignore=example-app",
36 | "build": "yarn lerna exec -- yarn build --ignore=example-app",
37 | "test": "jest",
38 | "lint": "eslint \"packages/**/src/**/*.{js,jsx,ts,tsx}\"",
39 | "start:docs": "cd docs && yarn start",
40 | "build:docs": "cd docs && yarn build"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/packages/config/README.md:
--------------------------------------------------------------------------------
1 | # @kazekyo/nau
2 |
3 | > Configuration for [Nau](https://github.com/kazekyo/nau)
4 |
5 | See the [documentation](https://www.naugraphql.com/docs/introduction) for more information.
6 |
7 | ## Installation
8 |
9 | ```sh
10 | yarn add -D @kazekyo/nau-config
11 | ```
12 |
--------------------------------------------------------------------------------
/packages/config/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@kazekyo/nau-config",
3 | "version": "0.4.4",
4 | "description": "Configuration for Nau",
5 | "author": "kazekyo",
6 | "bugs": {
7 | "url": "https://github.com/kazekyo/nau/issues"
8 | },
9 | "dependencies": {
10 | "@relay-graphql-js/validation-rules": "^0.1.0"
11 | },
12 | "peerDependencies": {
13 | "graphql": "^15.6.1"
14 | },
15 | "files": [
16 | "dist"
17 | ],
18 | "homepage": "https://www.naugraphql.com",
19 | "keywords": [
20 | "apollo",
21 | "client",
22 | "graphql",
23 | "nau",
24 | "directive",
25 | "config",
26 | "configuration"
27 | ],
28 | "license": "MIT",
29 | "main": "dist/index.js",
30 | "repository": {
31 | "type": "git",
32 | "url": "git+https://github.com/kazekyo/nau.git"
33 | },
34 | "scripts": {
35 | "test": "jest",
36 | "lint": "eslint src",
37 | "generate:client-schema": "node scripts/generateClientSchema.js",
38 | "build": "yarn run build:types && yarn run build:js && yarn run generate:client-schema",
39 | "build:types": "tsc --build tsconfig.build.json",
40 | "build:js": "babel src --out-dir dist --root-mode upward --extensions \".ts,.tsx\" --source-maps inline --ignore \"**/__tests__/\",\"**/*.test.ts\",\"example\",\"**/testing/\""
41 | },
42 | "types": "dist/index.d.ts",
43 | "publishConfig": {
44 | "access": "public",
45 | "registry": "https://registry.npmjs.org/"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/packages/config/scripts/generateClientSchema.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const clientSchema = require('../dist/clientSchema');
3 |
4 | const apolloConfigClientSchema = clientSchema.apolloConfigClientSchema();
5 | const graphQLConfigClientSchema = clientSchema.graphQLConfigClientSchema();
6 |
7 | fs.writeFileSync(clientSchema.apolloConfigClientSchemaPath, apolloConfigClientSchema);
8 | fs.writeFileSync(clientSchema.graphqlConfigClientSchemaPath, graphQLConfigClientSchema);
9 |
--------------------------------------------------------------------------------
/packages/config/src/__tests__/clientSchema.test.ts:
--------------------------------------------------------------------------------
1 | import { apolloConfigClientSchema, graphQLConfigClientSchema } from '../clientSchema';
2 |
3 | describe('apolloClientSchema', () => {
4 | it('returns a client schema for apollo.config.js', () => {
5 | expect(apolloConfigClientSchema()).toBe(`directive @arguments on FRAGMENT_SPREAD
6 | directive @argumentDefinitions on FRAGMENT_DEFINITION
7 | directive @refetchable(queryName: String!) on FRAGMENT_DEFINITION
8 | directive @pagination on FIELD
9 | directive @appendNode(connections: [String!]) on FIELD
10 | directive @prependNode(connections: [String!]) on FIELD
11 | directive @deleteRecord(typename: String!) on FIELD
12 | directive @client on FIELD`);
13 | });
14 | });
15 |
16 | describe('graphQLConfigClientSchema', () => {
17 | it('returns a client schema for graphql.config.js', () => {
18 | expect(graphQLConfigClientSchema()).toBe(`directive @refetchable(queryName: String!) on FRAGMENT_DEFINITION
19 | directive @pagination on FIELD
20 | directive @appendNode(connections: [String!]) on FIELD
21 | directive @prependNode(connections: [String!]) on FIELD
22 | directive @deleteRecord(typename: String!) on FIELD
23 | directive @client on FIELD`);
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/packages/config/src/apolloConfig.ts:
--------------------------------------------------------------------------------
1 | import { ValidationRule } from 'graphql';
2 | import { loadDefaultValidationRules } from './dependencies/apollo';
3 | import { apolloConfigClientSchemaPath } from './clientSchema';
4 | import { validationRules } from './validationRules';
5 |
6 | const ignored = [
7 | 'NoUnusedFragmentsRule',
8 | 'NoUnusedVariablesRule',
9 | 'KnownArgumentNamesRule',
10 | 'NoUndefinedVariablesRule',
11 | ];
12 |
13 | export const generateValidationRulesForApolloConfig = (): ValidationRule[] => {
14 | const defaultValidationRules = loadDefaultValidationRules();
15 | return [
16 | ...defaultValidationRules.filter((f) => !ignored.includes(f.name)),
17 | ...validationRules,
18 | ] as unknown[] as ValidationRule[];
19 | };
20 |
21 | export const generateApolloConfig = () => {
22 | return {
23 | client: {
24 | includes: ['src/**/*.{ts,tsx,js,jsx,graphql,gql}', apolloConfigClientSchemaPath],
25 | validationRules: generateValidationRulesForApolloConfig(),
26 | },
27 | };
28 | };
29 |
--------------------------------------------------------------------------------
/packages/config/src/clientDirective.ts:
--------------------------------------------------------------------------------
1 | export const clientDirectives = {
2 | arguments: 'directive @arguments on FRAGMENT_SPREAD',
3 | argumentDefinitions: 'directive @argumentDefinitions on FRAGMENT_DEFINITION',
4 | refetchable: 'directive @refetchable(queryName: String!) on FRAGMENT_DEFINITION',
5 | pagination: 'directive @pagination on FIELD',
6 | appendNode: 'directive @appendNode(connections: [String!]) on FIELD',
7 | prependNode: 'directive @prependNode(connections: [String!]) on FIELD',
8 | deleteRecord: 'directive @deleteRecord(typename: String!) on FIELD',
9 | client: 'directive @client on FIELD',
10 | };
11 |
--------------------------------------------------------------------------------
/packages/config/src/clientSchema.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'path';
2 | import { clientDirectives } from './clientDirective';
3 |
4 | export const apolloConfigClientSchemaPath = path.join(__dirname, `apollo.graphql`);
5 | export const graphqlConfigClientSchemaPath = path.join(__dirname, `graphql-config.graphql`);
6 |
7 | export const graphQLConfigClientSchema = (): string => {
8 | return generateClientSchemaString({ exclude: ['arguments', 'argumentDefinitions'] });
9 | };
10 |
11 | export const apolloConfigClientSchema = (): string => {
12 | return generateClientSchemaString({ exclude: [] });
13 | };
14 |
15 | const generateClientSchemaString = ({ exclude }: { exclude: string[] }): string => {
16 | const directives = Object.entries(clientDirectives)
17 | .filter(([key]) => !exclude.includes(key))
18 | .map(([_, str]) => str);
19 | return directives.join('\n');
20 | };
21 |
--------------------------------------------------------------------------------
/packages/config/src/dependencies/apollo.ts:
--------------------------------------------------------------------------------
1 | import * as _GraphQL from 'graphql';
2 |
3 | const mod = require.main || module;
4 | const isJest = typeof jest !== 'undefined';
5 |
6 | // NOTE: By not loading dependencies until this function is called,
7 | // we can use this library in extensions without './errors/validation'
8 | export const loadDefaultValidationRules = () => {
9 | let validationRules: _GraphQL.ValidationRule[] = [];
10 | if (!isJest) {
11 | const { defaultValidationRules } = mod.require('./errors/validation') as {
12 | defaultValidationRules: _GraphQL.ValidationRule[];
13 | };
14 | validationRules = defaultValidationRules;
15 | }
16 | return validationRules;
17 | };
18 |
--------------------------------------------------------------------------------
/packages/config/src/dependencies/graphql.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */
2 | import * as _GraphQL from 'graphql';
3 | const mod = require.main || module;
4 |
5 | let GraphQLModule;
6 | try {
7 | GraphQLModule = mod.require('graphql');
8 | } catch {
9 | GraphQLModule = _GraphQL;
10 | }
11 |
12 | export const {
13 | BREAK,
14 | GraphQLError,
15 | parseType,
16 | visit,
17 | isNonNullType,
18 | valueFromAST,
19 | isTypeSubTypeOf,
20 | getNullableType,
21 | typeFromAST,
22 | GraphQLNonNull,
23 | GraphQLObjectType,
24 | visitWithTypeInfo,
25 | isInputType,
26 | TypeInfo,
27 | isObjectType,
28 | } = GraphQLModule as typeof _GraphQL;
29 |
--------------------------------------------------------------------------------
/packages/config/src/index.ts:
--------------------------------------------------------------------------------
1 | import { apolloConfigClientSchemaPath, graphqlConfigClientSchemaPath } from './clientSchema';
2 | import { generateApolloConfig } from './apolloConfig';
3 |
4 | export * from './validationRules';
5 | export * from './clientDirective';
6 |
7 | export const graphqlConfig = {
8 | clientSchemaPath: graphqlConfigClientSchemaPath,
9 | };
10 |
11 | export const apolloConfig = {
12 | generateConfig: generateApolloConfig,
13 | clientSchemaPath: apolloConfigClientSchemaPath,
14 | };
15 |
--------------------------------------------------------------------------------
/packages/config/src/validationRules/__tests__/paginationDirective.test.ts:
--------------------------------------------------------------------------------
1 | import { readFileSync } from 'fs';
2 | import { buildSchema, GraphQLError, parse, validate } from 'graphql';
3 | import path from 'path';
4 | import { paginationDirectiveValidationRule } from '../paginationDirective';
5 |
6 | const filePath = path.join(__dirname, '../../../../graphql-codegen-preset/src/utils/testing/example.graphql');
7 | const schemaString = readFileSync(filePath, { encoding: 'utf-8' });
8 | const schema = buildSchema(schemaString);
9 |
10 | const validateDocuments = (source: string) => {
11 | return validate(schema, parse(source), [paginationDirectiveValidationRule]);
12 | };
13 |
14 | describe('paginationDirectiveValidationRule', () => {
15 | it('allows use of @pagination directive', () => {
16 | const errors = validateDocuments(/* GraphQL */ `
17 | query TestQuery($cursor: String) {
18 | viewer {
19 | items(first: 1, after: $cursor) @pagination {
20 | edges {
21 | node {
22 | id
23 | }
24 | }
25 | }
26 | }
27 | }
28 | `);
29 | expect(errors).toHaveLength(0);
30 | });
31 |
32 | it('disallows use of @pagination directive without after or before', () => {
33 | const errors = validateDocuments(/* GraphQL */ `
34 | query TestQuery($cursor: String) {
35 | viewer {
36 | items(first: 1) @pagination {
37 | edges {
38 | node {
39 | id
40 | }
41 | }
42 | }
43 | }
44 | }
45 | `);
46 | expect(errors).toStrictEqual([new GraphQLError('@pagination directive is required `after` or `before` argument.')]);
47 | });
48 |
49 | it('disallows use of @pagination directive with non-connection field', () => {
50 | const errors = validateDocuments(/* GraphQL */ `
51 | query TestQuery($cursor: String) {
52 | viewer {
53 | items(first: 1, after: $cursor) {
54 | edges {
55 | node {
56 | id @pagination
57 | }
58 | }
59 | }
60 | }
61 | }
62 | `);
63 | expect(errors).toStrictEqual([
64 | new GraphQLError('@pagination can only be used with types whose name ends with "Connection".'),
65 | ]);
66 | });
67 | });
68 |
--------------------------------------------------------------------------------
/packages/config/src/validationRules/index.ts:
--------------------------------------------------------------------------------
1 | export * from './paginationDirective';
2 | export * from './validationRule';
3 |
--------------------------------------------------------------------------------
/packages/config/src/validationRules/paginationDirective.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-unsafe-return */
2 | import { DirectiveNode, FieldNode, GraphQLOutputType, ValidationRule } from 'graphql';
3 | import { GraphQLError, isNonNullType, isObjectType } from '../dependencies/graphql';
4 |
5 | const PAGINATION_DIRECTIVE_NAME = 'pagination';
6 |
7 | function hasAfterArgument(fieldNode: FieldNode): boolean {
8 | return !!(fieldNode.arguments && fieldNode.arguments.find((arg) => arg.name.value === 'after'));
9 | }
10 |
11 | function hasBeforeArgument(fieldNode: FieldNode): boolean {
12 | return !!(fieldNode.arguments && fieldNode.arguments.find((arg) => arg.name.value === 'before'));
13 | }
14 |
15 | const getPaginationDirective = (fieldNode: FieldNode): DirectiveNode | undefined => {
16 | if (!fieldNode.directives) return undefined;
17 | return fieldNode.directives.find((directive) => directive.name.value === PAGINATION_DIRECTIVE_NAME);
18 | };
19 |
20 | function isPaginationType(type: GraphQLOutputType): boolean {
21 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
22 | const connectionType = isNonNullType(type) ? type.ofType : type;
23 |
24 | if (!isObjectType(connectionType)) {
25 | return false;
26 | }
27 |
28 | return connectionType.name.endsWith('Connection');
29 | }
30 |
31 | export const paginationDirectiveValidationRule: ValidationRule = (context) => {
32 | return {
33 | Field: {
34 | enter(fieldNode) {
35 | const paginationDirective = getPaginationDirective(fieldNode);
36 | if (!paginationDirective) return;
37 |
38 | const type = context.getType();
39 | if (!type || !isPaginationType(type)) {
40 | context.reportError(
41 | new GraphQLError(
42 | `@${PAGINATION_DIRECTIVE_NAME} can only be used with types whose name ends with "Connection".`,
43 | fieldNode,
44 | ),
45 | );
46 | return;
47 | }
48 |
49 | if (!hasAfterArgument(fieldNode) && !hasBeforeArgument(fieldNode)) {
50 | context.reportError(
51 | new GraphQLError(
52 | `@${PAGINATION_DIRECTIVE_NAME} directive is required \`after\` or \`before\` argument.`,
53 | fieldNode,
54 | ),
55 | );
56 | }
57 | },
58 | },
59 | };
60 | };
61 |
--------------------------------------------------------------------------------
/packages/config/src/validationRules/validationRule.ts:
--------------------------------------------------------------------------------
1 | import {
2 | RelayArgumentsOfCorrectType,
3 | RelayDefaultValueOfCorrectType,
4 | RelayKnownArgumentNames,
5 | RelayNoUnusedArguments,
6 | } from '@relay-graphql-js/validation-rules';
7 | import { ValidationRule } from 'graphql';
8 | import { paginationDirectiveValidationRule } from './paginationDirective';
9 |
10 | export const validationRules: ValidationRule[] = [
11 | RelayArgumentsOfCorrectType,
12 | RelayDefaultValueOfCorrectType,
13 | RelayNoUnusedArguments,
14 | RelayKnownArgumentNames,
15 | paginationDirectiveValidationRule,
16 | ] as unknown[] as ValidationRule[];
17 |
--------------------------------------------------------------------------------
/packages/config/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "dist",
5 | "emitDeclarationOnly": true
6 | },
7 | "include": ["src/**/*"],
8 | "exclude": ["**/__tests__/", "**/*.test.ts", "**/testing/", "node_modules"]
9 | }
10 |
--------------------------------------------------------------------------------
/packages/core/README.md:
--------------------------------------------------------------------------------
1 | # @kazekyo/nau
2 |
3 | Nau is a tool that makes the use of [Apollo Client](https://github.com/apollographql/apollo-client) more productive for users using [Relay GraphQL Server Specification](https://relay.dev/docs/guides/graphql-server-specification) compliant backends.
4 |
5 | See the [documentation](https://www.naugraphql.com/docs/introduction) for more information.
6 |
7 | ## Installation
8 |
9 | ```sh
10 | yarn add @kazekyo/nau
11 | ```
12 |
--------------------------------------------------------------------------------
/packages/core/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@kazekyo/nau",
3 | "version": "0.4.4",
4 | "description": "Nau is a tool that makes the use of Apollo Client more productive for users using Relay GraphQL Server Specification compliant backends.",
5 | "author": "kazekyo",
6 | "bugs": {
7 | "url": "https://github.com/kazekyo/nau/issues"
8 | },
9 | "dependencies": {
10 | "js-base64": "^3.6.0"
11 | },
12 | "devDependencies": {
13 | "@types/react": "^18.0.8",
14 | "react": "^18.1.0",
15 | "react-dom": "^18.1.0"
16 | },
17 | "files": [
18 | "dist"
19 | ],
20 | "homepage": "https://www.naugraphql.com",
21 | "keywords": [
22 | "apollo",
23 | "client",
24 | "relay",
25 | "graphql",
26 | "pagination",
27 | "cache",
28 | "directive"
29 | ],
30 | "license": "MIT",
31 | "main": "dist/index.js",
32 | "repository": {
33 | "type": "git",
34 | "url": "git+https://github.com/kazekyo/nau.git"
35 | },
36 | "scripts": {
37 | "test": "jest",
38 | "lint": "eslint src",
39 | "build": "yarn run build:types && yarn run build:js",
40 | "build:types": "tsc --build tsconfig.build.json",
41 | "build:js": "babel src --out-dir dist --root-mode upward --extensions \".ts,.tsx\" --source-maps inline --ignore \"**/__tests__/\",\"**/*.test.ts\",\"example\",\"**/testing/\""
42 | },
43 | "types": "dist/index.d.ts",
44 | "peerDependencies": {
45 | "@apollo/client": "^3.6.5",
46 | "graphql": "^15.6.1"
47 | },
48 | "gitHead": "9337754515288d8fbd606f67b7ed88b14c6ef678",
49 | "publishConfig": {
50 | "access": "public",
51 | "registry": "https://registry.npmjs.org/"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/packages/core/src/hooks/index.ts:
--------------------------------------------------------------------------------
1 | export * from './usePagination';
2 |
--------------------------------------------------------------------------------
/packages/core/src/hooks/usePagination.ts:
--------------------------------------------------------------------------------
1 | import { DocumentNode, OperationVariables, TypedDocumentNode, useLazyQuery } from '@apollo/client';
2 | import { getNodesFromConnection } from '../utils';
3 |
4 | type LoadFunction = (count: number) => void;
5 | type PageInfo = {
6 | hasNextPage?: boolean;
7 | hasPreviousPage?: boolean;
8 | endCursor?: string | null;
9 | startCursor?: string | null;
10 | };
11 | type Edge = {
12 | node?: TNode | null;
13 | };
14 |
15 | export type UsePaginationOptions = {
16 | id: string;
17 | connection: {
18 | edges?: (Edge | null | undefined)[] | null;
19 | pageInfo?: PageInfo;
20 | };
21 | variables?: TVariables;
22 | };
23 |
24 | export const usePagination = (
25 | document: DocumentNode | TypedDocumentNode,
26 | // If 'options' is undefined, it will always return empty nodes. It exists to prevent the use of conditions outside of this hook.
27 | // https://en.reactjs.org/docs/hooks-rules.html#explanation
28 | options: UsePaginationOptions | undefined,
29 | ): {
30 | nodes: TNode[];
31 | loadNext: LoadFunction;
32 | loadPrevious: LoadFunction;
33 | hasNext: boolean;
34 | hasPrevious: boolean;
35 | isLoading: boolean;
36 | refetch?: ReturnType['1']['refetch'];
37 | } => {
38 | if (!options)
39 | return {
40 | nodes: [],
41 | loadNext: () => ({}),
42 | loadPrevious: () => ({}),
43 | hasNext: false,
44 | hasPrevious: false,
45 | isLoading: false,
46 | };
47 |
48 | const { id, connection } = options;
49 |
50 | const [call, { called, loading, fetchMore, refetch }] = useLazyQuery(document);
51 |
52 | const load = (count: number, cursor: string | undefined | null) => {
53 | if (!cursor) return;
54 | const allVariables = { id, count, cursor, ...(options.variables || {}) } as unknown;
55 | if (called && fetchMore) {
56 | void fetchMore({ variables: allVariables as TVariables });
57 | } else {
58 | void call({ variables: allVariables as TVariables, fetchPolicy: 'network-only', nextFetchPolicy: 'cache-first' });
59 | }
60 | };
61 |
62 | const loadNext: LoadFunction = (count) => load(count, connection?.pageInfo?.endCursor);
63 | const loadPrevious: LoadFunction = (count) => load(count, connection.pageInfo?.startCursor);
64 |
65 | const nodes = getNodesFromConnection({ connection });
66 |
67 | return {
68 | nodes,
69 | loadNext,
70 | loadPrevious,
71 | hasNext: connection.pageInfo?.hasNextPage || false,
72 | hasPrevious: connection.pageInfo?.hasPreviousPage || false,
73 | isLoading: loading,
74 | refetch,
75 | };
76 | };
77 |
--------------------------------------------------------------------------------
/packages/core/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './hooks';
2 | export * from './links';
3 | export * from './policies';
4 | export * from './utils';
5 |
--------------------------------------------------------------------------------
/packages/core/src/links/__tests__/cacheUpdaterLink.test.ts:
--------------------------------------------------------------------------------
1 | import { ApolloLink, execute, gql, Observable } from '@apollo/client';
2 | import { print } from 'graphql/language';
3 | import { createCacheUpdaterLink } from '../cacheUpdaterLink';
4 |
5 | describe('cacheUpdaterLink', () => {
6 | const subjectLink = createCacheUpdaterLink();
7 | it('removes directives and related variables from the mutation operation', (done) => {
8 | const document = gql`
9 | mutation Mutation($connections: [String!]!) {
10 | foo @prependNode(connections: $connections) {
11 | id
12 | }
13 | bar @appendNode(connections: $connections) {
14 | id
15 | }
16 | id @deleteRecord(typename: "Foo")
17 | }
18 | `;
19 | const expectedDocument = gql`
20 | mutation Mutation {
21 | foo {
22 | id
23 | }
24 | bar {
25 | id
26 | }
27 | id
28 | }
29 | `;
30 | const variables = { connections: ['dummyId'], other: 'ok' };
31 | const expectedVariables = { other: 'ok' };
32 |
33 | const successMockData = { data: { success: 'ok' } };
34 | const mockLink = new ApolloLink((operation) => {
35 | expect(print(operation.query)).toBe(print(expectedDocument));
36 | expect(operation.variables).toMatchObject(expectedVariables);
37 | return Observable.of(successMockData);
38 | });
39 | const link = subjectLink.concat(mockLink);
40 | execute(link, { query: document, variables }).subscribe((result) => {
41 | expect(result).toBe(successMockData);
42 | done();
43 | });
44 | });
45 | });
46 |
--------------------------------------------------------------------------------
/packages/core/src/links/cacheUpdaterLink.ts:
--------------------------------------------------------------------------------
1 | import { ApolloLink, Operation } from '@apollo/client';
2 | import { visit } from 'graphql/language';
3 | import { uniq } from 'lodash';
4 | import { CACHE_UPDATER_DIRECTIVE_NAMES, DELETE_VARIABLES_DIRECTIVE_NAMES } from '../utils';
5 | import { isQueryOperation } from '../utils/graphqlAST';
6 | import { createApolloLink } from './utils';
7 |
8 | const transform = (operation: Operation): Operation => {
9 | const input = operation.query;
10 | if (isQueryOperation(input)) return operation;
11 |
12 | let argumentNames: string[] = [];
13 | visit(input, {
14 | Directive: {
15 | enter(node) {
16 | if (
17 | DELETE_VARIABLES_DIRECTIVE_NAMES.find((name) => name === node.name.value) &&
18 | node.arguments &&
19 | node.arguments.length > 0
20 | ) {
21 | argumentNames = [...argumentNames, ...node.arguments.map((m) => m.name.value)];
22 | }
23 | },
24 | },
25 | });
26 |
27 | argumentNames = uniq(argumentNames);
28 |
29 | operation.query = visit(input, {
30 | VariableDefinition: {
31 | enter(node) {
32 | if (argumentNames.includes(node.variable.name.value)) {
33 | return null;
34 | }
35 | },
36 | },
37 | Directive: {
38 | enter(node) {
39 | if (CACHE_UPDATER_DIRECTIVE_NAMES.find((name) => name === node.name.value)) {
40 | return null;
41 | }
42 | },
43 | },
44 | });
45 |
46 | operation.variables = Object.fromEntries(
47 | Object.entries(operation.variables).filter(([key]) => !argumentNames.includes(key)),
48 | );
49 |
50 | return operation;
51 | };
52 |
53 | export const createCacheUpdaterLink = (): ApolloLink => {
54 | return createApolloLink((operation) => transform(operation));
55 | };
56 |
--------------------------------------------------------------------------------
/packages/core/src/links/index.ts:
--------------------------------------------------------------------------------
1 | export { createCacheUpdaterLink } from './cacheUpdaterLink';
2 | export * from './utils';
3 |
--------------------------------------------------------------------------------
/packages/core/src/links/utils.ts:
--------------------------------------------------------------------------------
1 | import { ApolloLink, FetchResult, Operation } from '@apollo/client';
2 | import { Observable } from '@apollo/client/utilities';
3 | import { isSubscriptionOperation } from '../utils';
4 |
5 | export const createApolloLink = (transform: (operation: Operation) => Operation): ApolloLink => {
6 | return new ApolloLink((operation, forward) => {
7 | if (!forward) return null;
8 |
9 | const newOperation = transform(operation);
10 | if (isSubscriptionOperation(newOperation.query)) {
11 | return new Observable((observer) =>
12 | forward(newOperation).subscribe((response) => observer.next(response)),
13 | );
14 | }
15 |
16 | return forward(newOperation).map((response) => response);
17 | });
18 | };
19 |
--------------------------------------------------------------------------------
/packages/core/src/policies/cacheUpdater/__tests__/deleteRecord.mock.ts:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client';
2 | import { relayStylePagination } from '@apollo/client/utilities';
3 | import { backendNodeIdGenerator } from '../../../utils/testing/backendNodeIdGenerator';
4 | import { withCacheUpdaterInternal } from '../withCacheUpdater';
5 |
6 | type ItemsConnectionType = {
7 | _connectionId: string;
8 | edges: { node: { id: string; __typename: 'Item' }; cursor: string }[];
9 | pageInfo: { hasNextPage?: boolean; hasPreviousPage?: boolean; endCursor?: string; startCursor?: string };
10 | };
11 |
12 | export type QueryDataType = {
13 | items1: ItemsConnectionType;
14 | viewer: {
15 | id: string;
16 | __typename: 'User';
17 | items2: ItemsConnectionType;
18 | };
19 | };
20 |
21 | const paginationMetaList = [
22 | {
23 | node: {
24 | typename: 'Item',
25 | },
26 | parents: [
27 | {
28 | typename: 'Query',
29 | connection: {
30 | fieldName: 'items',
31 | },
32 | edge: {
33 | typename: 'ItemEdge',
34 | },
35 | },
36 | {
37 | typename: 'User',
38 | connection: {
39 | fieldName: 'items',
40 | },
41 | edge: {
42 | typename: 'ItemEdge',
43 | },
44 | },
45 | ],
46 | },
47 | ];
48 |
49 | const deleteRecordMetaList = [
50 | { parent: { typename: 'RemovedItem' }, fields: [{ fieldName: 'id', typename: 'Item' }] },
51 | { parent: { typename: 'ItemRemovedPayload' }, fields: [{ fieldName: 'id', typename: 'Item' }] },
52 | ];
53 | export const testTypePolicies = withCacheUpdaterInternal({
54 | paginationMetaList,
55 | deleteRecordMetaList,
56 | typePolicies: {
57 | Query: {
58 | fields: {
59 | items: relayStylePagination(),
60 | },
61 | },
62 | User: {
63 | fields: {
64 | items: relayStylePagination(),
65 | },
66 | },
67 | },
68 | });
69 |
70 | export const userId = backendNodeIdGenerator({ typename: 'User', localId: '1' });
71 | export const item1Id = backendNodeIdGenerator({ typename: 'Item', localId: '1' });
72 | export const item2Id = backendNodeIdGenerator({ typename: 'Item', localId: '2' });
73 |
74 | export const queryDocument = gql`
75 | query TestQuery($cursor1: String, $cursor2: String) {
76 | items1: items(first: 1, after: $cursor1) {
77 | _connectionId @client
78 | edges {
79 | node {
80 | id
81 | __typename
82 | }
83 | cursor
84 | }
85 | pageInfo {
86 | hasNextPage
87 | endCursor
88 | }
89 | }
90 | viewer {
91 | id
92 | __typename
93 | items2: items(first: 2, after: $cursor2) {
94 | _connectionId @client
95 | edges {
96 | node {
97 | id
98 | __typename
99 | }
100 | cursor
101 | }
102 | pageInfo {
103 | hasNextPage
104 | endCursor
105 | }
106 | }
107 | }
108 | }
109 | `;
110 |
111 | export const queryMockData = {
112 | items1: {
113 | edges: [{ node: { id: item1Id, __typename: 'Item' }, cursor: 'cursor-1' }],
114 | pageInfo: { hasNextPage: true, endCursor: 'cursor-1' },
115 | },
116 | viewer: {
117 | id: userId,
118 | __typename: 'User',
119 | items2: {
120 | edges: [
121 | { node: { id: item1Id, __typename: 'Item' }, cursor: 'cursor-1' },
122 | { node: { id: item2Id, __typename: 'Item' }, cursor: 'cursor-2' },
123 | ],
124 | pageInfo: { hasNextPage: true, endCursor: 'cursor-1' },
125 | },
126 | },
127 | };
128 |
129 | export const mutationDocument = gql`
130 | mutation DeleteItemMutation($input: RemoveItemInput!) {
131 | removeItem(input: $input) {
132 | removedItem {
133 | id @deleteRecord(typename: "Item")
134 | __typename
135 | }
136 | }
137 | }
138 | `;
139 | export const mutationVariables = {
140 | input: {
141 | itemId: item1Id,
142 | userId: userId,
143 | },
144 | };
145 | export const mutationMockData = { removeItem: { removedItem: { id: item1Id, __typename: 'RemovedItem' } } };
146 |
147 | export const subscriptionDocument = gql`
148 | subscription ItemDeletedSubscription($connections: [String!]!) {
149 | itemRemoved {
150 | id @deleteRecord(typename: "Item")
151 | __typename
152 | }
153 | }
154 | `;
155 | export const subscriptionMockData = { itemRemoved: { id: item1Id, __typename: 'ItemRemovedPayload' } };
156 |
--------------------------------------------------------------------------------
/packages/core/src/policies/cacheUpdater/__tests__/deleteRecord.test.ts:
--------------------------------------------------------------------------------
1 | import { ApolloClient, InMemoryCache, useMutation, useQuery, useSubscription } from '@apollo/client';
2 | import { MockSubscriptionLink } from '@apollo/client/testing';
3 | import '@testing-library/jest-dom';
4 | import { act, waitFor } from '@testing-library/react';
5 | import { renderHook } from '@testing-library/react';
6 | import { clientMockedWrapperComponent, mockedWrapperComponent } from '../../../utils/testing/mockedWrapperComponent';
7 | import {
8 | item2Id,
9 | mutationDocument,
10 | mutationMockData,
11 | mutationVariables,
12 | QueryDataType,
13 | queryDocument,
14 | queryMockData,
15 | subscriptionDocument,
16 | subscriptionMockData,
17 | testTypePolicies,
18 | } from './deleteRecord.mock';
19 |
20 | describe('@deleteRecord', () => {
21 | let cache: InMemoryCache;
22 | beforeEach(() => {
23 | cache = new InMemoryCache({
24 | typePolicies: testTypePolicies,
25 | });
26 | });
27 |
28 | it('deletes the mutation result from the cache', async () => {
29 | const mocks = [
30 | { request: { query: queryDocument }, result: { data: queryMockData } },
31 | { request: { query: mutationDocument, variables: mutationVariables }, result: { data: mutationMockData } },
32 | ];
33 |
34 | const wrapper = mockedWrapperComponent({ mocks, cache });
35 |
36 | const useQueryHookResult = renderHook(() => useQuery(queryDocument), { wrapper });
37 | await waitFor(() => {
38 | expect(useQueryHookResult.result.current.data?.items1.edges.length).toBe(1);
39 | });
40 | expect(useQueryHookResult.result.current.data?.viewer.items2.edges.length).toBe(2);
41 | expect(useQueryHookResult.result.current.data).toMatchObject(queryMockData);
42 |
43 | const useMutationHookResult = renderHook(() => useMutation(mutationDocument), { wrapper });
44 | act(() => {
45 | const [deleteFunc] = useMutationHookResult.result.current;
46 | void deleteFunc({ variables: mutationVariables });
47 | });
48 |
49 | await waitFor(() => {
50 | expect(useQueryHookResult.result.current.data?.items1.edges.length).toBe(0);
51 | });
52 | expect(useQueryHookResult.result.current.data?.items1.edges.length).toBe(0);
53 | expect(useQueryHookResult.result.current.data?.viewer.items2.edges.length).toBe(1);
54 | expect(useQueryHookResult.result.current.data).toMatchObject({
55 | items1: {
56 | edges: [],
57 | },
58 | viewer: {
59 | items2: {
60 | edges: [{ node: { id: item2Id, __typename: 'Item' }, cursor: 'cursor-2' }],
61 | },
62 | },
63 | });
64 | });
65 |
66 | it('deletes the subscription result from the cache', async () => {
67 | const mocks = [{ request: { query: queryDocument }, result: { data: queryMockData } }];
68 | const wrapper = mockedWrapperComponent({ mocks, cache });
69 | const useQueryHookResult = renderHook(() => useQuery(queryDocument), { wrapper });
70 | await waitFor(() => {
71 | expect(useQueryHookResult.result.current.data?.items1.edges.length).toBe(1);
72 | });
73 | expect(useQueryHookResult.result.current.data?.items1.edges.length).toBe(1);
74 | expect(useQueryHookResult.result.current.data?.viewer.items2.edges.length).toBe(2);
75 | expect(useQueryHookResult.result.current.data).toMatchObject(queryMockData);
76 |
77 | const link = new MockSubscriptionLink();
78 | const client = new ApolloClient({ link, cache });
79 | const subscriptionWrapper = clientMockedWrapperComponent({ client });
80 | renderHook(() => useSubscription(subscriptionDocument), {
81 | wrapper: subscriptionWrapper,
82 | });
83 | act(() => {
84 | link.simulateResult({ result: { data: subscriptionMockData } }, true);
85 | });
86 | await waitFor(() => {
87 | expect(useQueryHookResult.result.current.data?.items1.edges.length).toBe(0);
88 | });
89 | expect(useQueryHookResult.result.current.data?.viewer.items2.edges.length).toBe(1);
90 | expect(useQueryHookResult.result.current.data).toMatchObject({
91 | items1: {
92 | edges: [],
93 | },
94 | viewer: {
95 | items2: {
96 | edges: [{ node: { id: item2Id, __typename: 'Item' }, cursor: 'cursor-2' }],
97 | },
98 | },
99 | });
100 | });
101 | });
102 |
--------------------------------------------------------------------------------
/packages/core/src/policies/cacheUpdater/__tests__/pagination/appendNode.mock.ts:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client';
2 | import { generateConnectionId } from '../../pagination';
3 | import { item11Id, item12Id, item21Id, userId } from './mock';
4 |
5 | export const mutationDocument = gql`
6 | mutation AddItemMutation($itemName: String!, $userId: ID!, $connections: [String!]!) {
7 | addItem(input: { itemName: $itemName, userId: $userId }) {
8 | item @appendNode(connections: $connections) {
9 | id
10 | __typename
11 | }
12 | }
13 | }
14 | `;
15 |
16 | export const mutationMockData = { addItem: { item: { id: item11Id, __typename: 'Item' } } };
17 |
18 | export const mutationVariables = {
19 | itemName: 'item11',
20 | userId: userId,
21 | connections: [
22 | generateConnectionId({
23 | parent: { id: userId, typename: 'User' },
24 | connection: { fieldName: 'items', args: {} },
25 | edge: { typename: 'ItemEdge' },
26 | }),
27 | ],
28 | };
29 |
30 | export const differentArgsMutationMockData1 = {
31 | addItem: { item: { id: item11Id, __typename: 'Item', name: 'Item11' } },
32 | };
33 |
34 | export const differentArgsMutationVariables1 = {
35 | itemName: 'item11',
36 | userId: userId,
37 | connections: [
38 | generateConnectionId({
39 | parent: { id: userId, typename: 'User' },
40 | connection: { fieldName: 'items', args: { search: '1' } },
41 | edge: { typename: 'ItemEdge' },
42 | }),
43 | ],
44 | };
45 |
46 | export const differentArgsMutationMockData2 = {
47 | addItem: { item: { id: item12Id, __typename: 'Item', name: 'Item12' } },
48 | };
49 |
50 | export const differentArgsMutationVariables2 = {
51 | itemName: 'item12',
52 | userId: userId,
53 | connections: [
54 | generateConnectionId({
55 | parent: { id: userId, typename: 'User' },
56 | connection: { fieldName: 'items', args: { search: '2' } },
57 | edge: { typename: 'ItemEdge' },
58 | }),
59 | ],
60 | };
61 |
62 | export const appendItemToRootMutationVariables = {
63 | itemName: 'item11',
64 | userId: userId,
65 | connections: [
66 | generateConnectionId({
67 | parent: { id: 'ROOT_QUERY', typename: 'Query' },
68 | connection: { fieldName: 'items', args: {} },
69 | edge: { typename: 'ItemEdge' },
70 | }),
71 | ],
72 | };
73 |
74 | export const subscriptionDocument = gql`
75 | subscription ItemAddedSubscription($connections: [String!]!) {
76 | itemAdded {
77 | item @appendNode(connections: $connections) {
78 | id
79 | __typename
80 | }
81 | }
82 | }
83 | `;
84 |
85 | export const subscriptionMockData = { itemAdded: { item: { id: item21Id, __typename: 'Item' } } };
86 |
--------------------------------------------------------------------------------
/packages/core/src/policies/cacheUpdater/__tests__/pagination/connectionId.mock.ts:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client';
2 | import { userId } from './mock';
3 |
4 | export type ConnectionIdOnlyQueryDataType = {
5 | items: { _connectionId: string };
6 | viewer: {
7 | id: string;
8 | __typename: 'User';
9 | items: { _connectionId: string };
10 | };
11 | };
12 |
13 | export const connectionIdOnlyQueryDocument = gql`
14 | query TestQuery($cursor: String) {
15 | items(first: 1, after: $cursor) {
16 | _connectionId @client
17 | }
18 | viewer {
19 | id
20 | __typename
21 | items(first: 1, after: $cursor) {
22 | _connectionId @client
23 | }
24 | }
25 | }
26 | `;
27 |
28 | export const connectionIdOnlyQueryMockData = {
29 | items: {},
30 | viewer: {
31 | id: userId,
32 | __typename: 'User',
33 | items: {},
34 | },
35 | };
36 |
--------------------------------------------------------------------------------
/packages/core/src/policies/cacheUpdater/__tests__/pagination/connectionId.test.ts:
--------------------------------------------------------------------------------
1 | import { InMemoryCache, useQuery } from '@apollo/client';
2 | import '@testing-library/jest-dom';
3 | import { renderHook, waitFor } from '@testing-library/react';
4 | import { mockedWrapperComponent } from '../../../../utils/testing/mockedWrapperComponent';
5 | import { generateConnectionId } from '../../pagination';
6 | import {
7 | ConnectionIdOnlyQueryDataType,
8 | connectionIdOnlyQueryDocument,
9 | connectionIdOnlyQueryMockData,
10 | } from './connectionId.mock';
11 | import { QueryDataType, queryDocument, queryMockData, testTypePolicies, userId } from './mock';
12 |
13 | describe('_connectionId', () => {
14 | let cache: InMemoryCache;
15 | beforeEach(() => {
16 | cache = new InMemoryCache({
17 | typePolicies: testTypePolicies,
18 | });
19 | });
20 | it('gets _connectionId of the connection of Root Query', async () => {
21 | const mocks = [{ request: { query: queryDocument }, result: { data: queryMockData } }];
22 | const wrapper = mockedWrapperComponent({ mocks, cache });
23 | const useQueryHookResult = renderHook(() => useQuery(queryDocument), { wrapper });
24 | const connectionId = generateConnectionId({
25 | parent: { id: 'ROOT_QUERY', typename: 'Query' },
26 | connection: { fieldName: 'items', args: {} },
27 | edge: { typename: 'ItemEdge' },
28 | });
29 | await waitFor(() => {
30 | expect(useQueryHookResult.result.current.data?.items._connectionId).toBe(connectionId);
31 | });
32 | });
33 |
34 | it('gets _connectionId of the connection of a type', async () => {
35 | const mocks = [{ request: { query: queryDocument }, result: { data: queryMockData } }];
36 | const wrapper = mockedWrapperComponent({ mocks, cache });
37 | const useQueryHookResult = renderHook(() => useQuery(queryDocument), { wrapper });
38 | const connectionId = generateConnectionId({
39 | parent: { id: userId, typename: 'User' },
40 | connection: { fieldName: 'items', args: { search: 'item' } },
41 | edge: { typename: 'ItemEdge' },
42 | });
43 | await waitFor(() => {
44 | expect(useQueryHookResult.result.current.data?.viewer.myItems._connectionId).toBe(connectionId);
45 | });
46 | });
47 |
48 | it('gets _connectionId of the connection with no edges etc. ', async () => {
49 | const mocks = [
50 | { request: { query: connectionIdOnlyQueryDocument }, result: { data: connectionIdOnlyQueryMockData } },
51 | ];
52 | const wrapper = mockedWrapperComponent({ mocks, cache });
53 | const useQueryHookResult = renderHook(
54 | () => useQuery(connectionIdOnlyQueryDocument),
55 | { wrapper },
56 | );
57 | const connectionId1 = generateConnectionId({
58 | parent: { id: 'ROOT_QUERY', typename: 'Query' },
59 | connection: { fieldName: 'items', args: {} },
60 | edge: { typename: 'ItemEdge' },
61 | });
62 | const connectionId2 = generateConnectionId({
63 | parent: { id: userId, typename: 'User' },
64 | connection: { fieldName: 'items', args: {} },
65 | edge: { typename: 'ItemEdge' },
66 | });
67 | await waitFor(() => {
68 | expect(useQueryHookResult.result.current.data?.items._connectionId).toBe(connectionId1);
69 | });
70 | expect(useQueryHookResult.result.current.data?.items._connectionId).toBe(connectionId1);
71 | expect(useQueryHookResult.result.current.data?.viewer.items._connectionId).toBe(connectionId2);
72 | });
73 | });
74 |
--------------------------------------------------------------------------------
/packages/core/src/policies/cacheUpdater/__tests__/pagination/mock.ts:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client';
2 | import { relayStylePagination } from '@apollo/client/utilities';
3 | import { backendNodeIdGenerator } from '../../../../utils/testing/backendNodeIdGenerator';
4 | import { withCacheUpdaterInternal } from '../../withCacheUpdater';
5 |
6 | type ItemsConnectionType = {
7 | _connectionId: string;
8 | edges: { node: { id: string; __typename: 'Item' }; cursor: string }[];
9 | pageInfo: { hasNextPage?: boolean; hasPreviousPage?: boolean; endCursor?: string; startCursor?: string };
10 | };
11 |
12 | export type QueryDataType = {
13 | items: ItemsConnectionType;
14 | viewer: {
15 | id: string;
16 | __typename: 'User';
17 | myItems: ItemsConnectionType;
18 | };
19 | };
20 |
21 | export type DifferentArgsConnectionsQueryDataType = {
22 | viewer: {
23 | id: string;
24 | __typename: 'User';
25 | items1: ItemsConnectionType;
26 | items2: ItemsConnectionType;
27 | };
28 | };
29 |
30 | const paginationMetaList = [
31 | {
32 | node: {
33 | typename: 'Item',
34 | },
35 | parents: [
36 | {
37 | typename: 'Query',
38 | connection: {
39 | fieldName: 'items',
40 | },
41 | edge: {
42 | typename: 'ItemEdge',
43 | },
44 | },
45 | {
46 | typename: 'User',
47 | connection: {
48 | fieldName: 'items',
49 | },
50 | edge: {
51 | typename: 'ItemEdge',
52 | },
53 | },
54 | ],
55 | },
56 | ];
57 |
58 | export const testTypePolicies = withCacheUpdaterInternal({
59 | paginationMetaList,
60 | deleteRecordMetaList: [],
61 | typePolicies: {
62 | Query: {
63 | fields: {
64 | items: relayStylePagination(),
65 | },
66 | },
67 | User: {
68 | fields: {
69 | items: relayStylePagination(['search']),
70 | },
71 | },
72 | },
73 | });
74 |
75 | export const userId = backendNodeIdGenerator({ typename: 'User', localId: '1' });
76 | // Use query
77 | export const item1Id = backendNodeIdGenerator({ typename: 'Item', localId: '1' });
78 | export const item2Id = backendNodeIdGenerator({ typename: 'Item', localId: '2' });
79 | // Use mutation
80 | export const item11Id = backendNodeIdGenerator({ typename: 'Item', localId: '11' });
81 | export const item12Id = backendNodeIdGenerator({ typename: 'Item', localId: '12' });
82 | // Use subscription
83 | export const item21Id = backendNodeIdGenerator({ typename: 'Item', localId: '21' });
84 |
85 | export const queryDocument = gql`
86 | query TestQuery($cursor: String) {
87 | items {
88 | _connectionId @client
89 | edges {
90 | node {
91 | id
92 | __typename
93 | }
94 | cursor
95 | }
96 | pageInfo {
97 | hasNextPage
98 | endCursor
99 | }
100 | }
101 | viewer {
102 | id
103 | __typename
104 | myItems: items(first: 1, after: $cursor, search: "item") {
105 | _connectionId @client
106 | edges {
107 | node {
108 | id
109 | __typename
110 | }
111 | cursor
112 | }
113 | pageInfo {
114 | hasNextPage
115 | endCursor
116 | }
117 | }
118 | }
119 | }
120 | `;
121 |
122 | export const queryMockData = {
123 | items: {
124 | edges: [{ node: { id: item1Id, __typename: 'Item' }, cursor: 'cursor-1' }],
125 | pageInfo: { hasNextPage: true, endCursor: 'cursor-1' },
126 | },
127 | viewer: {
128 | id: userId,
129 | __typename: 'User',
130 | myItems: {
131 | edges: [{ node: { id: item1Id, __typename: 'Item' }, cursor: 'cursor-1' }],
132 | pageInfo: { hasNextPage: true, endCursor: 'cursor-1' },
133 | },
134 | },
135 | };
136 |
137 | export const differentArgsConnectionsQueryDocument = gql`
138 | query TestQuery($cursor: String) {
139 | viewer {
140 | id
141 | __typename
142 | items1: items(first: 1, after: $cursor, search: "1") {
143 | _connectionId @client
144 | edges {
145 | node {
146 | id
147 | __typename
148 | }
149 | cursor
150 | }
151 | pageInfo {
152 | hasNextPage
153 | endCursor
154 | }
155 | }
156 | items2: items(first: 1, after: $cursor, search: "2") {
157 | _connectionId @client
158 | edges {
159 | node {
160 | id
161 | __typename
162 | }
163 | cursor
164 | }
165 | pageInfo {
166 | hasNextPage
167 | endCursor
168 | }
169 | }
170 | }
171 | }
172 | `;
173 | export const differentArgsConnectionsQueryMockData = {
174 | viewer: {
175 | id: userId,
176 | __typename: 'User',
177 | items1: {
178 | edges: [{ node: { id: item1Id, __typename: 'Item' }, cursor: 'cursor-1' }],
179 | pageInfo: { hasNextPage: true, endCursor: 'cursor-1' },
180 | },
181 | items2: {
182 | edges: [{ node: { id: item2Id, __typename: 'Item' }, cursor: 'cursor-2' }],
183 | pageInfo: { hasNextPage: true, endCursor: 'cursor-2' },
184 | },
185 | },
186 | };
187 |
188 | export const prependItemMutationDocument = gql`
189 | mutation AddItemMutation($itemName: String!, $userId: ID!, $connections: [String!]!) {
190 | addItem(input: { itemName: $itemName, userId: $userId }) {
191 | item @prependNode(connections: $connections) {
192 | id
193 | __typename
194 | }
195 | }
196 | }
197 | `;
198 | export const prependItemMutationMockData = { addItem: { item: { id: item11Id, __typename: 'Item' } } };
199 |
--------------------------------------------------------------------------------
/packages/core/src/policies/cacheUpdater/__tests__/pagination/prependNode.mock.ts:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client';
2 | import { generateConnectionId } from '../../pagination';
3 | import { item11Id, item12Id, item21Id, userId } from './mock';
4 |
5 | export const mutationDocument = gql`
6 | mutation AddItemMutation($itemName: String!, $userId: ID!, $connections: [String!]!) {
7 | addItem(input: { itemName: $itemName, userId: $userId }) {
8 | item @prependNode(connections: $connections) {
9 | id
10 | __typename
11 | }
12 | }
13 | }
14 | `;
15 |
16 | export const mutationMockData = { addItem: { item: { id: item11Id, __typename: 'Item' } } };
17 |
18 | export const mutationVariables = {
19 | itemName: 'item11',
20 | userId: userId,
21 | connections: [
22 | generateConnectionId({
23 | parent: { id: userId, typename: 'User' },
24 | connection: { fieldName: 'items', args: {} },
25 | edge: { typename: 'ItemEdge' },
26 | }),
27 | ],
28 | };
29 |
30 | export const differentArgsMutationMockData1 = {
31 | addItem: { item: { id: item11Id, __typename: 'Item', name: 'Item11' } },
32 | };
33 |
34 | export const differentArgsMutationVariables1 = {
35 | itemName: 'item11',
36 | userId: userId,
37 | connections: [
38 | generateConnectionId({
39 | parent: { id: userId, typename: 'User' },
40 | connection: { fieldName: 'items', args: { search: '1' } },
41 | edge: { typename: 'ItemEdge' },
42 | }),
43 | ],
44 | };
45 |
46 | export const differentArgsMutationMockData2 = {
47 | addItem: { item: { id: item12Id, __typename: 'Item', name: 'Item12' } },
48 | };
49 |
50 | export const differentArgsMutationVariables2 = {
51 | itemName: 'item12',
52 | userId: userId,
53 | connections: [
54 | generateConnectionId({
55 | parent: { id: userId, typename: 'User' },
56 | connection: { fieldName: 'items', args: { search: '2' } },
57 | edge: { typename: 'ItemEdge' },
58 | }),
59 | ],
60 | };
61 |
62 | export const prependItemToRootMutationVariables = {
63 | itemName: 'item11',
64 | userId: userId,
65 | connections: [
66 | generateConnectionId({
67 | parent: { id: 'ROOT_QUERY', typename: 'Query' },
68 | connection: { fieldName: 'items', args: {} },
69 | edge: { typename: 'ItemEdge' },
70 | }),
71 | ],
72 | };
73 |
74 | export const subscriptionDocument = gql`
75 | subscription ItemAddedSubscription($connections: [String!]!) {
76 | itemAdded {
77 | item @prependNode(connections: $connections) {
78 | id
79 | __typename
80 | }
81 | }
82 | }
83 | `;
84 |
85 | export const subscriptionMockData = { itemAdded: { item: { id: item21Id, __typename: 'Item' } } };
86 |
--------------------------------------------------------------------------------
/packages/core/src/policies/cacheUpdater/deleteRecord.ts:
--------------------------------------------------------------------------------
1 | import { FieldFunctionOptions, Reference, TypePolicies, TypePolicy } from '@apollo/client';
2 | import { FieldNode } from 'graphql';
3 | import { findDirectiveName } from '../../utils/directiveName';
4 | import { generateTypePolicyPairWithTypeMergeFunction, TypePolicyPair } from './util';
5 |
6 | export type DeleteRecordMeta = {
7 | parent: {
8 | typename: string;
9 | };
10 | fields: { fieldName: string; typename: string }[];
11 | };
12 |
13 | export const generateDeleteRecordTypePolicyPairs = ({
14 | deleteRecordMetaList,
15 | typePolicies,
16 | }: {
17 | deleteRecordMetaList: DeleteRecordMeta[];
18 | typePolicies: TypePolicies;
19 | }): [string, TypePolicy][] => {
20 | return deleteRecordMetaList.map((metadata): TypePolicyPair => {
21 | return generateTypePolicyPairWithTypeMergeFunction({
22 | innerFunction: ({ mergedObject, options }) => deleteRecord({ object: mergedObject, options, metadata }),
23 | typename: metadata.parent.typename,
24 | typePolicies,
25 | });
26 | });
27 | };
28 |
29 | const deleteRecord = ({
30 | object,
31 | options,
32 | metadata,
33 | }: {
34 | object: Reference;
35 | options: FieldFunctionOptions;
36 | metadata: DeleteRecordMeta;
37 | }): void => {
38 | const { cache, field, readField } = options;
39 |
40 | const fieldsWithDeleteRecord =
41 | field?.selectionSet?.selections
42 | .filter(
43 | (selection): selection is FieldNode =>
44 | !!findDirectiveName({ node: selection, directiveNames: ['deleteRecord'] }) && selection.kind === 'Field',
45 | )
46 | .map((selection) => selection.name.value) || [];
47 |
48 | if (fieldsWithDeleteRecord.length === 0) return;
49 |
50 | metadata.fields.forEach((fieldMeta) => {
51 | if (!fieldsWithDeleteRecord.includes(fieldMeta.fieldName)) return;
52 | const id = readField({ fieldName: fieldMeta.fieldName, from: object });
53 | if (typeof id != 'string') return;
54 | const cacheId = cache.identify({ id, __typename: fieldMeta.typename });
55 | cache.evict({ id: cacheId });
56 | });
57 |
58 | cache.gc();
59 | };
60 |
--------------------------------------------------------------------------------
/packages/core/src/policies/cacheUpdater/index.ts:
--------------------------------------------------------------------------------
1 | export { DeleteRecordMeta } from './deleteRecord';
2 | export { PaginationMeta } from './pagination';
3 | export * from './withCacheUpdater';
4 |
--------------------------------------------------------------------------------
/packages/core/src/policies/cacheUpdater/util.ts:
--------------------------------------------------------------------------------
1 | import { FieldFunctionOptions, FieldMergeFunction, Reference, TypePolicies, TypePolicy } from '@apollo/client';
2 |
3 | export type TypePolicyPair = [string, TypePolicy];
4 |
5 | export const findTypePolicy = ({
6 | key,
7 | typePolicies,
8 | }: {
9 | key: string;
10 | typePolicies: TypePolicies;
11 | }): TypePolicy | undefined => {
12 | const keys = Object.keys(typePolicies);
13 | if (keys.includes(key)) {
14 | return typePolicies[key];
15 | }
16 | return undefined;
17 | };
18 |
19 | export const generateTypePolicyPairWithTypeMergeFunction = ({
20 | innerFunction,
21 | typename,
22 | typePolicies,
23 | }: {
24 | innerFunction: (params: { mergedObject: Reference; incoming: Reference; options: FieldFunctionOptions }) => void;
25 | typename: string;
26 | typePolicies: TypePolicies;
27 | }): TypePolicyPair => {
28 | const originalTypePolicy = findTypePolicy({ key: typename, typePolicies });
29 |
30 | if (!originalTypePolicy) {
31 | return [
32 | typename,
33 | {
34 | merge: (existing, incoming, options) => {
35 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
36 | const object = options.mergeObjects(existing, incoming);
37 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
38 | innerFunction({ mergedObject: object, incoming, options });
39 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return
40 | return object;
41 | },
42 | },
43 | ];
44 | }
45 |
46 | const typePolicy: TypePolicy = {
47 | ...originalTypePolicy,
48 | merge: (existing: Reference, incoming: Reference, options) => {
49 | const object = options.mergeObjects(existing, incoming);
50 | if (object) innerFunction({ mergedObject: object, incoming, options });
51 | return callMerge({ merge: originalTypePolicy.merge, mergedObject: object, incoming, options });
52 | },
53 | };
54 | return [typename, typePolicy];
55 | };
56 |
57 | const callMerge = ({
58 | merge,
59 | mergedObject,
60 | incoming,
61 | options,
62 | }: {
63 | merge?: FieldMergeFunction | boolean;
64 | mergedObject: Reference;
65 | incoming: Reference;
66 | options: FieldFunctionOptions;
67 | }): Reference => {
68 | if (merge === false) {
69 | return incoming;
70 | } else if (merge === true || !merge) {
71 | return mergedObject;
72 | } else {
73 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return
74 | return merge(mergedObject, incoming, options);
75 | }
76 | };
77 |
--------------------------------------------------------------------------------
/packages/core/src/policies/cacheUpdater/withCacheUpdater.ts:
--------------------------------------------------------------------------------
1 | import { TypePolicies } from '@apollo/client';
2 | import { uniqWith } from 'lodash';
3 | import { DeleteRecordMeta, generateDeleteRecordTypePolicyPairs } from './deleteRecord';
4 | import {
5 | generatePaginationNodeTypePolicyPairs,
6 | generatePaginationParentTypePolicyPairs,
7 | PaginationMeta,
8 | } from './pagination';
9 | import { TypePolicyPair } from './util';
10 |
11 | const mergeTypePolicyPairs = (pairsList: Array): TypePolicyPair[] => {
12 | const allTypename = uniqWith(
13 | pairsList.flat().map(([key, _]) => key),
14 | (a, b) => a === b,
15 | );
16 | return allTypename.map((typename) => {
17 | const pairs = pairsList.flat().filter((pair) => pair[0] === typename);
18 | let newTypePolicy = {};
19 | pairs.forEach((pair) => {
20 | newTypePolicy = { ...newTypePolicy, ...pair[1] };
21 | });
22 | return [typename, newTypePolicy];
23 | });
24 | };
25 |
26 | const typePoliciesFromTypePolicyPairs = (typePolicyPairs: TypePolicyPair[]): TypePolicies => {
27 | return Object.fromEntries(typePolicyPairs);
28 | };
29 |
30 | // NOTE: If you want to remove an edge from an edges of a connection when using @deleteRecord,
31 | // you must set the information of that connection to `paginationMetaList`.
32 | // This means that the connection requires the @pagination directive.
33 | export const withCacheUpdaterInternal = ({
34 | paginationMetaList,
35 | deleteRecordMetaList,
36 | typePolicies,
37 | }: {
38 | paginationMetaList: PaginationMeta[];
39 | deleteRecordMetaList: DeleteRecordMeta[];
40 | typePolicies: TypePolicies;
41 | }): TypePolicies => {
42 | const paginationParentTypePolicyPairs = generatePaginationParentTypePolicyPairs({ paginationMetaList, typePolicies });
43 | const paginationNodeTypePolicyPairs = generatePaginationNodeTypePolicyPairs({ paginationMetaList, typePolicies });
44 | const paginationTypePolicyPairs = mergeTypePolicyPairs([
45 | paginationParentTypePolicyPairs,
46 | paginationNodeTypePolicyPairs,
47 | ]);
48 |
49 | let newTypePolicies = {
50 | ...typePolicies,
51 | ...typePoliciesFromTypePolicyPairs(paginationTypePolicyPairs),
52 | };
53 |
54 | const deleteRecordTypePolicyPairs = generateDeleteRecordTypePolicyPairs({
55 | deleteRecordMetaList,
56 | typePolicies: newTypePolicies,
57 | });
58 |
59 | newTypePolicies = { ...newTypePolicies, ...typePoliciesFromTypePolicyPairs(deleteRecordTypePolicyPairs) };
60 |
61 | return newTypePolicies;
62 | };
63 |
--------------------------------------------------------------------------------
/packages/core/src/policies/index.ts:
--------------------------------------------------------------------------------
1 | export * from './cacheUpdater';
2 |
--------------------------------------------------------------------------------
/packages/core/src/utils/directiveName.ts:
--------------------------------------------------------------------------------
1 | import { FieldNode, SelectionNode } from 'graphql/language';
2 |
3 | export type DirectiveName =
4 | | 'appendNode'
5 | | 'prependNode'
6 | | 'deleteRecord'
7 | | 'argumentDefinitions'
8 | | 'arguments'
9 | | 'refetchable'
10 | | 'pagination';
11 |
12 | export const ARGUMENT_DEFINITIONS_DIRECTIVE_NAME = 'argumentDefinitions';
13 | export const ARGUMENTS_DIRECTIVE_NAME = 'arguments';
14 | export const REFETCHABLE_DIRECTIVE_NAME = 'refetchable';
15 | export const PAGINATION_DIRECTIVE_NAME = 'pagination';
16 | export const DELETE_RECORD_DIRECTIVE_NAME = 'deleteRecord';
17 | export const INSERT_NODE_DIRECTIVE_NAMES: DirectiveName[] = ['appendNode', 'prependNode'];
18 | export const DELETE_VARIABLES_DIRECTIVE_NAMES: DirectiveName[] = [...INSERT_NODE_DIRECTIVE_NAMES];
19 | export const CACHE_UPDATER_DIRECTIVE_NAMES: DirectiveName[] = [
20 | ...INSERT_NODE_DIRECTIVE_NAMES,
21 | DELETE_RECORD_DIRECTIVE_NAME,
22 | ];
23 |
24 | export const findDirectiveName = ({
25 | node,
26 | directiveNames,
27 | }: {
28 | node: FieldNode | SelectionNode | null;
29 | directiveNames: DirectiveName[];
30 | }): DirectiveName | undefined => {
31 | const directiveName = node?.directives?.find((directive) =>
32 | directiveNames.find((name) => name === directive.name.value),
33 | )?.name.value;
34 | if (directiveName) return directiveName as DirectiveName;
35 | return undefined;
36 | };
37 |
--------------------------------------------------------------------------------
/packages/core/src/utils/getNodes.ts:
--------------------------------------------------------------------------------
1 | export const getNodesFromConnection = ({
2 | connection,
3 | }: {
4 | connection:
5 | | {
6 | edges?: Array<{ node?: TNode | null } | undefined | null> | null;
7 | }
8 | | undefined
9 | | null;
10 | }): TNode[] => {
11 | return connection?.edges?.map((edge) => edge?.node).filter((item): item is NonNullable => !!item) || [];
12 | };
13 |
--------------------------------------------------------------------------------
/packages/core/src/utils/graphqlAST.ts:
--------------------------------------------------------------------------------
1 | import { getMainDefinition } from '@apollo/client/utilities';
2 | import { DocumentNode } from 'graphql/language';
3 |
4 | export const isQueryOperation = (node: DocumentNode): boolean => {
5 | const mainDefinition = getMainDefinition(node);
6 | return mainDefinition.kind === 'OperationDefinition' && mainDefinition.operation === 'query';
7 | };
8 |
9 | export const isSubscriptionOperation = (query: DocumentNode): boolean => {
10 | const mainDefinition = getMainDefinition(query);
11 | return mainDefinition.kind === 'OperationDefinition' && mainDefinition.operation === 'subscription';
12 | };
13 |
--------------------------------------------------------------------------------
/packages/core/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './directiveName';
2 | export * from './getNodes';
3 | export * from './graphqlAST';
4 |
--------------------------------------------------------------------------------
/packages/core/src/utils/testing/backendNodeIdGenerator.ts:
--------------------------------------------------------------------------------
1 | import { encode } from 'js-base64';
2 | export const backendNodeIdGenerator = ({ typename, localId }: { typename: string; localId: string }): string =>
3 | encode(`${typename}|${localId}`);
4 |
--------------------------------------------------------------------------------
/packages/core/src/utils/testing/mockedWrapperComponent.tsx:
--------------------------------------------------------------------------------
1 | import { ApolloCache, ApolloClient, ApolloProvider, NormalizedCacheObject } from '@apollo/client';
2 | import { MockedProvider, MockedResponse } from '@apollo/client/testing';
3 | import '@testing-library/jest-dom';
4 | import * as React from 'react';
5 |
6 | export const mockedWrapperComponent = ({
7 | mocks,
8 | cache,
9 | }: {
10 | mocks: MockedResponse[];
11 | cache: ApolloCache>;
12 | }): React.FC<{ children: React.ReactChild }> => {
13 | return ({ children }: { children: React.ReactChild }) => (
14 |
15 | {children}
16 |
17 | );
18 | };
19 |
20 | export const clientMockedWrapperComponent = ({
21 | client,
22 | }: {
23 | client: ApolloClient;
24 | }): React.FC<{ children: React.ReactChild }> => {
25 | return ({ children }: { children: React.ReactChild }) => {children};
26 | };
27 |
--------------------------------------------------------------------------------
/packages/core/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "dist",
5 | "emitDeclarationOnly": true
6 | },
7 | "include": ["src/**/*"],
8 | "exclude": ["**/__tests__/", "**/*.test.ts", "**/testing/", "node_modules"]
9 | }
10 |
--------------------------------------------------------------------------------
/packages/graphql-codegen-preset/README.md:
--------------------------------------------------------------------------------
1 | # @kazekyo/nau-graphql-codegen-preset
2 |
3 | > A GraphQL Code Generator preset for [Nau](https://github.com/kazekyo/nau)
4 |
5 | See the [documentation](https://www.naugraphql.com/docs/introduction) for more information.
6 |
7 | ## Installation
8 |
9 | ```sh
10 | yarn add --dev @kazekyo/nau-graphql-codegen-preset
11 | ```
12 |
--------------------------------------------------------------------------------
/packages/graphql-codegen-preset/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@kazekyo/nau-graphql-codegen-preset",
3 | "version": "0.4.5",
4 | "description": "A GraphQL Code Generator preset for Nau",
5 | "author": "kazekyo",
6 | "bugs": {
7 | "url": "https://github.com/kazekyo/nau/issues"
8 | },
9 | "dependencies": {
10 | "@graphql-codegen/plugin-helpers": "^4.0.0",
11 | "@graphql-codegen/visitor-plugin-common": "^3.0.0",
12 | "@graphql-tools/utils": "^9.1.3",
13 | "@kazekyo/nau": "^0.4.4",
14 | "@kazekyo/nau-config": "^0.4.4",
15 | "@relay-graphql-js/validation-rules": "^0.1.0",
16 | "js-base64": "^3.6.0",
17 | "lodash.clonedeep": "^4.5.0"
18 | },
19 | "devDependencies": {
20 | "@apollo/client": "^3.6.2",
21 | "@graphql-codegen/cli": "^3.0.0",
22 | "@graphql-codegen/testing": "^1.17.7",
23 | "@types/lodash.clonedeep": "^4.5.6"
24 | },
25 | "files": [
26 | "dist"
27 | ],
28 | "homepage": "https://www.naugraphql.com",
29 | "keywords": [
30 | "apollo",
31 | "client",
32 | "relay",
33 | "graphql",
34 | "pagination",
35 | "cache",
36 | "directive"
37 | ],
38 | "license": "MIT",
39 | "main": "dist/index.js",
40 | "repository": {
41 | "type": "git",
42 | "url": "git+https://github.com/kazekyo/nau.git"
43 | },
44 | "scripts": {
45 | "test": "jest",
46 | "lint": "eslint src",
47 | "build": "yarn run build:types && yarn run build:js",
48 | "build:types": "tsc --build tsconfig.build.json",
49 | "build:js": "babel src --out-dir dist --root-mode upward --extensions \".ts,.tsx\" --source-maps inline --ignore \"**/__tests__/\",\"**/*.test.ts\",\"example\",\"**/testing/\""
50 | },
51 | "types": "dist/index.d.ts",
52 | "peerDependencies": {
53 | "graphql": "^15.6.1"
54 | },
55 | "gitHead": "6579f7231e41383ef50ade8820137e975b579047",
56 | "publishConfig": {
57 | "access": "public",
58 | "registry": "https://registry.npmjs.org/"
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/packages/graphql-codegen-preset/src/__tests__/__snapshots__/preset.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`preset generateTypeScriptCode outputs TypeScript code when the flag is true 1`] = `
4 | "import { TypePolicy } from '@apollo/client';
5 | import { withCacheUpdaterInternal } from '@kazekyo/nau';
6 |
7 | export const paginationMetaList = [{ node: { typename: 'Item' }, parents: [{ typename: 'User', connection: { fieldName: 'items' }, edge: { typename: 'ItemEdge' } }] }];
8 |
9 | export const deleteRecordMetaList = [{ parent: { typename: 'RemovedItem' }, fields: [{ fieldName: 'id', typename: 'Item' }] }, { parent: { typename: 'ItemRemovedPayload' }, fields: [{ fieldName: 'id', typename: 'Item' }] }];
10 |
11 | export type CacheUpdaterTypePolicies = {
12 | User: TypePolicy;
13 | [__typename: string]: TypePolicy;
14 | };
15 |
16 | export const withCacheUpdater = (typePolicies: CacheUpdaterTypePolicies) =>
17 | withCacheUpdaterInternal({
18 | paginationMetaList,
19 | deleteRecordMetaList,
20 | typePolicies,
21 | });"
22 | `;
23 |
--------------------------------------------------------------------------------
/packages/graphql-codegen-preset/src/__tests__/fixtures/exampleFile.ts:
--------------------------------------------------------------------------------
1 | import { gql } from '@apollo/client';
2 |
3 | const myFragment = gql`
4 | fragment MyFragment_user on User
5 | @refetchable(queryName: "App_PaginationQuery")
6 | @argumentDefinitions(count: { type: "Int", defaultValue: 5 }, cursor: { type: "String" }) {
7 | name
8 | items(first: $count, after: $cursor) @pagination {
9 | edges {
10 | node {
11 | id
12 | name
13 | }
14 | }
15 | }
16 | }
17 | `;
18 |
19 | gql`
20 | query MyAppQuery {
21 | viewer {
22 | id
23 | ...MyFragment_user @arguments(count: 5)
24 | }
25 | }
26 | ${myFragment}
27 | `;
28 |
29 | gql`
30 | mutation DeleteItemMutation($input: RemoveItemInput!) {
31 | removeItem(input: $input) {
32 | removedItem {
33 | id @deleteRecord(typename: "Item")
34 | __typename
35 | }
36 | }
37 | }
38 | `;
39 |
40 | gql`
41 | subscription ItemDeletedSubscription($connections: [String!]!) {
42 | itemRemoved {
43 | id @deleteRecord(typename: "Item")
44 | __typename
45 | }
46 | }
47 | `;
48 |
--------------------------------------------------------------------------------
/packages/graphql-codegen-preset/src/__tests__/preset.test.ts:
--------------------------------------------------------------------------------
1 | import { executeCodegen } from '@graphql-codegen/cli';
2 | import { Source } from '@graphql-tools/utils';
3 | import { parse } from 'graphql';
4 | import path from 'path';
5 | import { preset } from '../preset';
6 | import { printDocuments, testSchemaDocumentNode } from '../utils/testing/utils';
7 |
8 | describe('preset', () => {
9 | it('transforms documents', async () => {
10 | const documents: Source[] = [
11 | {
12 | document: parse(/* GraphQL */ `
13 | query TestQuery($cursor: String) {
14 | viewer {
15 | ...Fragment_user @arguments(count: 5)
16 | }
17 | }
18 | mutation AddItemMutation($input: AddItemInput!, $connections: [String!]!) {
19 | addItem(input: $input) {
20 | item @prependNode(connections: $connections) {
21 | name
22 | }
23 | }
24 | }
25 | subscription ItemAddedSubscription($connections: [String!]!) {
26 | itemAdded {
27 | item @prependNode(connections: $connections) {
28 | name
29 | }
30 | }
31 | }
32 | fragment Fragment_user on User @argumentDefinitions(count: { type: "Int", defaultValue: 1 }) {
33 | items(first: $count, after: $cursor) @pagination {
34 | edges {
35 | node {
36 | id
37 | }
38 | }
39 | }
40 | }
41 | `),
42 | },
43 | ];
44 | const expectedDocument = parse(/* GraphQL */ `
45 | query TestQuery($cursor: String) {
46 | viewer {
47 | ...Fragment_user_bi8xLGNvdW50OjU
48 | }
49 | }
50 | mutation AddItemMutation($input: AddItemInput!, $connections: [String!]!) {
51 | addItem(input: $input) {
52 | item @prependNode(connections: $connections) {
53 | name
54 | id
55 | __typename
56 | }
57 | }
58 | }
59 | subscription ItemAddedSubscription($connections: [String!]!) {
60 | itemAdded {
61 | item @prependNode(connections: $connections) {
62 | name
63 | id
64 | __typename
65 | }
66 | }
67 | }
68 | fragment Fragment_user_bi8xLGNvdW50OjU on User {
69 | items(first: 5, after: $cursor) {
70 | edges {
71 | node {
72 | id
73 | __typename
74 | }
75 | cursor
76 | }
77 | pageInfo {
78 | hasNextPage
79 | endCursor
80 | hasPreviousPage
81 | startCursor
82 | }
83 | _connectionId @client
84 | }
85 | id
86 | __typename
87 | }
88 | `);
89 |
90 | const result = await preset.buildGeneratesSection({
91 | baseOutputDir: 'src/generated/graphql.ts',
92 | config: {},
93 | presetConfig: {},
94 | schema: testSchemaDocumentNode,
95 | documents: documents,
96 | plugins: [],
97 | pluginMap: {},
98 | });
99 |
100 | expect(printDocuments(result[0].documents)).toBe(printDocuments([{ document: expectedDocument }]));
101 | });
102 |
103 | describe('generateTypeScriptCode', () => {
104 | it('outputs nothing code to the content when the flag is not set to true', async () => {
105 | const input = {
106 | schema: path.join(__dirname, '../utils/testing/example.graphql'),
107 | documents: path.join(__dirname, './fixtures/exampleFile.ts'),
108 | generates: {
109 | ['./generated.ts']: {
110 | preset,
111 | plugins: [],
112 | },
113 | },
114 | };
115 |
116 | const result = await executeCodegen(input);
117 | expect(result[0].content).toBe('');
118 | });
119 |
120 | it('outputs TypeScript code when the flag is true', async () => {
121 | const input = {
122 | schema: path.join(__dirname, '../utils/testing/example.graphql'),
123 | documents: path.join(__dirname, './fixtures/exampleFile.ts'),
124 | generates: {
125 | ['./generated.ts']: {
126 | preset,
127 | plugins: [],
128 | presetConfig: {
129 | generateTypeScriptCode: true,
130 | },
131 | },
132 | },
133 | };
134 |
135 | const result = await executeCodegen(input);
136 | expect(result[0].content).toMatchSnapshot();
137 | });
138 | });
139 |
140 | describe('Multiple Sections', () => {
141 | it('works with no errors', async () => {
142 | const input = {
143 | schema: path.join(__dirname, '../utils/testing/example.graphql'),
144 | generates: {
145 | ['./generated.ts']: {
146 | preset,
147 | plugins: [],
148 | presetConfig: {
149 | generateTypeScriptCode: true,
150 | },
151 | documents: path.join(__dirname, './fixtures/exampleFile.ts'),
152 | },
153 | ['./generated2.ts']: {
154 | preset,
155 | plugins: [],
156 | presetConfig: {
157 | generateTypeScriptCode: true,
158 | },
159 | documents: path.join(__dirname, './fixtures/exampleFile.ts'),
160 | },
161 | ['./generated3.ts']: {
162 | preset,
163 | plugins: [],
164 | documents: path.join(__dirname, './fixtures/exampleFile.ts'),
165 | },
166 | },
167 | };
168 |
169 | const result = await executeCodegen(input);
170 | expect(result.length).toBe(3);
171 | });
172 | });
173 | });
174 |
--------------------------------------------------------------------------------
/packages/graphql-codegen-preset/src/index.ts:
--------------------------------------------------------------------------------
1 | import { preset } from './preset';
2 | export default preset;
3 |
--------------------------------------------------------------------------------
/packages/graphql-codegen-preset/src/plugins/cache-updater-support/__tests__/index.test.ts:
--------------------------------------------------------------------------------
1 | import { mergeOutputs } from '@graphql-codegen/plugin-helpers';
2 | import { validateTs } from '@graphql-codegen/testing';
3 | import { parse } from 'graphql';
4 | import { plugin } from '..';
5 | import { testGraphqlSchema } from '../../../utils/testing/utils';
6 |
7 | describe('cache-updater-support', () => {
8 | const documentsForPagination = [
9 | {
10 | document: parse(/* GraphQL */ `
11 | query TestQuery($cursor1: String, $cursor2: String, $cursor3: String) {
12 | itemsOnRoot: items(first: 1, after: $cursor1, keyword: "test") @pagination {
13 | edges {
14 | node {
15 | id
16 | __typename
17 | }
18 | cursor
19 | }
20 | pageInfo {
21 | hasNextPage
22 | endCursor
23 | }
24 | }
25 | viewer {
26 | id
27 | ...Fragment_user
28 | myItems: items(first: 1, after: $cursor2, keyword: "test2") @pagination {
29 | edges {
30 | node {
31 | name
32 | id
33 | __typename
34 | }
35 | cursor
36 | }
37 | pageInfo {
38 | hasNextPage
39 | endCursor
40 | }
41 | }
42 | }
43 | }
44 | fragment Fragment_user on User {
45 | id
46 | itemsOnFragment: items(first: 1, after: $cursor2, keyword: "test3") @pagination {
47 | edges {
48 | node {
49 | name
50 | id
51 | __typename
52 | }
53 | cursor
54 | }
55 | pageInfo {
56 | hasNextPage
57 | endCursor
58 | }
59 | }
60 | }
61 | `),
62 | },
63 | {
64 | document: parse(/* GraphQL */ `
65 | mutation DeleteItemMutation($input: RemoveItemInput!) {
66 | removeItem(input: $input) {
67 | removedItem {
68 | id @deleteRecord(typename: "Item")
69 | __typename
70 | }
71 | }
72 | }
73 | `),
74 | },
75 | {
76 | document: parse(/* GraphQL */ `
77 | subscription ItemDeletedSubscription($connections: [String!]!) {
78 | itemRemoved {
79 | id @deleteRecord(typename: "Item")
80 | __typename
81 | }
82 | }
83 | `),
84 | },
85 | ];
86 |
87 | it('generates paginationMetaList code', async () => {
88 | const result = await plugin(testGraphqlSchema, documentsForPagination, { documentFiles: documentsForPagination });
89 | const merged = mergeOutputs([result]);
90 | validateTs(merged, undefined, false, true);
91 | expect(result.content).toContain(
92 | `export const paginationMetaList = [{ node: { typename: 'Item' }, parents: [{ typename: 'Query', connection: { fieldName: 'items' }, edge: { typename: 'ItemEdge' } }, { typename: 'User', connection: { fieldName: 'items' }, edge: { typename: 'ItemEdge' } }] }];`,
93 | );
94 | });
95 |
96 | it('generates deleteRecordMetaList code', async () => {
97 | const result = await plugin(testGraphqlSchema, documentsForPagination, { documentFiles: documentsForPagination });
98 | const merged = mergeOutputs([result]);
99 | validateTs(merged, undefined, false, true);
100 | expect(result.content).toContain(
101 | `const deleteRecordMetaList = [{ parent: { typename: 'RemovedItem' }, fields: [{ fieldName: 'id', typename: 'Item' }] }, { parent: { typename: 'ItemRemovedPayload' }, fields: [{ fieldName: 'id', typename: 'Item' }] }];`,
102 | );
103 | });
104 |
105 | it('generates withCacheUpdater code', async () => {
106 | const result = await plugin(testGraphqlSchema, documentsForPagination, { documentFiles: documentsForPagination });
107 | const merged = mergeOutputs([result]);
108 | validateTs(merged, undefined, false, true);
109 | expect(result.content).toContain(
110 | `
111 | export type CacheUpdaterTypePolicies = {
112 | Query: TypePolicy;
113 | User: TypePolicy;
114 | [__typename: string]: TypePolicy;
115 | };
116 |
117 | export const withCacheUpdater = (typePolicies: CacheUpdaterTypePolicies) =>
118 | withCacheUpdaterInternal({
119 | paginationMetaList,
120 | deleteRecordMetaList,
121 | typePolicies,
122 | });`,
123 | );
124 | expect(result.prepend).toStrictEqual([
125 | "import { TypePolicy } from '@apollo/client';",
126 | "import { withCacheUpdaterInternal } from '@kazekyo/nau';",
127 | ]);
128 | });
129 | });
130 |
--------------------------------------------------------------------------------
/packages/graphql-codegen-preset/src/plugins/cache-updater-support/config.ts:
--------------------------------------------------------------------------------
1 | import { ClientSideBasePluginConfig, RawClientSideBasePluginConfig } from '@graphql-codegen/visitor-plugin-common';
2 |
3 | // eslint-disable-next-line @typescript-eslint/no-empty-interface
4 | export interface PaginationConfig {}
5 |
6 | export interface PaginationRawPluginConfig extends RawClientSideBasePluginConfig, PaginationConfig {}
7 | export interface PaginationPluginConfig extends ClientSideBasePluginConfig, PaginationConfig {}
8 |
--------------------------------------------------------------------------------
/packages/graphql-codegen-preset/src/plugins/cache-updater-support/index.ts:
--------------------------------------------------------------------------------
1 | import { PluginFunction, PluginValidateFn, Types } from '@graphql-codegen/plugin-helpers';
2 | import { LoadedFragment } from '@graphql-codegen/visitor-plugin-common';
3 | import { concatAST, FragmentDefinitionNode, GraphQLSchema, Kind, TypeInfo, visit, visitWithTypeInfo } from 'graphql';
4 | import { extname } from 'path';
5 | import { nonNullable } from '../../utils/nonNullable';
6 | import { PaginationPluginConfig, PaginationRawPluginConfig } from './config';
7 | import { PaginationVisitor } from './visitor';
8 |
9 | export const plugin: PluginFunction<
10 | PaginationRawPluginConfig & { documentFiles: Types.DocumentFile[] },
11 | Types.ComplexPluginOutput
12 | > = (schema, _, config) => {
13 | const { documentFiles } = config;
14 | const documents = documentFiles.map((file) => file.document).filter(nonNullable);
15 | const allAst = concatAST(documents);
16 |
17 | const allFragments: LoadedFragment[] = [
18 | ...(allAst.definitions.filter((d) => d.kind === Kind.FRAGMENT_DEFINITION) as FragmentDefinitionNode[]).map(
19 | (fragmentDef) => ({
20 | node: fragmentDef,
21 | name: fragmentDef.name.value,
22 | onType: fragmentDef.typeCondition.name.value,
23 | isExternal: false,
24 | }),
25 | ),
26 | ...(config.externalFragments || []),
27 | ];
28 | const typeInfo = new TypeInfo(schema);
29 | const visitor = new PaginationVisitor(schema, allFragments, config, documentFiles, typeInfo);
30 |
31 | visit(
32 | allAst,
33 | visitWithTypeInfo(typeInfo, { Directive: visitor.Directive.bind(visitor), Field: visitor.Field.bind(visitor) }),
34 | );
35 |
36 | return {
37 | prepend: visitor.getImports(),
38 | content: visitor.getContent(),
39 | };
40 | };
41 |
42 | export const validate: PluginValidateFn = (
43 | schema: GraphQLSchema,
44 | documents: Types.DocumentFile[],
45 | config: PaginationPluginConfig,
46 | outputFile: string,
47 | ) => {
48 | if (extname(outputFile) !== '.tsx' && extname(outputFile) !== '.ts') {
49 | throw new Error(`Plugin "nau.cacheUpdaterSupport" requires extension to be ".tsx" or ".ts"!`);
50 | }
51 | };
52 |
--------------------------------------------------------------------------------
/packages/graphql-codegen-preset/src/preset.ts:
--------------------------------------------------------------------------------
1 | import { Types } from '@graphql-codegen/plugin-helpers';
2 | import { validateGraphQlDocuments } from '@graphql-tools/utils';
3 | import { paginationDirectiveValidationRule } from '@kazekyo/nau-config';
4 | import {
5 | RelayArgumentsOfCorrectType,
6 | RelayDefaultValueOfCorrectType,
7 | RelayKnownArgumentNames,
8 | RelayNoUnusedArguments,
9 | } from '@relay-graphql-js/validation-rules';
10 | import { ASTVisitor, buildASTSchema, GraphQLSchema, specifiedRules, ValidationContext, ValidationRule } from 'graphql';
11 | import cloneDeep from 'lodash.clonedeep';
12 | import * as paginationPlugin from './plugins/cache-updater-support';
13 | import { PresetConfig } from './presetConfig';
14 | import { addCustomClientDirective } from './schemaTransforms/addClientDirective';
15 | import { addConnectionId } from './schemaTransforms/addConnectionId';
16 | import { transform as addFieldsForAddingNode } from './transforms/addFieldsForAddingNode';
17 | import { transform as addPaginationFields } from './transforms/addPaginationFields';
18 | import { transform as fixVariableNotDefinedInRoot } from './transforms/fixVariableNotDefinedInRoot';
19 | import { transform as generateRefetchQuery } from './transforms/generateRefetchQuery';
20 | import { transform as passArgumentValueToFragment } from './transforms/passArgumentValueToFragment';
21 | import { transform as removeCustomDirective } from './transforms/removeCustomDirective';
22 | import { nonNullable } from './utils/nonNullable';
23 |
24 | const transformDocuments = ({
25 | documentFiles,
26 | }: {
27 | documentFiles: Types.DocumentFile[];
28 | }): { documentFiles: Types.DocumentFile[] } => {
29 | let result = { documentFiles };
30 | [
31 | passArgumentValueToFragment,
32 | generateRefetchQuery,
33 | fixVariableNotDefinedInRoot,
34 | addPaginationFields,
35 | addFieldsForAddingNode,
36 | ].forEach((transformFunc) => {
37 | result = transformFunc(result);
38 | });
39 | return result;
40 | };
41 |
42 | const transformSchema = (schema: GraphQLSchema, documentFiles: Types.DocumentFile[]): GraphQLSchema => {
43 | let result = schema;
44 | [addCustomClientDirective, addConnectionId].forEach((transform) => {
45 | result = transform(result, documentFiles).schema;
46 | });
47 | return result;
48 | };
49 |
50 | const validationRules = (): ValidationRule[] => {
51 | const ignored = [
52 | 'NoUnusedFragmentsRule',
53 | 'NoUnusedVariablesRule',
54 | 'KnownArgumentNamesRule',
55 | 'NoUndefinedVariablesRule',
56 | ];
57 | const v4ignored = ignored.map((rule) => rule.replace(/Rule$/, ''));
58 |
59 | const rules = specifiedRules.filter(
60 | (f: (context: ValidationContext) => ASTVisitor) => !ignored.includes(f.name) && !v4ignored.includes(f.name),
61 | );
62 | return [
63 | ...rules,
64 | RelayArgumentsOfCorrectType,
65 | RelayDefaultValueOfCorrectType,
66 | RelayNoUnusedArguments,
67 | RelayKnownArgumentNames,
68 | paginationDirectiveValidationRule,
69 | ] as unknown[] as ValidationRule[];
70 | };
71 |
72 | export const preset: Types.OutputPreset = {
73 | buildGeneratesSection: (options) => {
74 | const originalGraphQLSchema: GraphQLSchema = options.schemaAst
75 | ? options.schemaAst
76 | : buildASTSchema(options.schema, options.config);
77 |
78 | const schemaObject = transformSchema(originalGraphQLSchema, options.documents);
79 |
80 | const errors = validateGraphQlDocuments(
81 | schemaObject,
82 | options.documents.map((d) => d.document).filter(nonNullable),
83 | validationRules(),
84 | );
85 | if (errors.length > 0) {
86 | throw new Error(
87 | `GraphQL Document Validation failed with ${errors.length} errors;
88 | ${errors.map((error, index) => `Error ${index}: ${error.stack || ''}`).join('\n\n')}`,
89 | );
90 | }
91 |
92 | const transformedObject = transformDocuments({ documentFiles: cloneDeep(options.documents) });
93 |
94 | let pluginMap = options.pluginMap;
95 | let plugins = options.plugins;
96 | const { generateTypeScriptCode } = options.presetConfig;
97 | if (generateTypeScriptCode) {
98 | pluginMap = { [`nau-pagination-code`]: paginationPlugin, ...pluginMap };
99 | plugins = [
100 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
101 | { [`nau-pagination-code`]: { documentFiles: cloneDeep(transformedObject.documentFiles) } },
102 | ...plugins,
103 | ];
104 | }
105 |
106 | const { documentFiles } = removeCustomDirective({ documentFiles: transformedObject.documentFiles });
107 |
108 | const result = [
109 | {
110 | filename: options.baseOutputDir,
111 | plugins: plugins,
112 | pluginMap: pluginMap,
113 | config: options.config,
114 | schema: options.schema,
115 | schemaAst: schemaObject,
116 | documents: documentFiles,
117 | },
118 | ];
119 | return result;
120 | },
121 | };
122 |
--------------------------------------------------------------------------------
/packages/graphql-codegen-preset/src/presetConfig.ts:
--------------------------------------------------------------------------------
1 | export type PresetConfig = {
2 | /**
3 | * @name generateTypeScriptCode
4 | * @type boolean
5 | * @default false
6 | * @description Optional, generate TypeScript code.
7 | *
8 | * @example
9 | * ```yml
10 | * generates:
11 | * src/:
12 | * preset: @kazekyo/nau-graphql-codegen-preset
13 | * presetConfig:
14 | * generateTypeScriptCode: true
15 | * plugins:
16 | * - typescript
17 | * - typescript-operations
18 | * - typescript-react-apollo
19 | * ```
20 | */
21 | generateTypeScriptCode?: boolean;
22 | };
23 |
--------------------------------------------------------------------------------
/packages/graphql-codegen-preset/src/schemaTransforms/__tests__/addConnectionId.test.ts:
--------------------------------------------------------------------------------
1 | import { parse, printSchema } from 'graphql';
2 | import { testGraphqlSchema } from '../../utils/testing/utils';
3 | import { addConnectionId } from '../addConnectionId';
4 | describe('addConnectionId', () => {
5 | const documentFiles = [
6 | {
7 | document: parse(/* GraphQL */ `
8 | query TestQuery($cursor1: String, $cursor2: String, $cursor3: String) {
9 | itemsOnRoot: items(first: 1, after: $cursor1, keyword: "test") @pagination {
10 | edges {
11 | node {
12 | id
13 | __typename
14 | }
15 | cursor
16 | }
17 | pageInfo {
18 | hasNextPage
19 | endCursor
20 | }
21 | }
22 | viewer {
23 | id
24 | ...Fragment_user
25 | myItems: items(first: 1, after: $cursor2, keyword: "test2") @pagination {
26 | edges {
27 | node {
28 | name
29 | id
30 | __typename
31 | }
32 | cursor
33 | }
34 | pageInfo {
35 | hasNextPage
36 | endCursor
37 | }
38 | }
39 | }
40 | }
41 | fragment Fragment_user on User {
42 | id
43 | itemsOnFragment: items(first: 1, after: $cursor2, keyword: "test3") @pagination {
44 | edges {
45 | node {
46 | name
47 | id
48 | __typename
49 | }
50 | cursor
51 | }
52 | pageInfo {
53 | hasNextPage
54 | endCursor
55 | }
56 | }
57 | }
58 | `),
59 | },
60 | ];
61 | it('adds the connectionId field to the connection type with @pagination attached', () => {
62 | const before = `
63 | """A connection to a list of items."""
64 | type ItemConnection {
65 | """A list of edges."""
66 | edges: [ItemEdge]
67 |
68 | """Information to aid in pagination."""
69 | pageInfo: PageInfo!
70 | }`;
71 | expect(printSchema(testGraphqlSchema)).toContain(before);
72 | const result = addConnectionId(testGraphqlSchema, documentFiles);
73 | const after = `
74 | """A connection to a list of items."""
75 | type ItemConnection {
76 | """A list of edges."""
77 | edges: [ItemEdge]
78 |
79 | """Information to aid in pagination."""
80 | pageInfo: PageInfo!
81 |
82 | """Information of the connection for a client"""
83 | _connectionId: String!
84 | }`;
85 | expect(printSchema(result.schema)).toContain(after);
86 | });
87 | });
88 |
--------------------------------------------------------------------------------
/packages/graphql-codegen-preset/src/schemaTransforms/addClientDirective.ts:
--------------------------------------------------------------------------------
1 | import { extendSchema, GraphQLSchema, parse } from 'graphql';
2 | import { clientDirectives } from '@kazekyo/nau-config';
3 |
4 | export const addCustomClientDirective = (schema: GraphQLSchema): { schema: GraphQLSchema } => {
5 | const currentDirectives = schema.getDirectives();
6 | const additionalDirectives = Object.entries(clientDirectives)
7 | .filter(([key, _]) => !currentDirectives.find((d) => d.name === key))
8 | .map(([_, value]) => value);
9 |
10 | if (additionalDirectives.length === 0) {
11 | return { schema };
12 | }
13 |
14 | return { schema: extendSchema(schema, parse(additionalDirectives.join('\n'))) };
15 | };
16 |
--------------------------------------------------------------------------------
/packages/graphql-codegen-preset/src/schemaTransforms/addConnectionId.ts:
--------------------------------------------------------------------------------
1 | import { Types } from '@graphql-codegen/plugin-helpers';
2 | import { PAGINATION_DIRECTIVE_NAME } from '@kazekyo/nau';
3 | import {
4 | concatAST,
5 | extendSchema,
6 | FieldDefinitionNode,
7 | GraphQLSchema,
8 | Kind,
9 | NameNode,
10 | ObjectTypeExtensionNode,
11 | TypeInfo,
12 | visit,
13 | visitWithTypeInfo,
14 | } from 'graphql';
15 | import { uniq } from 'lodash';
16 | import { getConnectionType, getEdgeType, getNodeType } from '../utils/graphqlSchema';
17 | import { nonNullable } from '../utils/nonNullable';
18 |
19 | export const addConnectionId = (
20 | schema: GraphQLSchema,
21 | documentFiles: Types.DocumentFile[],
22 | ): { schema: GraphQLSchema } => {
23 | const documents = documentFiles.map((file) => file.document).filter(nonNullable);
24 | const allAst = concatAST(documents);
25 |
26 | const paginationTypes: string[] = [];
27 | const typeInfo = new TypeInfo(schema);
28 | visit(
29 | allAst,
30 | visitWithTypeInfo(typeInfo, {
31 | Field: {
32 | leave(fieldNode) {
33 | if (!fieldNode.directives) return;
34 | const paginationDirective = fieldNode.directives.find(
35 | (directive) => directive.name.value === PAGINATION_DIRECTIVE_NAME,
36 | );
37 | if (!paginationDirective) return;
38 |
39 | const connectionType = getConnectionType({ type: typeInfo.getType() });
40 | if (!connectionType) return;
41 | const edgeType = getEdgeType({ connectionType, schema });
42 | if (!edgeType) return;
43 | const nodeType = getNodeType({ edgeType, schema });
44 | if (!nodeType) return;
45 |
46 | paginationTypes.push(connectionType.name);
47 | },
48 | },
49 | }),
50 | );
51 |
52 | const definitions = uniq(paginationTypes).map((typename) => generateExtendConnectionType(typename));
53 | const result = extendSchema(schema, { kind: Kind.DOCUMENT, definitions });
54 |
55 | return { schema: result };
56 | };
57 |
58 | const generateExtendConnectionType = (connectionTypeName: string): ObjectTypeExtensionNode => {
59 | const name: NameNode = {
60 | kind: Kind.NAME,
61 | value: connectionTypeName,
62 | };
63 | const field: FieldDefinitionNode = {
64 | kind: Kind.FIELD_DEFINITION,
65 | name: {
66 | kind: Kind.NAME,
67 | value: '_connectionId',
68 | },
69 | description: {
70 | kind: Kind.STRING,
71 | value: 'Information of the connection for a client',
72 | },
73 | type: {
74 | kind: Kind.NON_NULL_TYPE,
75 | type: {
76 | kind: Kind.NAMED_TYPE,
77 | name: {
78 | kind: Kind.NAME,
79 | value: 'String',
80 | },
81 | },
82 | },
83 | };
84 | const extentions: ObjectTypeExtensionNode = {
85 | kind: Kind.OBJECT_TYPE_EXTENSION,
86 | name: name,
87 | fields: [field],
88 | };
89 | return extentions;
90 | };
91 |
--------------------------------------------------------------------------------
/packages/graphql-codegen-preset/src/transforms/__tests__/addFieldsForAddingNode.test.ts:
--------------------------------------------------------------------------------
1 | import { parse } from 'graphql';
2 | import { printDocuments } from '../../utils/testing/utils';
3 | import { transform } from '../addFieldsForAddingNode';
4 |
5 | describe('transform', () => {
6 | it('adds fileds for @prependNode', () => {
7 | const document = parse(/* GraphQL */ `
8 | mutation AddItemMutation($input: AddItemInput!, $connections: [String!]!) {
9 | addItem(input: $input) {
10 | item @prependNode(connections: $connections) {
11 | name
12 | }
13 | }
14 | }
15 |
16 | subscription ItemAddedSubscription($connections: [String!]!) {
17 | itemAdded {
18 | item @prependNode(connections: $connections) {
19 | name
20 | }
21 | }
22 | }
23 | `);
24 | const expectedDocument = parse(/* GraphQL */ `
25 | mutation AddItemMutation($input: AddItemInput!, $connections: [String!]!) {
26 | addItem(input: $input) {
27 | item @prependNode(connections: $connections) {
28 | name
29 | id
30 | __typename
31 | }
32 | }
33 | }
34 |
35 | subscription ItemAddedSubscription($connections: [String!]!) {
36 | itemAdded {
37 | item @prependNode(connections: $connections) {
38 | name
39 | id
40 | __typename
41 | }
42 | }
43 | }
44 | `);
45 | const result = transform({ documentFiles: [{ document }] });
46 |
47 | expect(printDocuments(result.documentFiles)).toBe(printDocuments([{ document: expectedDocument }]));
48 | });
49 |
50 | it('adds fileds for @appendNode', () => {
51 | const document = parse(/* GraphQL */ `
52 | mutation AddItemMutation($input: AddItemInput!, $connections: [String!]!) {
53 | addItem(input: $input) {
54 | item @appendNode(connections: $connections) {
55 | name
56 | }
57 | }
58 | }
59 |
60 | subscription ItemAddedSubscription($connections: [String!]!) {
61 | itemAdded {
62 | item @appendNode(connections: $connections) {
63 | name
64 | }
65 | }
66 | }
67 | `);
68 | const expectedDocument = parse(/* GraphQL */ `
69 | mutation AddItemMutation($input: AddItemInput!, $connections: [String!]!) {
70 | addItem(input: $input) {
71 | item @appendNode(connections: $connections) {
72 | name
73 | id
74 | __typename
75 | }
76 | }
77 | }
78 |
79 | subscription ItemAddedSubscription($connections: [String!]!) {
80 | itemAdded {
81 | item @appendNode(connections: $connections) {
82 | name
83 | id
84 | __typename
85 | }
86 | }
87 | }
88 | `);
89 | const result = transform({ documentFiles: [{ document }] });
90 |
91 | expect(printDocuments(result.documentFiles)).toBe(printDocuments([{ document: expectedDocument }]));
92 | });
93 | });
94 |
--------------------------------------------------------------------------------
/packages/graphql-codegen-preset/src/transforms/__tests__/generateRefetchQuery.test.ts:
--------------------------------------------------------------------------------
1 | import { parse } from 'graphql';
2 | import { printDocuments } from '../../utils/testing/utils';
3 | import { transform } from '../generateRefetchQuery';
4 |
5 | describe('transform', () => {
6 | it('generates the refetch query', () => {
7 | const document = parse(/* GraphQL */ `
8 | query TestQuery {
9 | viewer {
10 | ...UserFragment @arguments(count: 5)
11 | }
12 | }
13 | fragment UserFragment on User
14 | @argumentDefinitions(count: { type: "Int", defaultValue: 2 }, cursor: { type: "String" })
15 | @refetchable(queryName: "TestRefetchQuery") {
16 | id
17 | items(first: $count, after: $cursor) {
18 | edges {
19 | node {
20 | id
21 | }
22 | }
23 | }
24 | }
25 | `);
26 | const expectedDocument = parse(/* GraphQL */ `
27 | query TestQuery {
28 | viewer {
29 | ...UserFragment @arguments(count: 5)
30 | }
31 | }
32 | fragment UserFragment on User
33 | @argumentDefinitions(count: { type: "Int", defaultValue: 2 }, cursor: { type: "String" })
34 | @refetchable(queryName: "TestRefetchQuery") {
35 | id
36 | items(first: $count, after: $cursor) {
37 | edges {
38 | node {
39 | id
40 | }
41 | }
42 | }
43 | }
44 | query TestRefetchQuery($count: Int = 2, $cursor: String, $id: ID!) {
45 | node(id: $id) {
46 | id
47 | __typename
48 | ...UserFragment
49 | }
50 | }
51 | `);
52 | const result = transform({ documentFiles: [{ document }] });
53 |
54 | expect(printDocuments(result.documentFiles)).toBe(printDocuments([{ document: expectedDocument }]));
55 | });
56 |
57 | it('collects all variables from fragments and defines them as variables of the query', () => {
58 | const document = parse(/* GraphQL */ `
59 | query TestQuery {
60 | viewer {
61 | ...UserFragment1
62 | }
63 | }
64 | fragment UserFragment1 on User
65 | @argumentDefinitions(count: { type: "Int", defaultValue: 2 }, cursor: { type: "String" })
66 | @refetchable(queryName: "TestRefetchQuery") {
67 | ...UserFragment2
68 | items(first: $count, after: $cursor) {
69 | edges {
70 | node {
71 | id
72 | }
73 | }
74 | }
75 | }
76 | fragment UserFragment2 on User
77 | @argumentDefinitions(
78 | iconWidth: { type: "Int", defaultValue: 100 }
79 | iconHeight: { type: "Int", defaultValue: 100 }
80 | ) {
81 | id
82 | iconImage(width: $iconWidth, height: $iconHeight)
83 | }
84 | `);
85 |
86 | const expectedDocument = parse(/* GraphQL */ `
87 | query TestQuery {
88 | viewer {
89 | ...UserFragment1
90 | }
91 | }
92 | fragment UserFragment1 on User
93 | @argumentDefinitions(count: { type: "Int", defaultValue: 2 }, cursor: { type: "String" })
94 | @refetchable(queryName: "TestRefetchQuery") {
95 | ...UserFragment2
96 | items(first: $count, after: $cursor) {
97 | edges {
98 | node {
99 | id
100 | }
101 | }
102 | }
103 | }
104 | fragment UserFragment2 on User
105 | @argumentDefinitions(
106 | iconWidth: { type: "Int", defaultValue: 100 }
107 | iconHeight: { type: "Int", defaultValue: 100 }
108 | ) {
109 | id
110 | iconImage(width: $iconWidth, height: $iconHeight)
111 | }
112 | query TestRefetchQuery(
113 | $count: Int = 2
114 | $cursor: String
115 | $iconWidth: Int = 100
116 | $iconHeight: Int = 100
117 | $id: ID!
118 | ) {
119 | node(id: $id) {
120 | id
121 | __typename
122 | ...UserFragment1
123 | }
124 | }
125 | `);
126 | const result = transform({ documentFiles: [{ document }] });
127 |
128 | expect(printDocuments(result.documentFiles)).toBe(printDocuments([{ document: expectedDocument }]));
129 | });
130 | });
131 |
--------------------------------------------------------------------------------
/packages/graphql-codegen-preset/src/transforms/__tests__/passArgumentValueToFragment.test.ts:
--------------------------------------------------------------------------------
1 | import { parse } from 'graphql';
2 | import { printDocuments } from '../../utils/testing/utils';
3 | import { transform } from '../passArgumentValueToFragment';
4 |
5 | describe('transform', () => {
6 | it('changes the documents', () => {
7 | const document = parse(/* GraphQL */ `
8 | query TestQuery($testNumber: Int!) {
9 | viewer {
10 | ...UserFragment @arguments(arg1: 1, arg2: "2", arg3: $testNumber)
11 | }
12 | }
13 | fragment UserFragment on User
14 | @argumentDefinitions(arg1: { type: "Int", defaultValue: 10 }, arg2: { type: "String" }, arg3: { type: "Int!" }) {
15 | id
16 | item(itemArg1: $arg1, itemArg2: $arg2, itemArg3: $arg3) {
17 | id
18 | name
19 | }
20 | }
21 | `);
22 | const expectedDocument = parse(/* GraphQL */ `
23 | query TestQuery($testNumber: Int!) {
24 | viewer {
25 | ...UserFragment_bi8xLGFyZzE6MSxhcmcyOiIyIixhcmczOiR0ZXN0TnVtYmVy
26 | @arguments(arg1: 1, arg2: "2", arg3: $testNumber)
27 | }
28 | }
29 | fragment UserFragment_bi8xLGFyZzE6MSxhcmcyOiIyIixhcmczOiR0ZXN0TnVtYmVy on User
30 | @argumentDefinitions(arg1: { type: "Int", defaultValue: 10 }, arg2: { type: "String" }, arg3: { type: "Int!" }) {
31 | id
32 | item(itemArg1: 1, itemArg2: "2", itemArg3: $testNumber) {
33 | id
34 | name
35 | }
36 | }
37 | `);
38 | const result = transform({ documentFiles: [{ document }] });
39 |
40 | expect(printDocuments(result.documentFiles)).toBe(printDocuments([{ document: expectedDocument }]));
41 | });
42 |
43 | it('copies the fragment definition with @arguments attached if there are both a spreading fragment with @arguments attached and a spreading fragment without @arguments attached', () => {
44 | const document = parse(/* GraphQL */ `
45 | query TestQuery1 {
46 | viewer {
47 | ...UserFragment @arguments(arg1: 1)
48 | }
49 | }
50 | query TestQuery2 {
51 | viewer {
52 | ...UserFragment
53 | }
54 | }
55 | query TestQuery3 {
56 | viewer {
57 | ...UserFragment
58 | }
59 | }
60 | query TestQuery4 {
61 | viewer {
62 | ...UserFragment_wrapped
63 | }
64 | }
65 | fragment UserFragment on User @argumentDefinitions(arg1: { type: "Int", defaultValue: 10 }) {
66 | id
67 | item(itemArg1: $arg1) {
68 | id
69 | }
70 | }
71 | fragment UserFragment_wrapped on User {
72 | ...UserFragment @arguments(arg1: 2)
73 | }
74 | `);
75 | const expectedDocument = parse(/* GraphQL */ `
76 | query TestQuery1 {
77 | viewer {
78 | ...UserFragment_bi8xLGFyZzE6MQ @arguments(arg1: 1)
79 | }
80 | }
81 | query TestQuery2 {
82 | viewer {
83 | ...UserFragment
84 | }
85 | }
86 | query TestQuery3 {
87 | viewer {
88 | ...UserFragment
89 | }
90 | }
91 | query TestQuery4 {
92 | viewer {
93 | ...UserFragment_wrapped
94 | }
95 | }
96 | fragment UserFragment_bi8xLGFyZzE6MQ on User @argumentDefinitions(arg1: { type: "Int", defaultValue: 10 }) {
97 | id
98 | item(itemArg1: 1) {
99 | id
100 | }
101 | }
102 | fragment UserFragment on User @argumentDefinitions(arg1: { type: "Int", defaultValue: 10 }) {
103 | id
104 | item(itemArg1: $arg1) {
105 | id
106 | }
107 | }
108 | fragment UserFragment_bi8xLGFyZzE6Mg on User @argumentDefinitions(arg1: { type: "Int", defaultValue: 10 }) {
109 | id
110 | item(itemArg1: 2) {
111 | id
112 | }
113 | }
114 | fragment UserFragment_wrapped on User {
115 | ...UserFragment_bi8xLGFyZzE6Mg @arguments(arg1: 2)
116 | }
117 | `);
118 | const result = transform({ documentFiles: [{ document }] });
119 |
120 | expect(printDocuments(result.documentFiles)).toBe(printDocuments([{ document: expectedDocument }]));
121 | });
122 |
123 | it('deletes the original fragment definition if @arguments is attached to all spreading fragments', () => {
124 | const document = parse(/* GraphQL */ `
125 | query TestQuery1 {
126 | viewer {
127 | ...UserFragment @arguments(arg1: 1)
128 | }
129 | }
130 | query TestQuery2 {
131 | viewer {
132 | ...UserFragment @arguments(arg1: 2)
133 | }
134 | }
135 | fragment UserFragment on User @argumentDefinitions(arg1: { type: "Int", defaultValue: 10 }) {
136 | id
137 | item(itemArg1: $arg1) {
138 | id
139 | }
140 | }
141 | `);
142 | const expectedDocument = parse(/* GraphQL */ `
143 | query TestQuery1 {
144 | viewer {
145 | ...UserFragment_bi8xLGFyZzE6MQ @arguments(arg1: 1)
146 | }
147 | }
148 | query TestQuery2 {
149 | viewer {
150 | ...UserFragment_bi8xLGFyZzE6Mg @arguments(arg1: 2)
151 | }
152 | }
153 | fragment UserFragment_bi8xLGFyZzE6MQ on User @argumentDefinitions(arg1: { type: "Int", defaultValue: 10 }) {
154 | id
155 | item(itemArg1: 1) {
156 | id
157 | }
158 | }
159 | fragment UserFragment_bi8xLGFyZzE6Mg on User @argumentDefinitions(arg1: { type: "Int", defaultValue: 10 }) {
160 | id
161 | item(itemArg1: 2) {
162 | id
163 | }
164 | }
165 | `);
166 | const result = transform({ documentFiles: [{ document }] });
167 |
168 | expect(printDocuments(result.documentFiles)).toBe(printDocuments([{ document: expectedDocument }]));
169 | });
170 | });
171 |
--------------------------------------------------------------------------------
/packages/graphql-codegen-preset/src/transforms/__tests__/removeCustomDirective.test.ts:
--------------------------------------------------------------------------------
1 | import { parse } from 'graphql';
2 | import { printDocuments } from '../../utils/testing/utils';
3 | import { transform } from '../removeCustomDirective';
4 |
5 | describe('transform', () => {
6 | it('removes custom directives', () => {
7 | const document = parse(/* GraphQL */ `
8 | query TestQuery($cursor: String) {
9 | viewer {
10 | ...UserFragment @arguments(arg: 1)
11 | }
12 | }
13 | fragment UserFragment on User
14 | @refetchable(queryName: "RefetchQuery")
15 | @argumentDefinitions(arg: { type: "Int", defaultValue: 10 }) {
16 | id
17 | items(first: 1, after: $cursor) @pagination {
18 | edges {
19 | node {
20 | id
21 | }
22 | }
23 | }
24 | }
25 | `);
26 | const expectedDocument = parse(/* GraphQL */ `
27 | query TestQuery($cursor: String) {
28 | viewer {
29 | ...UserFragment
30 | }
31 | }
32 | fragment UserFragment on User {
33 | id
34 | items(first: 1, after: $cursor) {
35 | edges {
36 | node {
37 | id
38 | }
39 | }
40 | }
41 | }
42 | `);
43 | const result = transform({ documentFiles: [{ document }] });
44 |
45 | expect(printDocuments(result.documentFiles)).toBe(printDocuments([{ document: expectedDocument }]));
46 | });
47 | });
48 |
--------------------------------------------------------------------------------
/packages/graphql-codegen-preset/src/transforms/addFieldsForAddingNode.ts:
--------------------------------------------------------------------------------
1 | import { Types } from '@graphql-codegen/plugin-helpers';
2 | import { INSERT_NODE_DIRECTIVE_NAMES } from '@kazekyo/nau';
3 | import { visit } from 'graphql';
4 | import {
5 | addFieldToSelectionSetNodeWithoutDuplication,
6 | getDirectives,
7 | idField,
8 | typenameField,
9 | } from '../utils/graphqlAST';
10 |
11 | export const transform = ({
12 | documentFiles,
13 | }: {
14 | documentFiles: Types.DocumentFile[];
15 | }): { documentFiles: Types.DocumentFile[] } => {
16 | const files = documentFiles.map((file) => {
17 | if (!file.document) return file;
18 |
19 | file.document = visit(file.document, {
20 | Field: {
21 | leave(fieldNode) {
22 | if (!fieldNode.directives || !fieldNode.selectionSet) return fieldNode;
23 | const directives = getDirectives({ node: fieldNode, directiveNames: INSERT_NODE_DIRECTIVE_NAMES });
24 | if (directives.length === 0) return fieldNode;
25 |
26 | const selectionSet = addFieldToSelectionSetNodeWithoutDuplication({
27 | selectionSetNode: fieldNode.selectionSet,
28 | additionalFields: [idField, typenameField],
29 | });
30 |
31 | return { ...fieldNode, selectionSet };
32 | },
33 | },
34 | });
35 | return file;
36 | });
37 |
38 | return { documentFiles: files };
39 | };
40 |
--------------------------------------------------------------------------------
/packages/graphql-codegen-preset/src/transforms/generateRefetchQuery.ts:
--------------------------------------------------------------------------------
1 | import { Types } from '@graphql-codegen/plugin-helpers';
2 | import { ARGUMENT_DEFINITIONS_DIRECTIVE_NAME, REFETCHABLE_DIRECTIVE_NAME } from '@kazekyo/nau';
3 | import {
4 | DocumentNode,
5 | FragmentDefinitionNode,
6 | Kind,
7 | parse,
8 | parseType,
9 | print,
10 | VariableDefinitionNode,
11 | visit,
12 | } from 'graphql';
13 | import { uniqWith } from 'lodash';
14 | import { getFragmentDefinitionsByDocumentFiles } from '../utils/graphqlAST';
15 | import { nonNullable } from '../utils/nonNullable';
16 | import { getArgumentDefinitionDataList } from './util';
17 |
18 | export const transform = ({
19 | documentFiles,
20 | }: {
21 | documentFiles: Types.DocumentFile[];
22 | }): { documentFiles: Types.DocumentFile[] } => {
23 | const fragmentDefinitions = getFragmentDefinitionsByDocumentFiles(documentFiles);
24 |
25 | const refetchQueryMaterials: {
26 | [queryName: string]: { variableDefinitions: VariableDefinitionNode[]; innerFragmentName: string };
27 | } = {};
28 |
29 | fragmentDefinitions.forEach((definition) => {
30 | visit(definition, {
31 | Directive: {
32 | enter(node) {
33 | if (node.name.value !== REFETCHABLE_DIRECTIVE_NAME || !node.arguments) return false;
34 |
35 | const argument = node.arguments[0];
36 | if (argument.value.kind !== Kind.STRING) return false;
37 | const queryName = argument.value.value;
38 |
39 | const collectResult = collectVariableDefinitions({
40 | fragmentDefinition: definition,
41 | allFragmentDefinitions: fragmentDefinitions,
42 | collectedFragmentNames: [],
43 | });
44 |
45 | refetchQueryMaterials[queryName] = {
46 | innerFragmentName: definition.name.value,
47 | variableDefinitions: uniqWith(
48 | collectResult.variableDefinitions,
49 | (a, b) => a.variable.name.value === b.variable.name.value,
50 | ),
51 | };
52 | return false;
53 | },
54 | },
55 | });
56 | });
57 |
58 | const refetchDocumentNodes: DocumentNode[] = Object.entries(refetchQueryMaterials)
59 | .map(([queryName, material]) => {
60 | const documentNode = parse(queryString({ queryName, fragmentName: material.innerFragmentName }));
61 | if (documentNode.definitions[0].kind !== 'OperationDefinition') return null;
62 | const refetchDocumentNode: DocumentNode = {
63 | ...documentNode,
64 | definitions: [
65 | {
66 | ...documentNode.definitions[0],
67 | variableDefinitions: [
68 | ...material.variableDefinitions,
69 | {
70 | kind: Kind.VARIABLE_DEFINITION,
71 | type: parseType('ID!'),
72 | variable: { kind: Kind.VARIABLE, name: { kind: Kind.NAME, value: 'id' } },
73 | },
74 | ],
75 | },
76 | ],
77 | };
78 | return refetchDocumentNode;
79 | })
80 | .filter(nonNullable);
81 |
82 | const files = [
83 | ...documentFiles,
84 | ...refetchDocumentNodes.map((documentNode) => {
85 | return {
86 | document: documentNode,
87 | rawSDL: print(documentNode),
88 | location: 'generated by Nau',
89 | };
90 | }),
91 | ];
92 |
93 | return { documentFiles: files };
94 | };
95 |
96 | const queryString = ({ queryName, fragmentName }: { queryName: string; fragmentName: string }): string => {
97 | return `
98 | query ${queryName} {
99 | node(id: $id) {
100 | id
101 | __typename
102 | ...${fragmentName}
103 | }
104 | }`;
105 | };
106 |
107 | const collectVariableDefinitions = ({
108 | fragmentDefinition,
109 | allFragmentDefinitions,
110 | collectedFragmentNames,
111 | }: {
112 | fragmentDefinition: FragmentDefinitionNode;
113 | allFragmentDefinitions: FragmentDefinitionNode[];
114 | collectedFragmentNames: string[];
115 | }): { variableDefinitions: VariableDefinitionNode[]; collectedFragmentNames: string[] } => {
116 | let variableDefinitions: VariableDefinitionNode[] = [];
117 | let fragmentNames = [...collectedFragmentNames, fragmentDefinition.name.value];
118 |
119 | // Collect variable definitions
120 | visit(fragmentDefinition, {
121 | Directive: {
122 | enter(node) {
123 | if (node.name.value !== ARGUMENT_DEFINITIONS_DIRECTIVE_NAME) return;
124 | const argumentDataList = getArgumentDefinitionDataList(node);
125 | argumentDataList.forEach((data) =>
126 | variableDefinitions.push({
127 | kind: Kind.VARIABLE_DEFINITION,
128 | type: data.type,
129 | variable: { kind: Kind.VARIABLE, name: data.name },
130 | defaultValue: data.defaultValue,
131 | }),
132 | );
133 | return false;
134 | },
135 | },
136 | });
137 |
138 | // Visit nested fragments
139 | visit(fragmentDefinition, {
140 | FragmentSpread: {
141 | enter(node) {
142 | const fragmentName = node.name.value;
143 |
144 | if (fragmentNames.includes(fragmentName)) return false;
145 |
146 | const nestedFragment = allFragmentDefinitions.find((definition) => definition.name.value === fragmentName);
147 | if (!nestedFragment) return false;
148 |
149 | const nestedFragmentResult = collectVariableDefinitions({
150 | fragmentDefinition: nestedFragment,
151 | allFragmentDefinitions,
152 | collectedFragmentNames: fragmentNames,
153 | });
154 |
155 | variableDefinitions = [...variableDefinitions, ...nestedFragmentResult.variableDefinitions];
156 | fragmentNames = [...fragmentNames, ...nestedFragmentResult.collectedFragmentNames];
157 | return false;
158 | },
159 | },
160 | });
161 |
162 | return { variableDefinitions, collectedFragmentNames: fragmentNames };
163 | };
164 |
--------------------------------------------------------------------------------
/packages/graphql-codegen-preset/src/transforms/removeCustomDirective.ts:
--------------------------------------------------------------------------------
1 | import { Types } from '@graphql-codegen/plugin-helpers';
2 | import {
3 | ARGUMENTS_DIRECTIVE_NAME,
4 | ARGUMENT_DEFINITIONS_DIRECTIVE_NAME,
5 | PAGINATION_DIRECTIVE_NAME,
6 | REFETCHABLE_DIRECTIVE_NAME,
7 | } from '@kazekyo/nau';
8 | import { visit } from 'graphql';
9 |
10 | const DIRECTIVES = [
11 | ARGUMENT_DEFINITIONS_DIRECTIVE_NAME,
12 | ARGUMENTS_DIRECTIVE_NAME,
13 | REFETCHABLE_DIRECTIVE_NAME,
14 | PAGINATION_DIRECTIVE_NAME,
15 | ];
16 |
17 | export const transform = ({
18 | documentFiles,
19 | }: {
20 | documentFiles: Types.DocumentFile[];
21 | }): { documentFiles: Types.DocumentFile[] } => {
22 | const files = documentFiles.map((file) => {
23 | if (!file.document) return file;
24 |
25 | file.document = visit(file.document, {
26 | Directive: {
27 | enter(node) {
28 | if (DIRECTIVES.includes(node.name.value)) {
29 | return null;
30 | }
31 | },
32 | },
33 | });
34 | return file;
35 | });
36 |
37 | return { documentFiles: files };
38 | };
39 |
--------------------------------------------------------------------------------
/packages/graphql-codegen-preset/src/transforms/util.ts:
--------------------------------------------------------------------------------
1 | import { ARGUMENT_DEFINITIONS_DIRECTIVE_NAME } from '@kazekyo/nau';
2 | import { ConstValueNode, DirectiveNode, FragmentDefinitionNode, Kind, NameNode, parseType, TypeNode } from 'graphql';
3 | import { decode, encode } from 'js-base64';
4 |
5 | export const mergeCustomizer = (objValue: unknown, srcValue: unknown): unknown => {
6 | if (Array.isArray(objValue) && Array.isArray(srcValue)) {
7 | return objValue.concat(srcValue) as unknown[];
8 | }
9 | };
10 |
11 | export const FRAGMENT_NAME_INFO_ID_1 = '1';
12 | export const FRAGMENT_NAME_INFO_ID_2 = '2';
13 | export const FRAGMENT_NAME_INFO_ID_3 = '3';
14 |
15 | export const getUniqueFragmentName = (name: string, info: string): string => {
16 | const splitName = name.split('_');
17 | const endStr = splitName.pop();
18 | if (splitName.length === 1 || !endStr) return `${name}_${encode('n/' + info, true)}`;
19 |
20 | const decodedInfo = decode(endStr);
21 | const isAlreadyUniqueName = decodedInfo.startsWith('n/');
22 | if (!isAlreadyUniqueName) return `${name}_${encode('n/' + info, true)}`;
23 |
24 | return `${splitName.join('_')}_${encode(decodedInfo + '/' + info, true)}`;
25 | };
26 |
27 | export type ArgumentDefinitionData = { name: NameNode; type: TypeNode; defaultValue?: ConstValueNode };
28 | export const getArgumentDefinitionDataList = (node: DirectiveNode): ArgumentDefinitionData[] => {
29 | if (node.name.value !== ARGUMENT_DEFINITIONS_DIRECTIVE_NAME) return [];
30 | if (!node.arguments || node.arguments.length === 0) return [];
31 |
32 | const list: ArgumentDefinitionData[] = [];
33 |
34 | // Example: node is @argumentDefinitions(arg1: { type: "Int", defaultValue: 10 })
35 | node.arguments.forEach((argument) => {
36 | if (argument.value.kind !== 'ObjectValue') return;
37 | const name = argument.name; // Example: name is arg1
38 |
39 | const typeField = argument.value.fields.find((field) => field.name.value === 'type');
40 | let typeString: string | undefined;
41 | if (typeField && typeField.value.kind === Kind.STRING) {
42 | typeString = typeField.value.value; // Example: typeString is "Int"
43 | }
44 | if (!typeString) return;
45 |
46 | const defaultValueField = argument.value.fields.find((field) => field.name.value === 'defaultValue');
47 | const defaultValue = defaultValueField?.value; // Example: defaultValue is 10
48 | let defaultConstValue: ConstValueNode | undefined = undefined;
49 | if (defaultValue && defaultValue.kind !== Kind.VARIABLE) {
50 | defaultConstValue = defaultValue as ConstValueNode;
51 | }
52 |
53 | list.push({ name, type: parseType(typeString), defaultValue: defaultConstValue });
54 | });
55 | return list;
56 | };
57 |
58 | // TODO : Delete
59 | export type ChangedFragments = { [originalFragmentName: string]: FragmentDefinitionNode[] };
60 | export const addFragmentToChangedFragment = ({
61 | originalFragmentName,
62 | changedFragments,
63 | changedFragmentDefinition,
64 | }: {
65 | originalFragmentName: string;
66 | changedFragments: ChangedFragments;
67 | changedFragmentDefinition: FragmentDefinitionNode;
68 | }): ChangedFragments => {
69 | const definitions = changedFragments[originalFragmentName] || [];
70 | if (definitions.find((definition) => definition.name.value === changedFragmentDefinition.name.value)) {
71 | return changedFragments;
72 | }
73 | definitions.push(changedFragmentDefinition);
74 | changedFragments[originalFragmentName] = definitions;
75 | return changedFragments;
76 | };
77 |
78 | export const existsFragmentDefinitionInChangedFragments = ({
79 | changedFragments,
80 | newFragmentName,
81 | }: {
82 | changedFragments: ChangedFragments;
83 | newFragmentName: string;
84 | }): boolean => {
85 | const fragmentDefinitions = Object.entries(changedFragments)
86 | .map(([_, fragments]) => fragments)
87 | .flat();
88 | return !!fragmentDefinitions.find((definition) => definition.name.value === newFragmentName);
89 | };
90 |
--------------------------------------------------------------------------------
/packages/graphql-codegen-preset/src/utils/graphqlAST.ts:
--------------------------------------------------------------------------------
1 | import { Types } from '@graphql-codegen/plugin-helpers';
2 | import { DirectiveName } from '@kazekyo/nau';
3 | import {
4 | DirectiveNode,
5 | DocumentNode,
6 | FieldNode,
7 | FragmentDefinitionNode,
8 | Kind,
9 | OperationDefinitionNode,
10 | SelectionNode,
11 | SelectionSetNode,
12 | } from 'graphql';
13 | import { nonNullable } from './nonNullable';
14 |
15 | export const pageInfoField: FieldNode = {
16 | kind: Kind.FIELD,
17 | name: { kind: Kind.NAME, value: 'pageInfo' },
18 | selectionSet: {
19 | kind: Kind.SELECTION_SET,
20 | selections: [
21 | { kind: Kind.FIELD, name: { kind: Kind.NAME, value: 'hasNextPage' } },
22 | { kind: Kind.FIELD, name: { kind: Kind.NAME, value: 'endCursor' } },
23 | { kind: Kind.FIELD, name: { kind: Kind.NAME, value: 'hasPreviousPage' } },
24 | { kind: Kind.FIELD, name: { kind: Kind.NAME, value: 'startCursor' } },
25 | ],
26 | },
27 | };
28 |
29 | export const cursorField: FieldNode = { kind: Kind.FIELD, name: { kind: Kind.NAME, value: 'cursor' } };
30 |
31 | export const idField: FieldNode = { kind: Kind.FIELD, name: { kind: Kind.NAME, value: 'id' } };
32 |
33 | export const typenameField: FieldNode = { kind: Kind.FIELD, name: { kind: Kind.NAME, value: '__typename' } };
34 |
35 | export const nodeField: FieldNode = {
36 | kind: Kind.FIELD,
37 | name: { kind: Kind.NAME, value: 'node' },
38 | selectionSet: {
39 | kind: Kind.SELECTION_SET,
40 | selections: [idField, typenameField],
41 | },
42 | };
43 |
44 | export const edgesField: FieldNode = {
45 | kind: Kind.FIELD,
46 | name: { kind: Kind.NAME, value: 'edges' },
47 | selectionSet: {
48 | kind: Kind.SELECTION_SET,
49 | selections: [nodeField, cursorField],
50 | },
51 | };
52 |
53 | export const connectionIdField: FieldNode = {
54 | kind: Kind.FIELD,
55 | name: { kind: Kind.NAME, value: '_connectionId' },
56 | directives: [{ kind: Kind.DIRECTIVE, name: { kind: Kind.NAME, value: 'client' } }],
57 | };
58 |
59 | export function getOperationDefinitions(doc: DocumentNode): OperationDefinitionNode[] {
60 | return doc.definitions.filter((definition) => definition.kind === 'OperationDefinition') as OperationDefinitionNode[];
61 | }
62 |
63 | export const getFragmentDefinitions = (documentNode: DocumentNode): FragmentDefinitionNode[] => {
64 | return documentNode.definitions.filter(
65 | (definition): definition is FragmentDefinitionNode => definition.kind === 'FragmentDefinition',
66 | );
67 | };
68 |
69 | export const getFragmentDefinitionsByDocumentFiles = (
70 | documentFiles: Types.DocumentFile[],
71 | ): FragmentDefinitionNode[] => {
72 | return documentFiles
73 | .map((file) => file.document?.definitions)
74 | .filter(nonNullable)
75 | .flat()
76 | .filter((definition): definition is FragmentDefinitionNode => definition.kind === Kind.FRAGMENT_DEFINITION);
77 | };
78 |
79 | export const getFragmentDefinitionByName = ({
80 | fragmentDefinitions,
81 | fragmentName,
82 | }: {
83 | fragmentDefinitions: FragmentDefinitionNode[];
84 | fragmentName: string;
85 | }): FragmentDefinitionNode | undefined => {
86 | return fragmentDefinitions.find((definition) => definition.name.value === fragmentName);
87 | };
88 |
89 | export const getDirectives = ({
90 | node,
91 | directiveNames,
92 | }: {
93 | node: FieldNode | SelectionNode | null;
94 | directiveNames: DirectiveName[];
95 | }): DirectiveNode[] => {
96 | const directives = node?.directives?.filter((directive) =>
97 | directiveNames.find((name) => name === directive.name.value),
98 | );
99 | if (!directives) return [];
100 | return directives;
101 | };
102 |
103 | export const isSameNameFieldNode = ({ selection, name }: { selection: SelectionNode; name: string }): boolean => {
104 | return selection.kind === Kind.FIELD && selection.name.value === name;
105 | };
106 |
107 | // Add the field, but do nothing if the field already exists
108 | export const addFieldWithoutDuplication = ({
109 | fieldNode,
110 | additionalFields,
111 | }: {
112 | fieldNode: FieldNode;
113 | additionalFields: FieldNode[];
114 | }): FieldNode => {
115 | if (!fieldNode.selectionSet) return fieldNode;
116 | const selectionSet = addFieldToSelectionSetNodeWithoutDuplication({
117 | selectionSetNode: fieldNode.selectionSet,
118 | additionalFields,
119 | });
120 |
121 | return {
122 | ...fieldNode,
123 | selectionSet,
124 | };
125 | };
126 |
127 | // Add the field, but do nothing if the field already exists
128 | export const addFieldToSelectionSetNodeWithoutDuplication = ({
129 | selectionSetNode,
130 | additionalFields,
131 | }: {
132 | selectionSetNode: SelectionSetNode;
133 | additionalFields: FieldNode[];
134 | }): SelectionSetNode => {
135 | const selections = selectionSetNode.selections;
136 |
137 | const fieldNodes = additionalFields.filter(
138 | (fieldNode) => !selections.find((selection) => isSameNameFieldNode({ selection, name: fieldNode.name.value })),
139 | );
140 | if (fieldNodes.length === 0) return selectionSetNode;
141 |
142 | return {
143 | ...selectionSetNode,
144 | selections: [...selections, ...fieldNodes],
145 | };
146 | };
147 |
--------------------------------------------------------------------------------
/packages/graphql-codegen-preset/src/utils/graphqlSchema.ts:
--------------------------------------------------------------------------------
1 | import { GraphQLObjectType, GraphQLOutputType, GraphQLSchema, isListType, isNonNullType, isObjectType } from 'graphql';
2 | import { getFieldDef } from 'graphql/execution/execute';
3 | import { Maybe } from 'graphql/jsutils/Maybe';
4 | import { edgesField, nodeField } from './graphqlAST';
5 |
6 | export const getConnectionType = ({ type }: { type: Maybe }): GraphQLObjectType | undefined => {
7 | let connectionType = type;
8 | if (!connectionType) return;
9 | if (isNonNullType(connectionType)) {
10 | connectionType = connectionType.ofType as GraphQLObjectType;
11 | }
12 | if (!isObjectType(connectionType)) return;
13 | if (!connectionType.name.endsWith('Connection')) return;
14 | return connectionType;
15 | };
16 |
17 | export const getEdgeType = ({
18 | connectionType,
19 | schema,
20 | }: {
21 | connectionType: GraphQLObjectType;
22 | schema: GraphQLSchema;
23 | }): GraphQLObjectType | undefined => {
24 | const edgesFieldDef = getFieldDef(schema, connectionType, edgesField);
25 | if (!edgesFieldDef) return;
26 |
27 | let edgesType = edgesFieldDef.type;
28 | if (isNonNullType(edgesType)) {
29 | edgesType = edgesType.ofType;
30 | }
31 | if (!isListType(edgesType)) return;
32 |
33 | let edgeType = edgesType.ofType as Maybe;
34 | if (!edgeType) return;
35 |
36 | if (isNonNullType(edgeType)) {
37 | edgeType = edgeType.ofType;
38 | }
39 | if (!isObjectType(edgeType)) return;
40 |
41 | return edgeType;
42 | };
43 |
44 | export const getNodeType = ({
45 | edgeType,
46 | schema,
47 | }: {
48 | edgeType: GraphQLObjectType;
49 | schema: GraphQLSchema;
50 | }): GraphQLObjectType | undefined => {
51 | const nodeFieldDef = getFieldDef(schema, edgeType, nodeField);
52 | if (!nodeFieldDef) return;
53 |
54 | let nodeType = nodeFieldDef.type;
55 | if (!nodeType) return;
56 |
57 | if (isNonNullType(nodeType)) {
58 | nodeType = nodeType.ofType;
59 | }
60 | if (!isObjectType(nodeType)) return;
61 |
62 | return nodeType;
63 | };
64 |
--------------------------------------------------------------------------------
/packages/graphql-codegen-preset/src/utils/nonNullable.ts:
--------------------------------------------------------------------------------
1 | export const nonNullable = (value: T): value is NonNullable => value != null;
2 |
--------------------------------------------------------------------------------
/packages/graphql-codegen-preset/src/utils/testing/example.graphql:
--------------------------------------------------------------------------------
1 | directive @arguments on FRAGMENT_SPREAD
2 |
3 | directive @argumentDefinitions on FRAGMENT_DEFINITION
4 |
5 | directive @refetchable(queryName: String!) on FRAGMENT_DEFINITION
6 |
7 | directive @pagination on FIELD
8 |
9 | directive @appendNode(connections: [String!]) on FIELD
10 |
11 | directive @prependNode(connections: [String!]) on FIELD
12 |
13 | directive @deleteRecord(typename: String!) on FIELD
14 |
15 | directive @client on FIELD
16 |
17 | input AddItemInput {
18 | clientMutationId: String
19 | itemName: String!
20 | userId: ID!
21 | }
22 |
23 | type AddItemPayload {
24 | clientMutationId: String
25 | item: Item!
26 | }
27 |
28 | type Item implements Node {
29 | """
30 | The ID of an object
31 | """
32 | id: ID!
33 |
34 | """
35 | The name of the item.
36 | """
37 | name: String!
38 | }
39 |
40 | type ItemAddedPayload {
41 | item: Item!
42 | }
43 |
44 | """
45 | A connection to a list of items.
46 | """
47 | type ItemConnection {
48 | """
49 | A list of edges.
50 | """
51 | edges: [ItemEdge]
52 |
53 | """
54 | Information to aid in pagination.
55 | """
56 | pageInfo: PageInfo!
57 | }
58 |
59 | """
60 | An edge in a connection.
61 | """
62 | type ItemEdge {
63 | """
64 | A cursor for use in pagination
65 | """
66 | cursor: String!
67 |
68 | """
69 | The item at the end of the edge
70 | """
71 | node: Item
72 | }
73 |
74 | type ItemRemovedPayload {
75 | """
76 | The ID of an object
77 | """
78 | id: ID!
79 | }
80 |
81 | type Mutation {
82 | addItem(input: AddItemInput!): AddItemPayload
83 | removeItem(input: RemoveItemInput!): RemoveItemPayload
84 | updateItem(input: UpdateItemInput!): UpdateItemPayload
85 | }
86 |
87 | """
88 | An object with an ID
89 | """
90 | interface Node {
91 | """
92 | The id of the object.
93 | """
94 | id: ID!
95 | }
96 |
97 | """
98 | Information about pagination in a connection.
99 | """
100 | type PageInfo {
101 | """
102 | When paginating forwards, the cursor to continue.
103 | """
104 | endCursor: String
105 |
106 | """
107 | When paginating forwards, are there more items?
108 | """
109 | hasNextPage: Boolean!
110 |
111 | """
112 | When paginating backwards, are there more items?
113 | """
114 | hasPreviousPage: Boolean!
115 |
116 | """
117 | When paginating backwards, the cursor to continue.
118 | """
119 | startCursor: String
120 | }
121 |
122 | type Query {
123 | items(
124 | """
125 | Returns the items in the list that come after the specified cursor.
126 | """
127 | after: String
128 |
129 | """
130 | Returns the items in the list that come before the specified cursor.
131 | """
132 | before: String
133 |
134 | """
135 | Returns the first n items from the list.
136 | """
137 | first: Int
138 | keyword: String
139 |
140 | """
141 | Returns the last n items from the list.
142 | """
143 | last: Int
144 | ): ItemConnection!
145 |
146 | """
147 | Fetches an object given its ID
148 | """
149 | node(
150 | """
151 | The ID of an object
152 | """
153 | id: ID!
154 | ): Node
155 | viewer: User
156 | }
157 |
158 | input RemoveItemInput {
159 | clientMutationId: String
160 | itemId: String!
161 | userId: ID!
162 | }
163 |
164 | type RemoveItemPayload {
165 | clientMutationId: String
166 | removedItem: RemovedItem!
167 | }
168 |
169 | type RemovedItem {
170 | """
171 | The ID of an object
172 | """
173 | id: ID!
174 | }
175 |
176 | type Subscription {
177 | itemAdded: ItemAddedPayload!
178 | itemRemoved: ItemRemovedPayload!
179 | }
180 |
181 | input UpdateItemInput {
182 | clientMutationId: String
183 | itemId: ID!
184 | newItemName: String!
185 | }
186 |
187 | type UpdateItemPayload {
188 | clientMutationId: String
189 | item: Item!
190 | }
191 |
192 | type User implements Node {
193 | """
194 | The ID of an object
195 | """
196 | id: ID!
197 | items(
198 | """
199 | Returns the items in the list that come after the specified cursor.
200 | """
201 | after: String
202 |
203 | """
204 | Returns the items in the list that come before the specified cursor.
205 | """
206 | before: String
207 |
208 | """
209 | Returns the first n items from the list.
210 | """
211 | first: Int
212 | keyword: String
213 |
214 | """
215 | Returns the last n items from the list.
216 | """
217 | last: Int
218 | ): ItemConnection!
219 |
220 | """
221 | The name of the user.
222 | """
223 | name: String
224 | }
225 |
--------------------------------------------------------------------------------
/packages/graphql-codegen-preset/src/utils/testing/utils.ts:
--------------------------------------------------------------------------------
1 | import { Types } from '@graphql-codegen/plugin-helpers';
2 | import { readFileSync } from 'fs';
3 | import { buildSchema, concatAST, parse, print } from 'graphql';
4 | import path from 'path';
5 | import { nonNullable } from '../nonNullable';
6 |
7 | export const printDocuments = (documentFiles: Types.DocumentFile[]): string => {
8 | const ast = concatAST(documentFiles.map((v) => v.document).filter(nonNullable));
9 | return print(ast);
10 | };
11 |
12 | const filePath = path.join(__dirname, './example.graphql');
13 | const schemaString = readFileSync(filePath, { encoding: 'utf-8' });
14 | export const testGraphqlSchema = buildSchema(schemaString);
15 | export const testSchemaDocumentNode = parse(schemaString);
16 |
--------------------------------------------------------------------------------
/packages/graphql-codegen-preset/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "dist",
5 | "emitDeclarationOnly": true
6 | },
7 | "include": ["src/**/*"],
8 | "exclude": ["**/__tests__/", "**/*.test.ts", "**/testing/", "node_modules"]
9 | }
10 |
--------------------------------------------------------------------------------
/setupJest.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-var-requires */
2 | if (typeof global.TextEncoder === 'undefined') {
3 | global.TextEncoder = require('util').TextEncoder;
4 | }
5 |
6 | if (typeof global.TextDecoder === 'undefined') {
7 | global.TextDecoder = require('util').TextDecoder;
8 | }
9 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "dist",
5 | "emitDeclarationOnly": true
6 | },
7 | "include": ["src/**/*"],
8 | "exclude": ["**/__tests__/", "**/*.test.ts", "example", "**/testing/", "**/__test-utils__/**/*", "node_modules"]
9 | }
10 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "esModuleInterop": true,
5 | "declaration": true,
6 | "target": "es2019",
7 | "sourceMap": true,
8 | "baseUrl": ".",
9 | "outDir": "dist",
10 | "lib": ["es2019"],
11 | "strict": true,
12 | "moduleResolution": "node",
13 | "skipLibCheck": true,
14 | "jsx": "react"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------