├── .circleci └── config.yml ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── .vscode └── settings.json ├── README.md ├── package.json ├── packages ├── graphql-codegen-relay-optimizer-plugin │ ├── .eslintrc.json │ ├── .npmignore │ ├── LICENSE │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── __tests__ │ │ │ └── index.spec.ts │ │ ├── index.ts │ │ └── tyes.d.ts │ └── tsconfig.json └── todo-app-example │ ├── .eslintrc.json │ ├── .gitignore │ ├── README.md │ ├── codegen.yml │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json │ ├── server │ ├── .babelrc │ ├── README.md │ ├── data │ │ ├── database.js │ │ ├── schema.graphql │ │ └── schema │ │ │ ├── index.js │ │ │ ├── mutations │ │ │ ├── AddTodoMutation.js │ │ │ ├── ChangeTodoStatusMutation.js │ │ │ ├── MarkAllTodosMutation.js │ │ │ ├── RemoveCompletedTodosMutation.js │ │ │ ├── RemoveTodoMutation.js │ │ │ └── RenameTodoMutation.js │ │ │ ├── nodes.js │ │ │ └── queries │ │ │ └── UserQuery.js │ └── server.js │ ├── src │ ├── App.tsx │ ├── Option.ts │ ├── TodoApp.query.graphql │ ├── components │ │ ├── Todo.tsx │ │ ├── TodoApp.tsx │ │ ├── TodoApp_user.fragment.graphql │ │ ├── TodoList.tsx │ │ ├── TodoListFooter.tsx │ │ ├── TodoListFooter_user.fragment.graphql │ │ ├── TodoList_user.fragment.graphql │ │ ├── TodoTextInput.tsx │ │ ├── Todo_todo.fragment.graphql │ │ └── Todo_user.fragment.graphql │ ├── generated-types.tsx │ ├── index.tsx │ ├── mutations │ │ ├── AddTodoMutation.mutation.graphql │ │ ├── AddTodoMutation.ts │ │ ├── ChangeTodoStatusMutation.mutation.graphql │ │ ├── ChangeTodoStatusMutation.ts │ │ ├── MarkAllTodosMutation.mutation.graphql │ │ ├── MarkAllTodosMutation.ts │ │ ├── RemoveCompletedTodosMutation.mutation.graphql │ │ ├── RemoveCompletedTodosMutation.ts │ │ ├── RemoveTodoMutation.mutation.graphql │ │ ├── RemoveTodoMutation.ts │ │ ├── RenameTodoMutation.mutation.graphql │ │ └── RenameTodoMutation.ts │ └── react-app-env.d.ts │ └── tsconfig.json ├── renovate.json └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | references: 4 | js_deps_cache_key: &js_deps_cache_key v8-dependency-js-deps-{{ checksum "yarn.lock" }} 5 | js_deps_backup_cache_key: &js_deps_backup_cache_key v8-dependency-js-deps 6 | 7 | jobs: 8 | build: 9 | docker: 10 | - image: circleci/node:12.22.1 11 | working_directory: /tmp/workspace 12 | steps: 13 | - checkout 14 | - restore_cache: 15 | keys: 16 | - *js_deps_cache_key 17 | - *js_deps_backup_cache_key 18 | - run: 19 | name: Install dependencies 20 | command: yarn --pure-lockfile 21 | - run: 22 | name: Eslint 23 | command: yarn eslint 24 | - run: 25 | name: Build 26 | command: yarn build 27 | - run: 28 | name: Test 29 | command: yarn test 30 | - save_cache: 31 | key: dependency-cache-{{ checksum "package.json" }} 32 | paths: 33 | - node_modules 34 | 35 | workflows: 36 | version: 2 37 | build-deploy: 38 | jobs: 39 | - build 40 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "plugins": ["jest"], 4 | "extends": ["eslint:recommended", "plugin:jest/recommended", "prettier"], 5 | "rules": { 6 | "prefer-const": ["error"], 7 | "no-var": ["error"], 8 | "eqeqeq": ["error"], 9 | "camelcase": ["error"], 10 | "no-console": ["off"], 11 | "no-unused-vars": ["error"] 12 | }, 13 | "overrides": [ 14 | { 15 | "files": "__tests__/**/*.spec.js", 16 | "env": { 17 | "jest/globals": true 18 | } 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | *.log 4 | build 5 | dist 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | packages/todo-app-example/server/**/* 2 | dist 3 | node_modules 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "eslint.validate": [ 4 | "javascript", 5 | "javascriptreact", 6 | "typescript", 7 | "typescriptreact" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | Demonstration on how to use some of the benefits of relay with react-apollo (or any graphql framework) by using the relay-compiler as a graphql-codegen plugin to transform/optimize queries. 4 | 5 | Benefits: 6 | 7 | - More optimized Queries 8 | - Use Relay Features With Apollo 9 | - Fragment Arguments 10 | - Data Masking 11 | 12 | Read the introduction and setup guide over here: [Optimizing your Apollo Client Operations with GraphQL Code Generator and the Relay Compiler](https://medium.com/the-guild/optimizing-queries-with-the-graphql-code-generator-b8c37d692857) 13 | 14 | Checkout the example app here: [`todo-app-example`](packages/todo-app-example) 15 | 16 | # Packages [![CircleCI](https://circleci.com/gh/n1ru4l/graphql-codegen-relay-plugins/tree/master.svg?style=svg)](https://circleci.com/gh/n1ru4l/graphql-codegen-relay-plugins/tree/master) 17 | 18 | ## [`@n1ru4l/graphql-codegen-relay-optimizer-plugin`](packages/graphql-codegen-relay-optimizer-plugin) 19 | 20 | Use the relay-compiler foro ptimizing your GraphQL Queries. 21 | 22 | ## [`todo-app-example`](packages/todo-app-example) 23 | 24 | Example usage of the `packages/graphql-codegen-relay-optimizer-plugin` plugin. 25 | 26 | # TODO 27 | 28 | - [ ] Generate Fragment Types that can be used for the Components 29 | - [x] Add todo-app from relay-examples and convert it to react-apollo 30 | - [ ] Add support for `@relay(plural: Boolean)` 31 | - [ ] Add support for masking (https://relay.dev/docs/en/graphql-in-relay#relaymask-boolean) 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "license": "MIT", 4 | "workspaces": [ 5 | "packages/graphql-codegen-relay-optimizer-plugin", 6 | "packages/todo-app-example" 7 | ], 8 | "lint-staged": { 9 | "*.{js}": [ 10 | "eslint" 11 | ], 12 | "*.{js,json,css,md,ts,tsx}": [ 13 | "prettier --write" 14 | ] 15 | }, 16 | "scripts": { 17 | "precommit": "lint-staged", 18 | "postinstall": "yarn workspaces run build", 19 | "eslint": "eslint --ext .ts,.js,.tsx --ignore-path .gitignore .", 20 | "test": "yarn workspaces run test", 21 | "build": "yarn workspaces run build" 22 | }, 23 | "devDependencies": { 24 | "eslint": "6.8.0", 25 | "eslint-config-prettier": "6.11.0", 26 | "eslint-plugin-jest": "24.1.3", 27 | "husky": "4.3.8", 28 | "jest": "24.9.0", 29 | "lint-staged": "10.5.4", 30 | "prettier": "2.3.0", 31 | "ts-jest": "25.5.1" 32 | }, 33 | "version": "0.0.0" 34 | } 35 | -------------------------------------------------------------------------------- /packages/graphql-codegen-relay-optimizer-plugin/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "plugin:@typescript-eslint/recommended", 5 | "prettier", 6 | "prettier/@typescript-eslint" 7 | ], 8 | "env": { 9 | "es6": true 10 | }, 11 | "parserOptions": { 12 | "ecmaVersion": 2018, 13 | "sourceType": "module" 14 | }, 15 | "rules": { 16 | "@typescript-eslint/explicit-function-return-type": ["off"] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/graphql-codegen-relay-optimizer-plugin/.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | node_modules 3 | __tests__ 4 | !dist 5 | example 6 | -------------------------------------------------------------------------------- /packages/graphql-codegen-relay-optimizer-plugin/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-present Laurin Quast 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/graphql-codegen-relay-optimizer-plugin/README.md: -------------------------------------------------------------------------------- 1 | # @n1ru4l/graphql-codegen-relay-optimizer-plugin [![CircleCI](https://circleci.com/gh/n1ru4l/graphql-codegen-relay-plugins/tree/master.svg?style=svg)](https://circleci.com/gh/n1ru4l/graphql-codegen-relay-plugins/tree/master) 2 | 3 | ## Description 4 | 5 | [GraphQL Codegen Plugin](https://github.com/dotansimha/graphql-code-generator) for bringing the benefits of Relay to GraphQL Codegen. 6 | 7 | ### Current List of Features 8 | 9 | - [Optimize Queries](https://relay.dev/docs/principles-and-architecture/compiler-architecture/#transforms) TL;DR: reduce query size 10 | - Inline Fragments 11 | - Flatten Transform 12 | - Skip Redundant Node Transform 13 | - FragmentArguments 14 | - [`@argumentsDefinition`](https://relay.dev/docs/api-reference/graphql-and-directives/#argumentdefinitions) 15 | - [`@arguments`](https://relay.dev/docs/api-reference/graphql-and-directives/#arguments) 16 | 17 | ## Install Instructions [![npm](https://img.shields.io/npm/dm/@n1ru4l/graphql-codegen-relay-optimizer-plugin.svg)](https://www.npmjs.com/package/@n1ru4l/graphql-codegen-relay-optimizer-plugin) 18 | 19 | `yarn add -D -E @n1ru4l/graphql-codegen-relay-optimizer-plugin` 20 | 21 | ## Usage Instructions 22 | 23 | **codegen.yml** 24 | 25 | ```yaml 26 | overwrite: true 27 | schema: schema.graphql 28 | generates: 29 | src/generated-types.tsx: 30 | documents: "src/documents/**/*.graphql" 31 | config: 32 | skipDocumentsValidation: true 33 | plugins: 34 | - "@n1ru4l/graphql-codegen-relay-optimizer-plugin" 35 | - "typescript" 36 | - "typescript-operations" 37 | - "typescript-react-apollo" 38 | ``` 39 | -------------------------------------------------------------------------------- /packages/graphql-codegen-relay-optimizer-plugin/jest.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | "use strict"; 3 | 4 | module.exports = { 5 | roots: ["/src"], 6 | transform: { 7 | "^.+\\.tsx?$": "ts-jest" 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /packages/graphql-codegen-relay-optimizer-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@n1ru4l/graphql-codegen-relay-optimizer-plugin", 3 | "version": "5.0.0", 4 | "description": "GraphQL Code Generator plugin for optimizing your GraphQL queries relay style.", 5 | "license": "MIT", 6 | "main": "dist/main/index.js", 7 | "module": "dist/module/index.js", 8 | "author": { 9 | "name": "Laurin Quast", 10 | "email": "laurinquast@googlemail.com", 11 | "url": "https://github.com/n1ru4l" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/n1ru4l/graphql-codegen-relay-plugins" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/n1ru4l/graphql-codegen-relay-plugins/issues" 19 | }, 20 | "keywords": [ 21 | "graphql", 22 | "codegen", 23 | "graphql-codegen", 24 | "plugin", 25 | "relay" 26 | ], 27 | "peerDependencies": { 28 | "graphql": "14.x.x || 15.x.x" 29 | }, 30 | "dependencies": { 31 | "@graphql-codegen/plugin-helpers": "1.18.6", 32 | "relay-compiler": "10.1.3" 33 | }, 34 | "devDependencies": { 35 | "@graphql-codegen/testing": "1.17.7", 36 | "@types/jest": "24.9.1", 37 | "@types/relay-compiler": "8.0.0", 38 | "graphql": "15.5.0", 39 | "jest": "24.7.1", 40 | "ts-jest": "25.5.1", 41 | "typescript": "4.2.3" 42 | }, 43 | "scripts": { 44 | "test": "jest", 45 | "build:module": "tsc --target es2017 --outDir dist/module", 46 | "build:main": "tsc --target es5 --outDir dist/main", 47 | "build": "yarn build:module && yarn build:main" 48 | }, 49 | "files": [ 50 | "dist/**/*", 51 | "LICENSE", 52 | "README.md" 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /packages/graphql-codegen-relay-optimizer-plugin/src/__tests__/index.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-ignore */ 2 | import "@graphql-codegen/testing"; 3 | 4 | import { 5 | buildSchema, 6 | parse, 7 | print, 8 | ASTNode, 9 | ExecutableDefinitionNode, 10 | } from "graphql"; 11 | import { plugin } from ".."; 12 | import { Types } from "@graphql-codegen/plugin-helpers"; 13 | 14 | const testSchema = buildSchema(/* GraphQL */ ` 15 | type Avatar { 16 | id: ID! 17 | url: String! 18 | } 19 | 20 | type User { 21 | id: ID! 22 | login: String! 23 | avatar(height: Int!, width: Int!): Avatar 24 | } 25 | 26 | type Query { 27 | user: User! 28 | users: [User!]! 29 | } 30 | `); 31 | 32 | // eslint-disable-next-line jest/expect-expect 33 | it("can be called", async () => { 34 | const testDocument = parse(/* GraphQL */ ` 35 | query user { 36 | user { 37 | id 38 | } 39 | } 40 | `); 41 | await plugin(testSchema, [{ document: testDocument }], {}); 42 | }); 43 | 44 | // eslint-disable-next-line jest/expect-expect 45 | it("can be called with queries that include connection fragments", async () => { 46 | const testDocument = parse(/* GraphQL */ ` 47 | query user { 48 | users @connection(key: "foo") { 49 | id 50 | } 51 | } 52 | `); 53 | await plugin(testSchema, [{ document: testDocument }], {}); 54 | }); 55 | 56 | it("can inline @argumentDefinitions/@arguments annotated fragments", async () => { 57 | const fragmentDocument = parse(/* GraphQL */ ` 58 | fragment UserLogin on User 59 | @argumentDefinitions( 60 | height: { type: "Int", defaultValue: 10 } 61 | width: { type: "Int", defaultValue: 10 } 62 | ) { 63 | id 64 | login 65 | avatar(width: $width, height: $height) { 66 | id 67 | url 68 | } 69 | } 70 | `); 71 | const queryDocument = parse(/* GraphQL */ ` 72 | query user { 73 | users { 74 | ...UserLogin @arguments(height: 30, width: 30) 75 | } 76 | } 77 | `); 78 | const input: Types.DocumentFile[] = [ 79 | { document: fragmentDocument }, 80 | { document: queryDocument }, 81 | ]; 82 | await plugin(testSchema, input, {}); 83 | const queryDoc = input.find( 84 | (doc) => doc.document?.definitions[0].kind === "OperationDefinition" 85 | ); 86 | 87 | expect(queryDoc).toBeDefined(); 88 | expect(print(queryDoc?.document as ASTNode)) 89 | .toBeSimilarStringTo(/* GraphQL */ ` 90 | query user { 91 | users { 92 | id 93 | login 94 | avatar(width: 30, height: 30) { 95 | id 96 | url 97 | } 98 | } 99 | } 100 | `); 101 | }); 102 | 103 | it("handles unions with interfaces the correct way", async () => { 104 | const schema = buildSchema(/* GraphQL */ ` 105 | type User { 106 | id: ID! 107 | login: String! 108 | } 109 | 110 | interface Error { 111 | message: String! 112 | } 113 | 114 | type UserNotFoundError implements Error { 115 | message: String! 116 | } 117 | 118 | type UserBlockedError implements Error { 119 | message: String! 120 | } 121 | 122 | union UserResult = User | UserNotFoundError | UserBlockedError 123 | 124 | type Query { 125 | user: UserResult! 126 | } 127 | `); 128 | 129 | const queryDocument = parse(/* GraphQL */ ` 130 | query user { 131 | user { 132 | ... on User { 133 | id 134 | login 135 | } 136 | ... on Error { 137 | message 138 | } 139 | } 140 | } 141 | `); 142 | 143 | const input: Types.DocumentFile[] = [{ document: queryDocument }]; 144 | await plugin(schema, input, {}); 145 | const queryDoc = input.find( 146 | (doc) => doc.document?.definitions[0].kind === "OperationDefinition" 147 | ); 148 | 149 | expect(queryDoc).toBeDefined(); 150 | expect(print(queryDoc?.document as ASTNode)) 151 | .toBeSimilarStringTo(/* GraphQL */ ` 152 | query user { 153 | user { 154 | ... on User { 155 | id 156 | login 157 | } 158 | ... on Error { 159 | message 160 | } 161 | } 162 | } 163 | `); 164 | }); 165 | 166 | it("keeps original fragment with argument definitions", async () => { 167 | const schema = buildSchema(/* GraphQL */ ` 168 | type Query { 169 | foo(arg: String): String 170 | } 171 | `); 172 | 173 | const queryDocument = parse(/* GraphQL */ ` 174 | query QueryDocument($arg: String) { 175 | ...FragmentDocument @arguments(arg: $arg) 176 | } 177 | `); 178 | 179 | const fragmentDocument = parse(/* GraphQL */ ` 180 | fragment FragmentDocument on Query 181 | @argumentDefinitions(arg: { type: "String", defaultValue: null }) { 182 | foo(arg: $arg) 183 | } 184 | `); 185 | 186 | const input: Types.DocumentFile[] = [ 187 | { document: queryDocument }, 188 | { document: fragmentDocument }, 189 | ]; 190 | await plugin(schema, input, {}); 191 | const documents = new Set( 192 | input.map( 193 | (f) => 194 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 195 | (f.document!.definitions[0] as ExecutableDefinitionNode).name!.value 196 | ) 197 | ); 198 | expect(documents.has("FragmentDocument")).toEqual(true); 199 | expect(documents.has("QueryDocument")).toEqual(true); 200 | }); 201 | -------------------------------------------------------------------------------- /packages/graphql-codegen-relay-optimizer-plugin/src/index.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema, parse, printSchema, DefinitionNode } from "graphql"; 2 | import { Types, PluginFunction } from "@graphql-codegen/plugin-helpers"; 3 | 4 | import { Parser as RelayParser } from "relay-compiler"; 5 | import CompilerContext from "relay-compiler/lib/core/CompilerContext"; 6 | import { create as relayCreate } from "relay-compiler/lib/core/Schema"; 7 | import { print as relayPrint } from "relay-compiler/lib/core/IRPrinter"; 8 | 9 | import { transform as skipRedundantNodesTransform } from "relay-compiler/lib/transforms/SkipRedundantNodesTransform"; 10 | import { transform as inlineFragmentsTransform } from "relay-compiler/lib/transforms/InlineFragmentsTransform"; 11 | import { transform as applyFragmentArgumentTransform } from "relay-compiler/lib/transforms/ApplyFragmentArgumentTransform"; 12 | import { transformWithOptions as flattenTransformWithOptions } from "relay-compiler/lib/transforms/FlattenTransform"; 13 | 14 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 15 | export interface RelayOptimizerPluginConfig {} 16 | 17 | export const plugin: PluginFunction = ( 18 | schema: GraphQLSchema, 19 | documents: Types.DocumentFile[], 20 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 21 | _config: RelayOptimizerPluginConfig 22 | ) => { 23 | // @TODO way for users to define directives they use, otherwise relay will throw an unknown directive error 24 | // Maybe we can scan the queries and add them dynamically without users having to do some extra stuff 25 | // transformASTSchema creates a new schema instance instead of mutating the old one 26 | const adjustedSchema = relayCreate(printSchema(schema)).extend([ 27 | /* GraphQL */ ` 28 | directive @connection(key: String!, filter: [String!]) on FIELD 29 | directive @client on FIELD 30 | `, 31 | ]); 32 | 33 | const documentAsts = documents.reduce((prev, v) => { 34 | return [...prev, ...(v.document?.definitions ?? [])]; 35 | }, [] as DefinitionNode[]); 36 | 37 | const relayDocuments = RelayParser.transform(adjustedSchema, documentAsts); 38 | 39 | const fragmentCompilerContext = new CompilerContext(adjustedSchema).addAll( 40 | relayDocuments 41 | ); 42 | 43 | const fragmentDocuments = fragmentCompilerContext 44 | .applyTransforms([ 45 | applyFragmentArgumentTransform, 46 | flattenTransformWithOptions({ flattenAbstractTypes: false }), 47 | skipRedundantNodesTransform, 48 | ]) 49 | .documents() 50 | .filter((doc) => doc.kind === "Fragment"); 51 | 52 | const queryCompilerContext = new CompilerContext(adjustedSchema) 53 | .addAll(relayDocuments) 54 | .applyTransforms([ 55 | applyFragmentArgumentTransform, 56 | inlineFragmentsTransform, 57 | flattenTransformWithOptions({ flattenAbstractTypes: false }), 58 | skipRedundantNodesTransform, 59 | ]); 60 | 61 | const newQueryDocuments: Types.DocumentFile[] = queryCompilerContext 62 | .documents() 63 | .map((doc) => ({ 64 | location: "optimized by relay", 65 | document: parse(relayPrint(adjustedSchema, doc)), 66 | })); 67 | 68 | const newDocuments: Types.DocumentFile[] = [ 69 | ...fragmentDocuments.map((doc) => ({ 70 | location: "optimized by relay", 71 | document: parse(relayPrint(adjustedSchema, doc)), 72 | })), 73 | ...newQueryDocuments, 74 | ]; 75 | 76 | const newDocumentsFragmentsLookup = new Set(); 77 | for (const doc of newDocuments) { 78 | if (!doc.document?.definitions) { 79 | continue; 80 | } 81 | for (const definition of doc.document.definitions) { 82 | if (definition.kind === "FragmentDefinition") { 83 | newDocumentsFragmentsLookup.add(definition.name.value); 84 | } 85 | } 86 | } 87 | const oldDocuments = documents.splice(0, documents.length); 88 | documents.push(...newDocuments); 89 | 90 | for (const doc of oldDocuments) { 91 | if (!doc.document?.definitions) { 92 | continue; 93 | } 94 | for (const definition of doc.document.definitions) { 95 | if ( 96 | definition.kind === "FragmentDefinition" && 97 | !newDocumentsFragmentsLookup.has(definition.name.value) 98 | ) { 99 | documents.push({ 100 | document: { kind: "Document", definitions: [definition] }, 101 | }); 102 | } 103 | } 104 | } 105 | 106 | return { 107 | content: "", 108 | }; 109 | }; 110 | -------------------------------------------------------------------------------- /packages/graphql-codegen-relay-optimizer-plugin/src/tyes.d.ts: -------------------------------------------------------------------------------- 1 | declare module "relay-compiler/lib/core/Schema" { 2 | export function create(schema: string): import("relay-compiler").Schema; 3 | } 4 | 5 | declare module "relay-compiler/lib/core/IRPrinter" { 6 | export function print( 7 | schema: import("relay-compiler").Schema, 8 | document: any 9 | ): string; 10 | } 11 | 12 | declare module "relay-compiler/lib/core/CompilerContext" { 13 | let CompilerContext: typeof import("relay-compiler").CompilerContext; 14 | export = CompilerContext; 15 | } 16 | -------------------------------------------------------------------------------- /packages/graphql-codegen-relay-optimizer-plugin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "esModuleInterop": true, 5 | "declaration": true, 6 | "strict": true, 7 | "skipLibCheck": true 8 | }, 9 | "include": ["src"], 10 | "exclude": ["./**/*.spec.ts", "dist"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/todo-app-example/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "react-app" 3 | } 4 | -------------------------------------------------------------------------------- /packages/todo-app-example/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /packages/todo-app-example/README.md: -------------------------------------------------------------------------------- 1 | ## GraphQL Codegen Relay TodoMVC 2 | 3 | Demonstration of using `graphql-codegen-relay-plugin` with TodoMVC. 4 | Server code is based on [Relay TodoMVC](https://github.com/relayjs/relay-examples/tree/master/todo) 5 | Frontend code is also based on [Relay TodoMVC](https://github.com/relayjs/relay-examples/tree/master/todo) but ported over to ReactApollo (`@apollo/react-hooks`). 6 | 7 | ## Usage instructions 8 | 9 | - `yarn server` 10 | - `yarn start` 11 | - Visit TODO MVC App on `localhost:3000` 12 | 13 | ## Development Info 14 | 15 | - Frontend uses `create-react-app`. 16 | - GraphQL Server uns on port `3001` 17 | - Types are generated with the command `yarn generate:types` 18 | -------------------------------------------------------------------------------- /packages/todo-app-example/codegen.yml: -------------------------------------------------------------------------------- 1 | overwrite: true 2 | schema: server/data/schema.graphql 3 | generates: 4 | src/generated-types.tsx: 5 | documents: "src/**/*.graphql" 6 | config: 7 | skipDocumentsValidation: true 8 | withHOC: false 9 | withComponent: false 10 | withHooks: true 11 | reactApolloVersion: 2 12 | gqlImport: graphql.macro#gql 13 | plugins: 14 | - "@n1ru4l/graphql-codegen-relay-optimizer-plugin" 15 | - "typescript" 16 | - "typescript-operations" 17 | - "typescript-react-apollo" 18 | -------------------------------------------------------------------------------- /packages/todo-app-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todo-app-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@apollo/react-common": "3.1.4", 7 | "@apollo/react-hooks": "3.1.5", 8 | "apollo-cache-inmemory": "1.6.6", 9 | "apollo-client": "2.6.10", 10 | "apollo-link-http": "1.5.17", 11 | "classnames": "2.3.1", 12 | "express": "4.17.1", 13 | "express-graphql": "0.12.0", 14 | "graphql": "15.5.0", 15 | "graphql-relay": "0.6.0", 16 | "graphql-tag": "2.11.0", 17 | "react": "17.0.2", 18 | "react-dom": "17.0.2", 19 | "react-scripts": "3.4.4", 20 | "todomvc-app-css": "2.3.0", 21 | "todomvc-common": "1.0.5" 22 | }, 23 | "scripts": { 24 | "start": "react-scripts start", 25 | "build": "react-scripts build", 26 | "test": "react-scripts test --passWithNoTests", 27 | "eject": "react-scripts eject", 28 | "generate:types": "gql-gen --config codegen.yml", 29 | "server": "babel-node --config-file ./server/.babelrc ./server/server.js" 30 | }, 31 | "eslintConfig": { 32 | "extends": "react-app" 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | }, 46 | "devDependencies": { 47 | "@babel/node": "7.13.13", 48 | "@babel/preset-flow": "7.13.13", 49 | "@graphql-codegen/cli": "1.21.4", 50 | "@graphql-codegen/plugin-helpers": "1.18.6", 51 | "@graphql-codegen/typescript": "1.22.0", 52 | "@graphql-codegen/typescript-operations": "1.17.16", 53 | "@graphql-codegen/typescript-react-apollo": "2.2.4", 54 | "@n1ru4l/graphql-codegen-relay-optimizer-plugin": "*", 55 | "@types/classnames": "2.2.11", 56 | "@types/jest": "24.9.1", 57 | "@types/node": "12.19.15", 58 | "@types/react": "17.0.5", 59 | "@types/react-dom": "16.9.12", 60 | "graphql.macro": "1.4.2", 61 | "typescript": "4.2.3" 62 | }, 63 | "proxy": "http://localhost:3001/graphql" 64 | } 65 | -------------------------------------------------------------------------------- /packages/todo-app-example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1ru4l/graphql-codegen-relay-plugins/0a2dbd2334a82922d5b58e89595b6adde438b6e3/packages/todo-app-example/public/favicon.ico -------------------------------------------------------------------------------- /packages/todo-app-example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | React App 23 | 24 | 25 | 26 |
27 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /packages/todo-app-example/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 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /packages/todo-app-example/server/.babelrc: -------------------------------------------------------------------------------- 1 | {"presets": ["@babel/preset-env", "@babel/preset-flow"]} -------------------------------------------------------------------------------- /packages/todo-app-example/server/README.md: -------------------------------------------------------------------------------- 1 | All Content here is copied from https://github.com/relayjs/relay-examples/tree/master/todo 2 | -------------------------------------------------------------------------------- /packages/todo-app-example/server/data/database.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /** 3 | * This file provided by Facebook is for non-commercial testing and evaluation 4 | * purposes only. Facebook reserves all rights not expressly granted. 5 | * 6 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 7 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 8 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 9 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 10 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 11 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | */ 13 | 14 | export class Todo { 15 | +id: string; 16 | +text: string; 17 | +complete: boolean; 18 | 19 | constructor(id: string, text: string, complete: boolean) { 20 | this.id = id; 21 | this.text = text; 22 | this.complete = complete; 23 | } 24 | } 25 | 26 | export class User { 27 | +id: string; 28 | 29 | constructor(id: string) { 30 | this.id = id; 31 | } 32 | } 33 | 34 | // Mock authenticated ID 35 | export const USER_ID = 'me'; 36 | 37 | // Mock user database table 38 | const usersById: Map = new Map([[USER_ID, new User(USER_ID)]]); 39 | 40 | // Mock todo database table 41 | const todosById: Map = new Map(); 42 | const todoIdsByUser: Map> = new Map([ 43 | [USER_ID, []], 44 | ]); 45 | 46 | // Seed initial data 47 | let nextTodoId: number = 0; 48 | addTodo('Taste JavaScript', true); 49 | addTodo('Buy a unicorn', false); 50 | 51 | function getTodoIdsForUser(id: string): $ReadOnlyArray { 52 | return todoIdsByUser.get(id) || []; 53 | } 54 | 55 | export function addTodo(text: string, complete: boolean): string { 56 | const todo = new Todo(`${nextTodoId++}`, text, complete); 57 | todosById.set(todo.id, todo); 58 | 59 | const todoIdsForUser = getTodoIdsForUser(USER_ID); 60 | todoIdsByUser.set(USER_ID, todoIdsForUser.concat(todo.id)); 61 | 62 | return todo.id; 63 | } 64 | 65 | export function changeTodoStatus(id: string, complete: boolean) { 66 | const todo = getTodoOrThrow(id); 67 | 68 | // Replace with the modified complete value 69 | todosById.set(id, new Todo(id, todo.text, complete)); 70 | } 71 | 72 | // Private, for strongest typing, only export `getTodoOrThrow` 73 | function getTodo(id: string): ?Todo { 74 | return todosById.get(id); 75 | } 76 | 77 | export function getTodoOrThrow(id: string): Todo { 78 | const todo = getTodo(id); 79 | 80 | if (!todo) { 81 | throw new Error(`Invariant exception, Todo ${id} not found`); 82 | } 83 | 84 | return todo; 85 | } 86 | 87 | export function getTodos(status: string = 'any'): $ReadOnlyArray { 88 | const todoIdsForUser = getTodoIdsForUser(USER_ID); 89 | const todosForUser = todoIdsForUser.map(getTodoOrThrow); 90 | 91 | if (status === 'any') { 92 | return todosForUser; 93 | } 94 | 95 | return todosForUser.filter( 96 | (todo: Todo): boolean => todo.complete === (status === 'completed'), 97 | ); 98 | } 99 | 100 | // Private, for strongest typing, only export `getUserOrThrow` 101 | function getUser(id: string): ?User { 102 | return usersById.get(id); 103 | } 104 | 105 | export function getUserOrThrow(id: string): User { 106 | const user = getUser(id); 107 | 108 | if (!user) { 109 | throw new Error(`Invariant exception, User ${id} not found`); 110 | } 111 | 112 | return user; 113 | } 114 | 115 | export function markAllTodos(complete: boolean): $ReadOnlyArray { 116 | const todosToChange = getTodos().filter( 117 | (todo: Todo): boolean => todo.complete !== complete, 118 | ); 119 | 120 | todosToChange.forEach((todo: Todo): void => 121 | changeTodoStatus(todo.id, complete), 122 | ); 123 | 124 | return todosToChange.map((todo: Todo): string => todo.id); 125 | } 126 | 127 | export function removeTodo(id: string) { 128 | const todoIdsForUser = getTodoIdsForUser(USER_ID); 129 | 130 | // Remove from the users list 131 | todoIdsByUser.set( 132 | USER_ID, 133 | todoIdsForUser.filter((todoId: string): boolean => todoId !== id), 134 | ); 135 | 136 | // And also from the total list of Todos 137 | todosById.delete(id); 138 | } 139 | 140 | export function removeCompletedTodos(): $ReadOnlyArray { 141 | const todoIdsForUser = getTodoIdsForUser(USER_ID); 142 | 143 | const todoIdsToRemove = getTodos() 144 | .filter((todo: Todo): boolean => todo.complete) 145 | .map((todo: Todo): string => todo.id); 146 | 147 | // Remove from the users list 148 | todoIdsByUser.set( 149 | USER_ID, 150 | todoIdsForUser.filter( 151 | (todoId: string): boolean => !todoIdsToRemove.includes(todoId), 152 | ), 153 | ); 154 | 155 | // And also from the total list of Todos 156 | todoIdsToRemove.forEach((id: string): boolean => todosById.delete(id)); 157 | 158 | return todoIdsToRemove; 159 | } 160 | 161 | export function renameTodo(id: string, text: string) { 162 | const todo = getTodoOrThrow(id); 163 | 164 | // Replace with the modified text value 165 | todosById.set(id, new Todo(id, text, todo.complete)); 166 | } 167 | -------------------------------------------------------------------------------- /packages/todo-app-example/server/data/schema.graphql: -------------------------------------------------------------------------------- 1 | input AddTodoInput { 2 | text: String! 3 | userId: ID! 4 | clientMutationId: String 5 | } 6 | 7 | type AddTodoPayload { 8 | todoEdge: TodoEdge! 9 | user: User! 10 | clientMutationId: String 11 | } 12 | 13 | input ChangeTodoStatusInput { 14 | complete: Boolean! 15 | id: ID! 16 | userId: ID! 17 | clientMutationId: String 18 | } 19 | 20 | type ChangeTodoStatusPayload { 21 | todo: Todo! 22 | user: User! 23 | clientMutationId: String 24 | } 25 | 26 | input MarkAllTodosInput { 27 | complete: Boolean! 28 | userId: ID! 29 | clientMutationId: String 30 | } 31 | 32 | type MarkAllTodosPayload { 33 | changedTodos: [Todo!] 34 | user: User! 35 | clientMutationId: String 36 | } 37 | 38 | type Mutation { 39 | addTodo(input: AddTodoInput!): AddTodoPayload 40 | changeTodoStatus(input: ChangeTodoStatusInput!): ChangeTodoStatusPayload 41 | markAllTodos(input: MarkAllTodosInput!): MarkAllTodosPayload 42 | removeCompletedTodos(input: RemoveCompletedTodosInput!): RemoveCompletedTodosPayload 43 | removeTodo(input: RemoveTodoInput!): RemoveTodoPayload 44 | renameTodo(input: RenameTodoInput!): RenameTodoPayload 45 | } 46 | 47 | """An object with an ID""" 48 | interface Node { 49 | """The id of the object.""" 50 | id: ID! 51 | } 52 | 53 | """Information about pagination in a connection.""" 54 | type PageInfo { 55 | """When paginating forwards, are there more items?""" 56 | hasNextPage: Boolean! 57 | 58 | """When paginating backwards, are there more items?""" 59 | hasPreviousPage: Boolean! 60 | """When paginating backwards, the cursor to continue.""" 61 | startCursor: String 62 | """When paginating forwards, the cursor to continue.""" 63 | endCursor: String 64 | } 65 | type Query { 66 | user(id: String): User 67 | """Fetches an object given its ID""" 68 | node( 69 | """The ID of an object""" 70 | id: ID! 71 | ): Node 72 | } 73 | input RemoveCompletedTodosInput { 74 | userId: ID! 75 | clientMutationId: String 76 | } 77 | type RemoveCompletedTodosPayload { 78 | deletedTodoIds: [String!] 79 | user: User! 80 | clientMutationId: String 81 | } 82 | input RemoveTodoInput { 83 | id: ID! 84 | userId: ID! 85 | clientMutationId: String 86 | } 87 | type RemoveTodoPayload { 88 | deletedTodoId: ID! 89 | user: User! 90 | clientMutationId: String 91 | } 92 | input RenameTodoInput { 93 | id: ID! 94 | text: String! 95 | clientMutationId: String 96 | } 97 | type RenameTodoPayload { 98 | todo: Todo! 99 | clientMutationId: String 100 | } 101 | type Todo implements Node { 102 | """The ID of an object""" 103 | id: ID! 104 | text: String! 105 | complete: Boolean! 106 | } 107 | """A connection to a list of items.""" 108 | type TodoConnection { 109 | """Information to aid in pagination.""" 110 | pageInfo: PageInfo! 111 | """A list of edges.""" 112 | edges: [TodoEdge] 113 | } 114 | """An edge in a connection.""" 115 | type TodoEdge { 116 | """The item at the end of the edge""" 117 | node: Todo 118 | """A cursor for use in pagination""" 119 | cursor: String! 120 | } 121 | type User implements Node { 122 | """The ID of an object""" 123 | id: ID! 124 | userId: String! 125 | todos(status: String = "any", after: String, first: Int, before: String, last: Int): TodoConnection 126 | totalCount: Int! 127 | completedCount: Int! 128 | } 129 | -------------------------------------------------------------------------------- /packages/todo-app-example/server/data/schema/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /** 3 | * This file provided by Facebook is for non-commercial testing and evaluation 4 | * purposes only. Facebook reserves all rights not expressly granted. 5 | * 6 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 7 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 8 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 9 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 10 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 11 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | */ 13 | 14 | import {GraphQLObjectType, GraphQLSchema} from 'graphql'; 15 | 16 | import {nodeField} from './nodes.js'; 17 | import {UserQuery} from './queries/UserQuery'; 18 | import {AddTodoMutation} from './mutations/AddTodoMutation'; 19 | import {ChangeTodoStatusMutation} from './mutations/ChangeTodoStatusMutation'; 20 | import {MarkAllTodosMutation} from './mutations/MarkAllTodosMutation'; 21 | import {RemoveCompletedTodosMutation} from './mutations/RemoveCompletedTodosMutation'; 22 | import {RemoveTodoMutation} from './mutations/RemoveTodoMutation'; 23 | import {RenameTodoMutation} from './mutations/RenameTodoMutation'; 24 | 25 | const Query = new GraphQLObjectType({ 26 | name: 'Query', 27 | fields: { 28 | user: UserQuery, 29 | node: nodeField, 30 | }, 31 | }); 32 | 33 | const Mutation = new GraphQLObjectType({ 34 | name: 'Mutation', 35 | fields: { 36 | addTodo: AddTodoMutation, 37 | changeTodoStatus: ChangeTodoStatusMutation, 38 | markAllTodos: MarkAllTodosMutation, 39 | removeCompletedTodos: RemoveCompletedTodosMutation, 40 | removeTodo: RemoveTodoMutation, 41 | renameTodo: RenameTodoMutation, 42 | }, 43 | }); 44 | 45 | export const schema = new GraphQLSchema({ 46 | query: Query, 47 | mutation: Mutation, 48 | }); 49 | -------------------------------------------------------------------------------- /packages/todo-app-example/server/data/schema/mutations/AddTodoMutation.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /* graphql-relay doesn't export types, and isn't in flow-typed. This gets too messy */ 3 | /* eslint flowtype/require-return-type: 'off' */ 4 | /** 5 | * This file provided by Facebook is for non-commercial testing and evaluation 6 | * purposes only. Facebook reserves all rights not expressly granted. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 9 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 10 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 11 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 12 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 13 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | */ 15 | 16 | import { 17 | cursorForObjectInConnection, 18 | mutationWithClientMutationId, 19 | } from 'graphql-relay'; 20 | 21 | import {GraphQLID, GraphQLNonNull, GraphQLString} from 'graphql'; 22 | import {GraphQLTodoEdge, GraphQLUser} from '../nodes'; 23 | 24 | import { 25 | addTodo, 26 | getTodoOrThrow, 27 | getTodos, 28 | getUserOrThrow, 29 | User, 30 | } from '../../database'; 31 | 32 | type Input = {| 33 | +text: string, 34 | +userId: string, 35 | |}; 36 | 37 | type Payload = {| 38 | +todoId: string, 39 | +userId: string, 40 | |}; 41 | 42 | const AddTodoMutation = mutationWithClientMutationId({ 43 | name: 'AddTodo', 44 | inputFields: { 45 | text: {type: new GraphQLNonNull(GraphQLString)}, 46 | userId: {type: new GraphQLNonNull(GraphQLID)}, 47 | }, 48 | outputFields: { 49 | todoEdge: { 50 | type: new GraphQLNonNull(GraphQLTodoEdge), 51 | resolve: ({todoId}: Payload) => { 52 | const todo = getTodoOrThrow(todoId); 53 | 54 | return { 55 | cursor: cursorForObjectInConnection([...getTodos()], todo), 56 | node: todo, 57 | }; 58 | }, 59 | }, 60 | user: { 61 | type: new GraphQLNonNull(GraphQLUser), 62 | resolve: ({userId}: Payload): User => getUserOrThrow(userId), 63 | }, 64 | }, 65 | mutateAndGetPayload: ({text, userId}: Input): Payload => { 66 | const todoId = addTodo(text, false); 67 | 68 | return {todoId, userId}; 69 | }, 70 | }); 71 | 72 | export {AddTodoMutation}; 73 | -------------------------------------------------------------------------------- /packages/todo-app-example/server/data/schema/mutations/ChangeTodoStatusMutation.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /* graphql-relay doesn't export types, and isn't in flow-typed. This gets too messy */ 3 | /* eslint flowtype/require-return-type: 'off' */ 4 | /** 5 | * This file provided by Facebook is for non-commercial testing and evaluation 6 | * purposes only. Facebook reserves all rights not expressly granted. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 9 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 10 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 11 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 12 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 13 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | */ 15 | 16 | import {fromGlobalId, mutationWithClientMutationId} from 'graphql-relay'; 17 | import {GraphQLBoolean, GraphQLID, GraphQLNonNull} from 'graphql'; 18 | import {GraphQLTodo, GraphQLUser} from '../nodes'; 19 | import { 20 | changeTodoStatus, 21 | getTodoOrThrow, 22 | getUserOrThrow, 23 | Todo, 24 | User, 25 | } from '../../database'; 26 | 27 | type Input = {| 28 | +complete: boolean, 29 | +id: string, 30 | +userId: string, 31 | |}; 32 | 33 | type Payload = {| 34 | +todoId: string, 35 | +userId: string, 36 | |}; 37 | 38 | const ChangeTodoStatusMutation = mutationWithClientMutationId({ 39 | name: 'ChangeTodoStatus', 40 | inputFields: { 41 | complete: {type: new GraphQLNonNull(GraphQLBoolean)}, 42 | id: {type: new GraphQLNonNull(GraphQLID)}, 43 | userId: {type: new GraphQLNonNull(GraphQLID)}, 44 | }, 45 | outputFields: { 46 | todo: { 47 | type: new GraphQLNonNull(GraphQLTodo), 48 | resolve: ({todoId}: Payload): Todo => getTodoOrThrow(todoId), 49 | }, 50 | user: { 51 | type: new GraphQLNonNull(GraphQLUser), 52 | resolve: ({userId}: Payload): User => getUserOrThrow(userId), 53 | }, 54 | }, 55 | mutateAndGetPayload: ({id, complete, userId}: Input): Payload => { 56 | const todoId = fromGlobalId(id).id; 57 | changeTodoStatus(todoId, complete); 58 | 59 | return {todoId, userId}; 60 | }, 61 | }); 62 | 63 | export {ChangeTodoStatusMutation}; 64 | -------------------------------------------------------------------------------- /packages/todo-app-example/server/data/schema/mutations/MarkAllTodosMutation.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /* graphql-relay doesn't export types, and isn't in flow-typed. This gets too messy */ 3 | /* eslint flowtype/require-return-type: 'off' */ 4 | /** 5 | * This file provided by Facebook is for non-commercial testing and evaluation 6 | * purposes only. Facebook reserves all rights not expressly granted. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 9 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 10 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 11 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 12 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 13 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | */ 15 | 16 | import {mutationWithClientMutationId} from 'graphql-relay'; 17 | import {GraphQLBoolean, GraphQLID, GraphQLList, GraphQLNonNull} from 'graphql'; 18 | import {GraphQLTodo, GraphQLUser} from '../nodes'; 19 | 20 | import { 21 | getTodoOrThrow, 22 | getUserOrThrow, 23 | markAllTodos, 24 | Todo, 25 | User, 26 | } from '../../database'; 27 | 28 | type Input = {| 29 | +complete: boolean, 30 | +userId: string, 31 | |}; 32 | 33 | type Payload = {| 34 | +changedTodoIds: $ReadOnlyArray, 35 | +userId: string, 36 | |}; 37 | 38 | const MarkAllTodosMutation = mutationWithClientMutationId({ 39 | name: 'MarkAllTodos', 40 | inputFields: { 41 | complete: {type: new GraphQLNonNull(GraphQLBoolean)}, 42 | userId: {type: new GraphQLNonNull(GraphQLID)}, 43 | }, 44 | outputFields: { 45 | changedTodos: { 46 | type: new GraphQLList(new GraphQLNonNull(GraphQLTodo)), 47 | resolve: ({changedTodoIds}: Payload): $ReadOnlyArray => 48 | changedTodoIds.map((todoId: string): Todo => getTodoOrThrow(todoId)), 49 | }, 50 | user: { 51 | type: new GraphQLNonNull(GraphQLUser), 52 | resolve: ({userId}: Payload): User => getUserOrThrow(userId), 53 | }, 54 | }, 55 | mutateAndGetPayload: ({complete, userId}: Input): Payload => { 56 | const changedTodoIds = markAllTodos(complete); 57 | 58 | return {changedTodoIds, userId}; 59 | }, 60 | }); 61 | 62 | export {MarkAllTodosMutation}; 63 | -------------------------------------------------------------------------------- /packages/todo-app-example/server/data/schema/mutations/RemoveCompletedTodosMutation.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /* graphql-relay doesn't export types, and isn't in flow-typed. This gets too messy */ 3 | /* eslint flowtype/require-return-type: 'off' */ 4 | /** 5 | * This file provided by Facebook is for non-commercial testing and evaluation 6 | * purposes only. Facebook reserves all rights not expressly granted. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 9 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 10 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 11 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 12 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 13 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | */ 15 | 16 | import {mutationWithClientMutationId, toGlobalId} from 'graphql-relay'; 17 | import {GraphQLID, GraphQLList, GraphQLNonNull, GraphQLString} from 'graphql'; 18 | import {GraphQLUser} from '../nodes'; 19 | import {getUserOrThrow, removeCompletedTodos, User} from '../../database'; 20 | 21 | type Input = {| 22 | +userId: string, 23 | |}; 24 | 25 | type Payload = {| 26 | +deletedTodoIds: $ReadOnlyArray, 27 | +userId: string, 28 | |}; 29 | 30 | const RemoveCompletedTodosMutation = mutationWithClientMutationId({ 31 | name: 'RemoveCompletedTodos', 32 | inputFields: { 33 | userId: {type: new GraphQLNonNull(GraphQLID)}, 34 | }, 35 | outputFields: { 36 | deletedTodoIds: { 37 | type: new GraphQLList(new GraphQLNonNull(GraphQLString)), 38 | resolve: ({deletedTodoIds}: Payload): $ReadOnlyArray => 39 | deletedTodoIds, 40 | }, 41 | user: { 42 | type: new GraphQLNonNull(GraphQLUser), 43 | resolve: ({userId}: Payload): User => getUserOrThrow(userId), 44 | }, 45 | }, 46 | mutateAndGetPayload: ({userId}: Input): Payload => { 47 | const deletedTodoLocalIds = removeCompletedTodos(); 48 | 49 | const deletedTodoIds = deletedTodoLocalIds.map( 50 | toGlobalId.bind(null, 'Todo'), 51 | ); 52 | 53 | return {deletedTodoIds, userId}; 54 | }, 55 | }); 56 | 57 | export {RemoveCompletedTodosMutation}; 58 | -------------------------------------------------------------------------------- /packages/todo-app-example/server/data/schema/mutations/RemoveTodoMutation.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /* graphql-relay doesn't export types, and isn't in flow-typed. This gets too messy */ 3 | /* eslint flowtype/require-return-type: 'off' */ 4 | /** 5 | * This file provided by Facebook is for non-commercial testing and evaluation 6 | * purposes only. Facebook reserves all rights not expressly granted. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 9 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 10 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 11 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 12 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 13 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | */ 15 | 16 | import {mutationWithClientMutationId, fromGlobalId} from 'graphql-relay'; 17 | import {GraphQLID, GraphQLNonNull} from 'graphql'; 18 | import {GraphQLUser} from '../nodes'; 19 | import {getUserOrThrow, removeTodo, User} from '../../database'; 20 | 21 | type Input = {| 22 | +id: string, 23 | +userId: string, 24 | |}; 25 | 26 | type Payload = {| 27 | +id: string, 28 | +userId: string, 29 | |}; 30 | 31 | const RemoveTodoMutation = mutationWithClientMutationId({ 32 | name: 'RemoveTodo', 33 | inputFields: { 34 | id: {type: new GraphQLNonNull(GraphQLID)}, 35 | userId: {type: new GraphQLNonNull(GraphQLID)}, 36 | }, 37 | outputFields: { 38 | deletedTodoId: { 39 | type: new GraphQLNonNull(GraphQLID), 40 | resolve: ({id}: Payload): string => id, 41 | }, 42 | user: { 43 | type: new GraphQLNonNull(GraphQLUser), 44 | resolve: ({userId}: Payload): User => getUserOrThrow(userId), 45 | }, 46 | }, 47 | mutateAndGetPayload: ({id, userId}: Input): Payload => { 48 | const localTodoId = fromGlobalId(id).id; 49 | removeTodo(localTodoId); 50 | 51 | return {id, userId}; 52 | }, 53 | }); 54 | 55 | export {RemoveTodoMutation}; 56 | -------------------------------------------------------------------------------- /packages/todo-app-example/server/data/schema/mutations/RenameTodoMutation.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /* graphql-relay doesn't export types, and isn't in flow-typed. This gets too messy */ 3 | /* eslint flowtype/require-return-type: 'off' */ 4 | /** 5 | * This file provided by Facebook is for non-commercial testing and evaluation 6 | * purposes only. Facebook reserves all rights not expressly granted. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 9 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 10 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 11 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 12 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 13 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | */ 15 | 16 | import {mutationWithClientMutationId, fromGlobalId} from 'graphql-relay'; 17 | import {GraphQLID, GraphQLNonNull, GraphQLString} from 'graphql'; 18 | import {GraphQLTodo} from '../nodes'; 19 | import {getTodoOrThrow, renameTodo, Todo} from '../../database'; 20 | 21 | type Input = {| 22 | +id: string, 23 | +text: string, 24 | |}; 25 | 26 | type Payload = {| 27 | +localTodoId: string, 28 | |}; 29 | 30 | const RenameTodoMutation = mutationWithClientMutationId({ 31 | name: 'RenameTodo', 32 | inputFields: { 33 | id: {type: new GraphQLNonNull(GraphQLID)}, 34 | text: {type: new GraphQLNonNull(GraphQLString)}, 35 | }, 36 | outputFields: { 37 | todo: { 38 | type: new GraphQLNonNull(GraphQLTodo), 39 | resolve: ({localTodoId}: Payload): Todo => getTodoOrThrow(localTodoId), 40 | }, 41 | }, 42 | mutateAndGetPayload: ({id, text}: Input): Payload => { 43 | const localTodoId = fromGlobalId(id).id; 44 | renameTodo(localTodoId, text); 45 | 46 | return {localTodoId}; 47 | }, 48 | }); 49 | 50 | export {RenameTodoMutation}; 51 | -------------------------------------------------------------------------------- /packages/todo-app-example/server/data/schema/nodes.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /* eslint flowtype/require-return-type: 'off' */ 3 | /** 4 | * This file provided by Facebook is for non-commercial testing and evaluation 5 | * purposes only. Facebook reserves all rights not expressly granted. 6 | * 7 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 8 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 9 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 10 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 11 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 12 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 13 | */ 14 | 15 | import { 16 | GraphQLBoolean, 17 | GraphQLInt, 18 | GraphQLNonNull, 19 | GraphQLObjectType, 20 | GraphQLString, 21 | } from 'graphql'; 22 | 23 | import { 24 | connectionArgs, 25 | connectionDefinitions, 26 | connectionFromArray, 27 | fromGlobalId, 28 | globalIdField, 29 | nodeDefinitions, 30 | } from 'graphql-relay'; 31 | 32 | import { 33 | Todo, 34 | User, 35 | USER_ID, 36 | getTodoOrThrow, 37 | getTodos, 38 | getUserOrThrow, 39 | } from '../database'; 40 | 41 | // $FlowFixMe graphql-relay types not available in flow-typed, strengthen this typing 42 | const {nodeInterface, nodeField} = nodeDefinitions( 43 | (globalId: string): ?{} => { 44 | const {type, id}: {id: string, type: string} = fromGlobalId(globalId); 45 | 46 | if (type === 'Todo') { 47 | return getTodoOrThrow(id); 48 | } else if (type === 'User') { 49 | return getUserOrThrow(id); 50 | } 51 | return null; 52 | }, 53 | (obj: {}): ?GraphQLObjectType => { 54 | if (obj instanceof Todo) { 55 | return GraphQLTodo; 56 | } else if (obj instanceof User) { 57 | return GraphQLUser; 58 | } 59 | return null; 60 | }, 61 | ); 62 | 63 | const GraphQLTodo = new GraphQLObjectType({ 64 | name: 'Todo', 65 | fields: { 66 | id: globalIdField('Todo'), 67 | text: { 68 | type: new GraphQLNonNull(GraphQLString), 69 | resolve: (todo: Todo): string => todo.text, 70 | }, 71 | complete: { 72 | type: new GraphQLNonNull(GraphQLBoolean), 73 | resolve: (todo: Todo): boolean => todo.complete, 74 | }, 75 | }, 76 | interfaces: [nodeInterface], 77 | }); 78 | 79 | const { 80 | connectionType: TodosConnection, 81 | edgeType: GraphQLTodoEdge, 82 | } = connectionDefinitions({ 83 | name: 'Todo', 84 | nodeType: GraphQLTodo, 85 | }); 86 | 87 | const GraphQLUser = new GraphQLObjectType({ 88 | name: 'User', 89 | fields: { 90 | id: globalIdField('User'), 91 | userId: { 92 | type: new GraphQLNonNull(GraphQLString), 93 | resolve: (): string => USER_ID, 94 | }, 95 | todos: { 96 | type: TodosConnection, 97 | args: { 98 | status: { 99 | type: GraphQLString, 100 | defaultValue: 'any', 101 | }, 102 | ...connectionArgs, 103 | }, 104 | // eslint-disable-next-line flowtype/require-parameter-type 105 | resolve: (root: {}, {status, after, before, first, last}) => 106 | connectionFromArray([...getTodos(status)], { 107 | after, 108 | before, 109 | first, 110 | last, 111 | }), 112 | }, 113 | totalCount: { 114 | type: new GraphQLNonNull(GraphQLInt), 115 | resolve: (): number => getTodos().length, 116 | }, 117 | completedCount: { 118 | type: new GraphQLNonNull(GraphQLInt), 119 | resolve: (): number => getTodos('completed').length, 120 | }, 121 | }, 122 | interfaces: [nodeInterface], 123 | }); 124 | 125 | export {nodeField, GraphQLTodo, GraphQLTodoEdge, GraphQLUser}; 126 | -------------------------------------------------------------------------------- /packages/todo-app-example/server/data/schema/queries/UserQuery.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /* graphql-relay doesn't export types, and isn't in flow-typed. This gets too messy */ 3 | /* eslint flowtype/require-return-type: 'off' */ 4 | /** 5 | * This file provided by Facebook is for non-commercial testing and evaluation 6 | * purposes only. Facebook reserves all rights not expressly granted. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 9 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 10 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 11 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 12 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 13 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | */ 15 | 16 | import {GraphQLString} from 'graphql'; 17 | import {GraphQLUser} from '../nodes'; 18 | import {User, getUserOrThrow} from '../../database'; 19 | 20 | type Input = { 21 | +id: string, 22 | }; 23 | 24 | const UserQuery = { 25 | type: GraphQLUser, 26 | args: { 27 | id: {type: GraphQLString}, 28 | }, 29 | resolve: (root: {}, {id}: Input): User => getUserOrThrow(id), 30 | }; 31 | 32 | export {UserQuery}; 33 | -------------------------------------------------------------------------------- /packages/todo-app-example/server/server.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /** 3 | * This file provided by Facebook is for non-commercial testing and evaluation 4 | * purposes only. Facebook reserves all rights not expressly granted. 5 | * 6 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 7 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 8 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 9 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 10 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 11 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | */ 13 | 14 | import express from 'express'; 15 | import graphQLHTTP from 'express-graphql'; 16 | import path from 'path'; 17 | import {schema} from './data/schema' 18 | 19 | const APP_PORT: number = 3001; 20 | 21 | const app = express() 22 | 23 | // Serve static resources 24 | app.use('/', express.static(path.resolve(__dirname, 'public'))); 25 | 26 | // Setup GraphQL endpoint 27 | app.use( 28 | '/graphql', 29 | graphQLHTTP({ 30 | schema: schema, 31 | pretty: true, 32 | }), 33 | ); 34 | 35 | app.listen(APP_PORT, () => { 36 | console.log(`App is now running on http://localhost:${APP_PORT}`); 37 | }); 38 | -------------------------------------------------------------------------------- /packages/todo-app-example/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useAppQueryQuery } from "./generated-types"; 3 | import TodoApp from "./components/TodoApp"; 4 | 5 | export const App: React.FC<{}> = () => { 6 | const { data, error } = useAppQueryQuery({ 7 | variables: { 8 | userId: "me" 9 | } 10 | }); 11 | 12 | if (data && data.user) { 13 | return ; 14 | } else if (error) { 15 | return
{error.message}
; 16 | } 17 | 18 | return
Loading
; 19 | }; 20 | -------------------------------------------------------------------------------- /packages/todo-app-example/src/Option.ts: -------------------------------------------------------------------------------- 1 | export type None = null | undefined; 2 | export type Some = Exclude; 3 | export const isSome = (input: T): input is Some => input != null; 4 | export const isNone = (input: unknown): input is None => input == null; -------------------------------------------------------------------------------- /packages/todo-app-example/src/TodoApp.query.graphql: -------------------------------------------------------------------------------- 1 | query appQuery($userId: String) { 2 | user(id: $userId) { 3 | ...TodoApp_user 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/todo-app-example/src/components/Todo.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { useChangeTodoStatusMutation } from "../mutations/ChangeTodoStatusMutation"; 3 | import { useRemoveTodoMutation } from "../mutations/RemoveTodoMutation"; 4 | import { useRenameTodoMutation } from "../mutations/RenameTodoMutation"; 5 | import TodoTextInput from "./TodoTextInput"; 6 | import classnames from "classnames"; 7 | import type { Todo_TodoFragment, Todo_UserFragment } from "../generated-types"; 8 | 9 | interface Props { 10 | todo: Todo_TodoFragment; 11 | user: Todo_UserFragment; 12 | } 13 | 14 | const Todo: React.FC = ({ todo, user }) => { 15 | const renameTodoMutation = useRenameTodoMutation(); 16 | const changeTodoStatusMutation = useChangeTodoStatusMutation(); 17 | const removeTodoMutation = useRemoveTodoMutation(); 18 | const [isEditing, setIsEditing] = React.useState(false); 19 | 20 | const handleCompleteChange = (e: React.SyntheticEvent) => { 21 | const complete = e.currentTarget.checked; 22 | changeTodoStatusMutation(complete, todo, user); 23 | }; 24 | 25 | const handleDestroyClick = () => removeTodo(); 26 | const handleLabelDoubleClick = () => setIsEditing(true); 27 | const handleTextInputCancel = () => setIsEditing(false); 28 | 29 | const handleTextInputDelete = () => { 30 | setIsEditing(false); 31 | removeTodo(); 32 | }; 33 | 34 | const handleTextInputSave = (text: string) => { 35 | setIsEditing(false); 36 | renameTodoMutation(text, todo); 37 | }; 38 | 39 | const removeTodo = () => removeTodoMutation(todo, user); 40 | 41 | return ( 42 |
  • 48 |
    49 | 55 | 56 | 57 |
    59 | 60 | {isEditing && ( 61 | 69 | )} 70 |
  • 71 | ); 72 | }; 73 | 74 | export default Todo; 75 | -------------------------------------------------------------------------------- /packages/todo-app-example/src/components/TodoApp.tsx: -------------------------------------------------------------------------------- 1 | import { useAddTodoMutation } from "../mutations/AddTodoMutation"; 2 | import TodoList from "./TodoList"; 3 | import TodoListFooter from "./TodoListFooter"; 4 | import TodoTextInput from "./TodoTextInput"; 5 | 6 | import React from "react"; 7 | import { TodoApp_UserFragment } from "../generated-types"; 8 | 9 | interface Props { 10 | user: TodoApp_UserFragment; 11 | } 12 | 13 | const TodoApp: React.FC = ({ user }) => { 14 | const addTodoMutation = useAddTodoMutation(); 15 | const handleTextInputSave = (text: string) => { 16 | addTodoMutation(text, user); 17 | return; 18 | }; 19 | 20 | const hasTodos = user.totalCount > 0; 21 | 22 | return ( 23 |
    24 |
    25 |
    26 |

    todos

    27 | 28 | 33 |
    34 | 35 | 36 | {hasTodos && } 37 |
    38 | 39 |
    40 |

    Double-click to edit a todo

    41 | 42 |

    43 | Frontend created by{" "} 44 | Laurin Quast 45 |

    46 |

    47 | Backend created by the{" "} 48 | Relay team 49 |

    50 | 51 |

    52 | Part of TodoMVC 53 |

    54 |
    55 |
    56 | ); 57 | }; 58 | 59 | export default TodoApp; 60 | -------------------------------------------------------------------------------- /packages/todo-app-example/src/components/TodoApp_user.fragment.graphql: -------------------------------------------------------------------------------- 1 | fragment TodoApp_user on User { 2 | id 3 | userId 4 | totalCount 5 | ...TodoListFooter_user 6 | ...TodoList_user 7 | } 8 | -------------------------------------------------------------------------------- /packages/todo-app-example/src/components/TodoList.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { useMarkAllTodosMutation } from "../mutations/MarkAllTodosMutation"; 3 | import Todo from "./Todo"; 4 | import type { TodoList_UserFragment } from "../generated-types"; 5 | import { isSome, None, Some } from "../Option"; 6 | 7 | type Todos = Exclude["todos"]; 8 | type Edges = Exclude["edges"]; 9 | type Edge = Exclude[number]; 10 | type Node = Exclude["node"], None>; 11 | 12 | interface Props { 13 | user: TodoList_UserFragment; 14 | } 15 | 16 | function isDefinedFilter(value: TValue): value is Some { 17 | return Boolean(value); 18 | } 19 | 20 | const TodoList: React.FC = ({ 21 | user, 22 | user: { todos, totalCount, completedCount }, 23 | }) => { 24 | const markAllTodosMutation = useMarkAllTodosMutation(); 25 | const handleMarkAllChange = (e: React.SyntheticEvent) => { 26 | const complete = e.currentTarget.checked; 27 | 28 | if (todos) { 29 | markAllTodosMutation(complete, todos, user); 30 | } 31 | }; 32 | 33 | const nodes: Readonly = 34 | isSome(todos) && isSome(todos.edges) 35 | ? todos?.edges 36 | .filter(isDefinedFilter) 37 | .map((edge) => edge.node) 38 | .filter(isDefinedFilter) 39 | : []; 40 | 41 | return ( 42 |
    43 | 49 | 50 | 51 | 52 |
      53 | {nodes.map((node) => ( 54 | 55 | ))} 56 |
    57 |
    58 | ); 59 | }; 60 | 61 | export default TodoList; 62 | -------------------------------------------------------------------------------- /packages/todo-app-example/src/components/TodoListFooter.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { TodoListFooter_UserFragment } from "../generated-types"; 3 | import { useRemoveCompletedTodosMutation } from "../mutations/RemoveCompletedTodosMutation"; 4 | import { None } from "../Option"; 5 | 6 | type Todos = Exclude; 7 | type Edges = Exclude; 8 | 9 | interface Props { 10 | user: TodoListFooter_UserFragment; 11 | } 12 | 13 | const TodoListFooter: React.FC = ({ 14 | user, 15 | user: { todos, completedCount, totalCount }, 16 | }) => { 17 | const removeCompletedTodosMutation = useRemoveCompletedTodosMutation(); 18 | const completedEdges: Readonly = 19 | todos && todos.edges 20 | ? todos.edges.filter((edge) => edge && edge.node && edge.node.complete) 21 | : []; 22 | 23 | const handleRemoveCompletedTodosClick = () => { 24 | removeCompletedTodosMutation( 25 | { 26 | edges: completedEdges, 27 | } as Todos, 28 | user 29 | ); 30 | }; 31 | 32 | const numRemainingTodos = totalCount - completedCount; 33 | 34 | return ( 35 |
    36 | 37 | {numRemainingTodos} item 38 | {numRemainingTodos === 1 ? "" : "s"} left 39 | 40 | 41 | {completedCount > 0 && ( 42 | 48 | )} 49 |
    50 | ); 51 | }; 52 | 53 | export default TodoListFooter; 54 | -------------------------------------------------------------------------------- /packages/todo-app-example/src/components/TodoListFooter_user.fragment.graphql: -------------------------------------------------------------------------------- 1 | fragment TodoListFooter_user on User 2 | @argumentDefinitions(first: { type: "Int", defaultValue: 2147483647 }) { 3 | id 4 | userId 5 | completedCount 6 | todos(first: $first) @connection(key: "TodoList_todos") { 7 | edges { 8 | node { 9 | id 10 | complete 11 | } 12 | } 13 | } 14 | totalCount 15 | } 16 | -------------------------------------------------------------------------------- /packages/todo-app-example/src/components/TodoList_user.fragment.graphql: -------------------------------------------------------------------------------- 1 | fragment TodoList_user on User { 2 | todos( 3 | first: 2147483647 # max GraphQLInt 4 | ) @connection(key: "TodoList_todos") { 5 | edges { 6 | node { 7 | id 8 | complete 9 | ...Todo_todo 10 | } 11 | } 12 | } 13 | id 14 | userId 15 | totalCount 16 | completedCount 17 | ...Todo_user 18 | } 19 | -------------------------------------------------------------------------------- /packages/todo-app-example/src/components/TodoTextInput.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | interface Props { 4 | className: string; 5 | commitOnBlur?: boolean; 6 | initialValue?: string; 7 | onCancel?: () => void; 8 | onDelete?: () => void; 9 | onSave: (value: string) => void; 10 | placeholder?: string; 11 | } 12 | 13 | const ENTER_KEY_CODE = 13; 14 | const ESC_KEY_CODE = 27; 15 | 16 | const TodoTextInput: React.FC = ({ 17 | className, 18 | commitOnBlur, 19 | initialValue, 20 | onCancel, 21 | onDelete, 22 | onSave, 23 | placeholder, 24 | }) => { 25 | const [text, setText] = React.useState(initialValue || ""); 26 | const inputRef = React.useRef(null); 27 | 28 | React.useEffect(() => { 29 | if (inputRef.current) { 30 | inputRef.current.focus(); 31 | } 32 | }, [inputRef]); 33 | 34 | const commitChanges = () => { 35 | const newText = text.trim(); 36 | 37 | if (onDelete && newText === "") { 38 | onDelete(); 39 | } else if (onCancel && newText === initialValue) { 40 | onCancel(); 41 | } else if (newText !== "") { 42 | onSave(newText); 43 | setText(""); 44 | } 45 | }; 46 | 47 | const handleBlur = () => { 48 | if (commitOnBlur) { 49 | commitChanges(); 50 | } 51 | }; 52 | 53 | const handleChange = (e: React.SyntheticEvent) => { 54 | setText(e.currentTarget.value); 55 | }; 56 | 57 | const handleKeyDown = (e: React.KeyboardEvent) => { 58 | if (onCancel && e.keyCode === ESC_KEY_CODE) { 59 | onCancel(); 60 | } else if (e.keyCode === ENTER_KEY_CODE) { 61 | commitChanges(); 62 | } 63 | }; 64 | 65 | return ( 66 | 75 | ); 76 | }; 77 | 78 | export default TodoTextInput; 79 | -------------------------------------------------------------------------------- /packages/todo-app-example/src/components/Todo_todo.fragment.graphql: -------------------------------------------------------------------------------- 1 | fragment Todo_todo on Todo { 2 | complete 3 | id 4 | text 5 | } 6 | -------------------------------------------------------------------------------- /packages/todo-app-example/src/components/Todo_user.fragment.graphql: -------------------------------------------------------------------------------- 1 | fragment Todo_user on User { 2 | id 3 | userId 4 | totalCount 5 | completedCount 6 | } 7 | -------------------------------------------------------------------------------- /packages/todo-app-example/src/generated-types.tsx: -------------------------------------------------------------------------------- 1 | import { gql } from 'graphql.macro'; 2 | import * as ApolloReactCommon from '@apollo/react-common'; 3 | import * as ApolloReactHooks from '@apollo/react-hooks'; 4 | export type Maybe = T | null; 5 | export type Exact = { [K in keyof T]: T[K] }; 6 | /** All built-in and custom scalars, mapped to their actual values */ 7 | export type Scalars = { 8 | ID: string; 9 | String: string; 10 | Boolean: boolean; 11 | Int: number; 12 | Float: number; 13 | }; 14 | 15 | export type AddTodoInput = { 16 | text: Scalars['String']; 17 | userId: Scalars['ID']; 18 | clientMutationId?: Maybe; 19 | }; 20 | 21 | export type AddTodoPayload = { 22 | __typename?: 'AddTodoPayload'; 23 | todoEdge: TodoEdge; 24 | user: User; 25 | clientMutationId?: Maybe; 26 | }; 27 | 28 | export type ChangeTodoStatusInput = { 29 | complete: Scalars['Boolean']; 30 | id: Scalars['ID']; 31 | userId: Scalars['ID']; 32 | clientMutationId?: Maybe; 33 | }; 34 | 35 | export type ChangeTodoStatusPayload = { 36 | __typename?: 'ChangeTodoStatusPayload'; 37 | todo: Todo; 38 | user: User; 39 | clientMutationId?: Maybe; 40 | }; 41 | 42 | export type MarkAllTodosInput = { 43 | complete: Scalars['Boolean']; 44 | userId: Scalars['ID']; 45 | clientMutationId?: Maybe; 46 | }; 47 | 48 | export type MarkAllTodosPayload = { 49 | __typename?: 'MarkAllTodosPayload'; 50 | changedTodos?: Maybe>; 51 | user: User; 52 | clientMutationId?: Maybe; 53 | }; 54 | 55 | export type Mutation = { 56 | __typename?: 'Mutation'; 57 | addTodo?: Maybe; 58 | changeTodoStatus?: Maybe; 59 | markAllTodos?: Maybe; 60 | removeCompletedTodos?: Maybe; 61 | removeTodo?: Maybe; 62 | renameTodo?: Maybe; 63 | }; 64 | 65 | 66 | export type MutationAddTodoArgs = { 67 | input: AddTodoInput; 68 | }; 69 | 70 | 71 | export type MutationChangeTodoStatusArgs = { 72 | input: ChangeTodoStatusInput; 73 | }; 74 | 75 | 76 | export type MutationMarkAllTodosArgs = { 77 | input: MarkAllTodosInput; 78 | }; 79 | 80 | 81 | export type MutationRemoveCompletedTodosArgs = { 82 | input: RemoveCompletedTodosInput; 83 | }; 84 | 85 | 86 | export type MutationRemoveTodoArgs = { 87 | input: RemoveTodoInput; 88 | }; 89 | 90 | 91 | export type MutationRenameTodoArgs = { 92 | input: RenameTodoInput; 93 | }; 94 | 95 | /** An object with an ID */ 96 | export type Node = { 97 | /** The id of the object. */ 98 | id: Scalars['ID']; 99 | }; 100 | 101 | /** Information about pagination in a connection. */ 102 | export type PageInfo = { 103 | __typename?: 'PageInfo'; 104 | /** When paginating forwards, are there more items? */ 105 | hasNextPage: Scalars['Boolean']; 106 | /** When paginating backwards, are there more items? */ 107 | hasPreviousPage: Scalars['Boolean']; 108 | /** When paginating backwards, the cursor to continue. */ 109 | startCursor?: Maybe; 110 | /** When paginating forwards, the cursor to continue. */ 111 | endCursor?: Maybe; 112 | }; 113 | 114 | export type Query = { 115 | __typename?: 'Query'; 116 | user?: Maybe; 117 | /** Fetches an object given its ID */ 118 | node?: Maybe; 119 | }; 120 | 121 | 122 | export type QueryUserArgs = { 123 | id?: Maybe; 124 | }; 125 | 126 | 127 | export type QueryNodeArgs = { 128 | id: Scalars['ID']; 129 | }; 130 | 131 | export type RemoveCompletedTodosInput = { 132 | userId: Scalars['ID']; 133 | clientMutationId?: Maybe; 134 | }; 135 | 136 | export type RemoveCompletedTodosPayload = { 137 | __typename?: 'RemoveCompletedTodosPayload'; 138 | deletedTodoIds?: Maybe>; 139 | user: User; 140 | clientMutationId?: Maybe; 141 | }; 142 | 143 | export type RemoveTodoInput = { 144 | id: Scalars['ID']; 145 | userId: Scalars['ID']; 146 | clientMutationId?: Maybe; 147 | }; 148 | 149 | export type RemoveTodoPayload = { 150 | __typename?: 'RemoveTodoPayload'; 151 | deletedTodoId: Scalars['ID']; 152 | user: User; 153 | clientMutationId?: Maybe; 154 | }; 155 | 156 | export type RenameTodoInput = { 157 | id: Scalars['ID']; 158 | text: Scalars['String']; 159 | clientMutationId?: Maybe; 160 | }; 161 | 162 | export type RenameTodoPayload = { 163 | __typename?: 'RenameTodoPayload'; 164 | todo: Todo; 165 | clientMutationId?: Maybe; 166 | }; 167 | 168 | export type Todo = Node & { 169 | __typename?: 'Todo'; 170 | /** The ID of an object */ 171 | id: Scalars['ID']; 172 | text: Scalars['String']; 173 | complete: Scalars['Boolean']; 174 | }; 175 | 176 | /** A connection to a list of items. */ 177 | export type TodoConnection = { 178 | __typename?: 'TodoConnection'; 179 | /** Information to aid in pagination. */ 180 | pageInfo: PageInfo; 181 | /** A list of edges. */ 182 | edges?: Maybe>>; 183 | }; 184 | 185 | /** An edge in a connection. */ 186 | export type TodoEdge = { 187 | __typename?: 'TodoEdge'; 188 | /** The item at the end of the edge */ 189 | node?: Maybe; 190 | /** A cursor for use in pagination */ 191 | cursor: Scalars['String']; 192 | }; 193 | 194 | export type User = Node & { 195 | __typename?: 'User'; 196 | /** The ID of an object */ 197 | id: Scalars['ID']; 198 | userId: Scalars['String']; 199 | todos?: Maybe; 200 | totalCount: Scalars['Int']; 201 | completedCount: Scalars['Int']; 202 | }; 203 | 204 | 205 | export type UserTodosArgs = { 206 | status?: Maybe; 207 | after?: Maybe; 208 | first?: Maybe; 209 | before?: Maybe; 210 | last?: Maybe; 211 | }; 212 | 213 | export type TodoApp_UserFragment = ( 214 | { __typename?: 'User' } 215 | & Pick 216 | & TodoListFooter_UserFragment 217 | & TodoList_UserFragment 218 | ); 219 | 220 | export type TodoListFooter_UserFragment = ( 221 | { __typename?: 'User' } 222 | & Pick 223 | & { todos?: Maybe<( 224 | { __typename?: 'TodoConnection' } 225 | & { edges?: Maybe 230 | )> } 231 | )>>> } 232 | )> } 233 | ); 234 | 235 | export type TodoList_UserFragment = ( 236 | { __typename?: 'User' } 237 | & Pick 238 | & { todos?: Maybe<( 239 | { __typename?: 'TodoConnection' } 240 | & { edges?: Maybe 245 | & Todo_TodoFragment 246 | )> } 247 | )>>> } 248 | )> } 249 | & Todo_UserFragment 250 | ); 251 | 252 | export type Todo_TodoFragment = ( 253 | { __typename?: 'Todo' } 254 | & Pick 255 | ); 256 | 257 | export type Todo_UserFragment = ( 258 | { __typename?: 'User' } 259 | & Pick 260 | ); 261 | 262 | export type AppQueryQueryVariables = Exact<{ 263 | userId?: Maybe; 264 | }>; 265 | 266 | 267 | export type AppQueryQuery = ( 268 | { __typename?: 'Query' } 269 | & { user?: Maybe<( 270 | { __typename?: 'User' } 271 | & Pick 272 | & { todos?: Maybe<( 273 | { __typename?: 'TodoConnection' } 274 | & { edges?: Maybe 279 | )> } 280 | )>>> } 281 | )> } 282 | )> } 283 | ); 284 | 285 | export type AddTodoMutationMutationVariables = Exact<{ 286 | input: AddTodoInput; 287 | }>; 288 | 289 | 290 | export type AddTodoMutationMutation = ( 291 | { __typename?: 'Mutation' } 292 | & { addTodo?: Maybe<( 293 | { __typename?: 'AddTodoPayload' } 294 | & { todoEdge: ( 295 | { __typename: 'TodoEdge' } 296 | & Pick 297 | & { node?: Maybe<( 298 | { __typename?: 'Todo' } 299 | & Pick 300 | )> } 301 | ), user: ( 302 | { __typename?: 'User' } 303 | & Pick 304 | ) } 305 | )> } 306 | ); 307 | 308 | export type ChangeTodoStatusMutationMutationVariables = Exact<{ 309 | input: ChangeTodoStatusInput; 310 | }>; 311 | 312 | 313 | export type ChangeTodoStatusMutationMutation = ( 314 | { __typename?: 'Mutation' } 315 | & { changeTodoStatus?: Maybe<( 316 | { __typename?: 'ChangeTodoStatusPayload' } 317 | & { todo: ( 318 | { __typename?: 'Todo' } 319 | & Pick 320 | ), user: ( 321 | { __typename?: 'User' } 322 | & Pick 323 | ) } 324 | )> } 325 | ); 326 | 327 | export type MarkAllTodosMutationMutationVariables = Exact<{ 328 | input: MarkAllTodosInput; 329 | }>; 330 | 331 | 332 | export type MarkAllTodosMutationMutation = ( 333 | { __typename?: 'Mutation' } 334 | & { markAllTodos?: Maybe<( 335 | { __typename?: 'MarkAllTodosPayload' } 336 | & { changedTodos?: Maybe 339 | )>>, user: ( 340 | { __typename?: 'User' } 341 | & Pick 342 | ) } 343 | )> } 344 | ); 345 | 346 | export type RemoveCompletedTodosMutationMutationVariables = Exact<{ 347 | input: RemoveCompletedTodosInput; 348 | }>; 349 | 350 | 351 | export type RemoveCompletedTodosMutationMutation = ( 352 | { __typename?: 'Mutation' } 353 | & { removeCompletedTodos?: Maybe<( 354 | { __typename?: 'RemoveCompletedTodosPayload' } 355 | & Pick 356 | & { user: ( 357 | { __typename?: 'User' } 358 | & Pick 359 | ) } 360 | )> } 361 | ); 362 | 363 | export type RemoveTodoMutationMutationVariables = Exact<{ 364 | input: RemoveTodoInput; 365 | }>; 366 | 367 | 368 | export type RemoveTodoMutationMutation = ( 369 | { __typename?: 'Mutation' } 370 | & { removeTodo?: Maybe<( 371 | { __typename?: 'RemoveTodoPayload' } 372 | & Pick 373 | & { user: ( 374 | { __typename?: 'User' } 375 | & Pick 376 | ) } 377 | )> } 378 | ); 379 | 380 | export type RenameTodoMutationMutationVariables = Exact<{ 381 | input: RenameTodoInput; 382 | }>; 383 | 384 | 385 | export type RenameTodoMutationMutation = ( 386 | { __typename?: 'Mutation' } 387 | & { renameTodo?: Maybe<( 388 | { __typename?: 'RenameTodoPayload' } 389 | & { todo: ( 390 | { __typename?: 'Todo' } 391 | & Pick 392 | ) } 393 | )> } 394 | ); 395 | 396 | export const TodoListFooter_UserFragmentDoc = gql` 397 | fragment TodoListFooter_user on User { 398 | id 399 | userId 400 | completedCount 401 | todos(first: 2147483647) @connection(key: "TodoList_todos") { 402 | edges { 403 | node { 404 | id 405 | complete 406 | } 407 | } 408 | } 409 | totalCount 410 | } 411 | `; 412 | export const Todo_TodoFragmentDoc = gql` 413 | fragment Todo_todo on Todo { 414 | complete 415 | id 416 | text 417 | } 418 | `; 419 | export const Todo_UserFragmentDoc = gql` 420 | fragment Todo_user on User { 421 | id 422 | userId 423 | totalCount 424 | completedCount 425 | } 426 | `; 427 | export const TodoList_UserFragmentDoc = gql` 428 | fragment TodoList_user on User { 429 | todos(first: 2147483647) @connection(key: "TodoList_todos") { 430 | edges { 431 | node { 432 | id 433 | complete 434 | ...Todo_todo 435 | } 436 | } 437 | } 438 | id 439 | userId 440 | totalCount 441 | completedCount 442 | ...Todo_user 443 | } 444 | ${Todo_TodoFragmentDoc} 445 | ${Todo_UserFragmentDoc}`; 446 | export const TodoApp_UserFragmentDoc = gql` 447 | fragment TodoApp_user on User { 448 | id 449 | userId 450 | totalCount 451 | ...TodoListFooter_user 452 | ...TodoList_user 453 | } 454 | ${TodoListFooter_UserFragmentDoc} 455 | ${TodoList_UserFragmentDoc}`; 456 | export const AppQueryDocument = gql` 457 | query appQuery($userId: String) { 458 | user(id: $userId) { 459 | id 460 | userId 461 | totalCount 462 | completedCount 463 | todos(first: 2147483647) @connection(key: "TodoList_todos") { 464 | edges { 465 | node { 466 | id 467 | complete 468 | text 469 | } 470 | } 471 | } 472 | } 473 | } 474 | `; 475 | 476 | /** 477 | * __useAppQueryQuery__ 478 | * 479 | * To run a query within a React component, call `useAppQueryQuery` and pass it any options that fit your needs. 480 | * When your component renders, `useAppQueryQuery` returns an object from Apollo Client that contains loading, error, and data properties 481 | * you can use to render your UI. 482 | * 483 | * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; 484 | * 485 | * @example 486 | * const { data, loading, error } = useAppQueryQuery({ 487 | * variables: { 488 | * userId: // value for 'userId' 489 | * }, 490 | * }); 491 | */ 492 | export function useAppQueryQuery(baseOptions?: ApolloReactHooks.QueryHookOptions) { 493 | return ApolloReactHooks.useQuery(AppQueryDocument, baseOptions); 494 | } 495 | export function useAppQueryLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions) { 496 | return ApolloReactHooks.useLazyQuery(AppQueryDocument, baseOptions); 497 | } 498 | export type AppQueryQueryHookResult = ReturnType; 499 | export type AppQueryLazyQueryHookResult = ReturnType; 500 | export type AppQueryQueryResult = ApolloReactCommon.QueryResult; 501 | export const AddTodoMutationDocument = gql` 502 | mutation AddTodoMutation($input: AddTodoInput!) { 503 | addTodo(input: $input) { 504 | todoEdge { 505 | __typename 506 | cursor 507 | node { 508 | complete 509 | id 510 | text 511 | } 512 | } 513 | user { 514 | id 515 | totalCount 516 | } 517 | } 518 | } 519 | `; 520 | export type AddTodoMutationMutationFn = ApolloReactCommon.MutationFunction; 521 | 522 | /** 523 | * __useAddTodoMutationMutation__ 524 | * 525 | * To run a mutation, you first call `useAddTodoMutationMutation` within a React component and pass it any options that fit your needs. 526 | * When your component renders, `useAddTodoMutationMutation` returns a tuple that includes: 527 | * - A mutate function that you can call at any time to execute the mutation 528 | * - An object with fields that represent the current status of the mutation's execution 529 | * 530 | * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; 531 | * 532 | * @example 533 | * const [addTodoMutationMutation, { data, loading, error }] = useAddTodoMutationMutation({ 534 | * variables: { 535 | * input: // value for 'input' 536 | * }, 537 | * }); 538 | */ 539 | export function useAddTodoMutationMutation(baseOptions?: ApolloReactHooks.MutationHookOptions) { 540 | return ApolloReactHooks.useMutation(AddTodoMutationDocument, baseOptions); 541 | } 542 | export type AddTodoMutationMutationHookResult = ReturnType; 543 | export type AddTodoMutationMutationResult = ApolloReactCommon.MutationResult; 544 | export type AddTodoMutationMutationOptions = ApolloReactCommon.BaseMutationOptions; 545 | export const ChangeTodoStatusMutationDocument = gql` 546 | mutation ChangeTodoStatusMutation($input: ChangeTodoStatusInput!) { 547 | changeTodoStatus(input: $input) { 548 | todo { 549 | id 550 | complete 551 | } 552 | user { 553 | id 554 | completedCount 555 | } 556 | } 557 | } 558 | `; 559 | export type ChangeTodoStatusMutationMutationFn = ApolloReactCommon.MutationFunction; 560 | 561 | /** 562 | * __useChangeTodoStatusMutationMutation__ 563 | * 564 | * To run a mutation, you first call `useChangeTodoStatusMutationMutation` within a React component and pass it any options that fit your needs. 565 | * When your component renders, `useChangeTodoStatusMutationMutation` returns a tuple that includes: 566 | * - A mutate function that you can call at any time to execute the mutation 567 | * - An object with fields that represent the current status of the mutation's execution 568 | * 569 | * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; 570 | * 571 | * @example 572 | * const [changeTodoStatusMutationMutation, { data, loading, error }] = useChangeTodoStatusMutationMutation({ 573 | * variables: { 574 | * input: // value for 'input' 575 | * }, 576 | * }); 577 | */ 578 | export function useChangeTodoStatusMutationMutation(baseOptions?: ApolloReactHooks.MutationHookOptions) { 579 | return ApolloReactHooks.useMutation(ChangeTodoStatusMutationDocument, baseOptions); 580 | } 581 | export type ChangeTodoStatusMutationMutationHookResult = ReturnType; 582 | export type ChangeTodoStatusMutationMutationResult = ApolloReactCommon.MutationResult; 583 | export type ChangeTodoStatusMutationMutationOptions = ApolloReactCommon.BaseMutationOptions; 584 | export const MarkAllTodosMutationDocument = gql` 585 | mutation MarkAllTodosMutation($input: MarkAllTodosInput!) { 586 | markAllTodos(input: $input) { 587 | changedTodos { 588 | id 589 | complete 590 | } 591 | user { 592 | id 593 | completedCount 594 | } 595 | } 596 | } 597 | `; 598 | export type MarkAllTodosMutationMutationFn = ApolloReactCommon.MutationFunction; 599 | 600 | /** 601 | * __useMarkAllTodosMutationMutation__ 602 | * 603 | * To run a mutation, you first call `useMarkAllTodosMutationMutation` within a React component and pass it any options that fit your needs. 604 | * When your component renders, `useMarkAllTodosMutationMutation` returns a tuple that includes: 605 | * - A mutate function that you can call at any time to execute the mutation 606 | * - An object with fields that represent the current status of the mutation's execution 607 | * 608 | * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; 609 | * 610 | * @example 611 | * const [markAllTodosMutationMutation, { data, loading, error }] = useMarkAllTodosMutationMutation({ 612 | * variables: { 613 | * input: // value for 'input' 614 | * }, 615 | * }); 616 | */ 617 | export function useMarkAllTodosMutationMutation(baseOptions?: ApolloReactHooks.MutationHookOptions) { 618 | return ApolloReactHooks.useMutation(MarkAllTodosMutationDocument, baseOptions); 619 | } 620 | export type MarkAllTodosMutationMutationHookResult = ReturnType; 621 | export type MarkAllTodosMutationMutationResult = ApolloReactCommon.MutationResult; 622 | export type MarkAllTodosMutationMutationOptions = ApolloReactCommon.BaseMutationOptions; 623 | export const RemoveCompletedTodosMutationDocument = gql` 624 | mutation RemoveCompletedTodosMutation($input: RemoveCompletedTodosInput!) { 625 | removeCompletedTodos(input: $input) { 626 | deletedTodoIds 627 | user { 628 | id 629 | completedCount 630 | totalCount 631 | } 632 | } 633 | } 634 | `; 635 | export type RemoveCompletedTodosMutationMutationFn = ApolloReactCommon.MutationFunction; 636 | 637 | /** 638 | * __useRemoveCompletedTodosMutationMutation__ 639 | * 640 | * To run a mutation, you first call `useRemoveCompletedTodosMutationMutation` within a React component and pass it any options that fit your needs. 641 | * When your component renders, `useRemoveCompletedTodosMutationMutation` returns a tuple that includes: 642 | * - A mutate function that you can call at any time to execute the mutation 643 | * - An object with fields that represent the current status of the mutation's execution 644 | * 645 | * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; 646 | * 647 | * @example 648 | * const [removeCompletedTodosMutationMutation, { data, loading, error }] = useRemoveCompletedTodosMutationMutation({ 649 | * variables: { 650 | * input: // value for 'input' 651 | * }, 652 | * }); 653 | */ 654 | export function useRemoveCompletedTodosMutationMutation(baseOptions?: ApolloReactHooks.MutationHookOptions) { 655 | return ApolloReactHooks.useMutation(RemoveCompletedTodosMutationDocument, baseOptions); 656 | } 657 | export type RemoveCompletedTodosMutationMutationHookResult = ReturnType; 658 | export type RemoveCompletedTodosMutationMutationResult = ApolloReactCommon.MutationResult; 659 | export type RemoveCompletedTodosMutationMutationOptions = ApolloReactCommon.BaseMutationOptions; 660 | export const RemoveTodoMutationDocument = gql` 661 | mutation RemoveTodoMutation($input: RemoveTodoInput!) { 662 | removeTodo(input: $input) { 663 | deletedTodoId 664 | user { 665 | id 666 | completedCount 667 | totalCount 668 | } 669 | } 670 | } 671 | `; 672 | export type RemoveTodoMutationMutationFn = ApolloReactCommon.MutationFunction; 673 | 674 | /** 675 | * __useRemoveTodoMutationMutation__ 676 | * 677 | * To run a mutation, you first call `useRemoveTodoMutationMutation` within a React component and pass it any options that fit your needs. 678 | * When your component renders, `useRemoveTodoMutationMutation` returns a tuple that includes: 679 | * - A mutate function that you can call at any time to execute the mutation 680 | * - An object with fields that represent the current status of the mutation's execution 681 | * 682 | * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; 683 | * 684 | * @example 685 | * const [removeTodoMutationMutation, { data, loading, error }] = useRemoveTodoMutationMutation({ 686 | * variables: { 687 | * input: // value for 'input' 688 | * }, 689 | * }); 690 | */ 691 | export function useRemoveTodoMutationMutation(baseOptions?: ApolloReactHooks.MutationHookOptions) { 692 | return ApolloReactHooks.useMutation(RemoveTodoMutationDocument, baseOptions); 693 | } 694 | export type RemoveTodoMutationMutationHookResult = ReturnType; 695 | export type RemoveTodoMutationMutationResult = ApolloReactCommon.MutationResult; 696 | export type RemoveTodoMutationMutationOptions = ApolloReactCommon.BaseMutationOptions; 697 | export const RenameTodoMutationDocument = gql` 698 | mutation RenameTodoMutation($input: RenameTodoInput!) { 699 | renameTodo(input: $input) { 700 | todo { 701 | id 702 | text 703 | } 704 | } 705 | } 706 | `; 707 | export type RenameTodoMutationMutationFn = ApolloReactCommon.MutationFunction; 708 | 709 | /** 710 | * __useRenameTodoMutationMutation__ 711 | * 712 | * To run a mutation, you first call `useRenameTodoMutationMutation` within a React component and pass it any options that fit your needs. 713 | * When your component renders, `useRenameTodoMutationMutation` returns a tuple that includes: 714 | * - A mutate function that you can call at any time to execute the mutation 715 | * - An object with fields that represent the current status of the mutation's execution 716 | * 717 | * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; 718 | * 719 | * @example 720 | * const [renameTodoMutationMutation, { data, loading, error }] = useRenameTodoMutationMutation({ 721 | * variables: { 722 | * input: // value for 'input' 723 | * }, 724 | * }); 725 | */ 726 | export function useRenameTodoMutationMutation(baseOptions?: ApolloReactHooks.MutationHookOptions) { 727 | return ApolloReactHooks.useMutation(RenameTodoMutationDocument, baseOptions); 728 | } 729 | export type RenameTodoMutationMutationHookResult = ReturnType; 730 | export type RenameTodoMutationMutationResult = ApolloReactCommon.MutationResult; 731 | export type RenameTodoMutationMutationOptions = ApolloReactCommon.BaseMutationOptions; -------------------------------------------------------------------------------- /packages/todo-app-example/src/index.tsx: -------------------------------------------------------------------------------- 1 | import "todomvc-common"; 2 | import "todomvc-app-css/index.css"; 3 | 4 | import React from "react"; 5 | import ReactDOM from "react-dom"; 6 | 7 | import { ApolloClient } from "apollo-client"; 8 | import { InMemoryCache } from "apollo-cache-inmemory"; 9 | import { createHttpLink } from "apollo-link-http"; 10 | import { ApolloProvider } from "@apollo/react-hooks"; 11 | import { App } from "./App"; 12 | 13 | const client = new ApolloClient({ 14 | cache: new InMemoryCache(), 15 | link: createHttpLink({ uri: "/graphql" }) 16 | }); 17 | 18 | ReactDOM.render( 19 | 20 | 21 | , 22 | document.getElementById("root") 23 | ); 24 | -------------------------------------------------------------------------------- /packages/todo-app-example/src/mutations/AddTodoMutation.mutation.graphql: -------------------------------------------------------------------------------- 1 | mutation AddTodoMutation($input: AddTodoInput!) { 2 | addTodo(input: $input) { 3 | todoEdge { 4 | __typename 5 | cursor 6 | node { 7 | complete 8 | id 9 | text 10 | } 11 | } 12 | user { 13 | id 14 | totalCount 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/todo-app-example/src/mutations/AddTodoMutation.ts: -------------------------------------------------------------------------------- 1 | import { 2 | useAddTodoMutationMutation, 3 | AddTodoInput, 4 | TodoApp_UserFragment, 5 | AddTodoMutationMutation, 6 | TodoList_UserFragmentDoc, 7 | TodoList_UserFragment, 8 | } from "../generated-types"; 9 | import * as Option from "../Option"; 10 | import { FetchResult } from "apollo-link"; 11 | import { useCallback } from "react"; 12 | import { MutationUpdaterFn } from "apollo-client"; 13 | 14 | let tempID = 0; 15 | 16 | const createOptimisticResponse = ( 17 | text: string, 18 | user: TodoApp_UserFragment 19 | ): AddTodoMutationMutation => { 20 | return { 21 | __typename: "Mutation", 22 | addTodo: { 23 | __typename: "AddTodoPayload", 24 | todoEdge: { 25 | __typename: "TodoEdge", 26 | cursor: "client:newTodoEdge:" + tempID++, 27 | node: { 28 | __typename: "Todo", 29 | id: "client:newTodo:" + tempID++, 30 | text, 31 | complete: false, 32 | }, 33 | }, 34 | user: { 35 | __typename: "User", 36 | id: user.id, 37 | totalCount: user.totalCount, 38 | }, 39 | }, 40 | }; 41 | }; 42 | 43 | const update: MutationUpdaterFn = (proxy, result) => { 44 | const userId = result?.data?.addTodo?.user.id; 45 | if (Option.isNone(userId)) { 46 | return; 47 | } 48 | const data = proxy.readFragment({ 49 | fragment: TodoList_UserFragmentDoc, 50 | fragmentName: "TodoList_user", 51 | id: `User:${userId}`, 52 | }); 53 | if (Option.isNone(data)) { 54 | return; 55 | } 56 | const edges = data.todos?.edges; 57 | const addTodo = result?.data?.addTodo; 58 | if (Option.isNone(edges) || Option.isNone(addTodo)) { 59 | return; 60 | } 61 | edges.push(addTodo.todoEdge); 62 | proxy.writeFragment({ 63 | fragment: TodoList_UserFragmentDoc, 64 | fragmentName: "TodoList_user", 65 | id: `User:${userId}`, 66 | data, 67 | }); 68 | }; 69 | 70 | type AddTodoMutationFetchResult = FetchResult< 71 | AddTodoMutationMutation, 72 | Record, 73 | Record 74 | >; 75 | 76 | export const useAddTodoMutation = () => { 77 | const [mutate] = useAddTodoMutationMutation(); 78 | 79 | return useCallback( 80 | (text: string, user: TodoApp_UserFragment) => { 81 | const input: AddTodoInput = { 82 | text, 83 | userId: user.userId, 84 | clientMutationId: `${tempID++}`, 85 | }; 86 | 87 | return mutate({ 88 | variables: { input }, 89 | optimisticResponse: createOptimisticResponse(text, user), 90 | update, 91 | }); 92 | }, 93 | [mutate] 94 | ); 95 | }; 96 | -------------------------------------------------------------------------------- /packages/todo-app-example/src/mutations/ChangeTodoStatusMutation.mutation.graphql: -------------------------------------------------------------------------------- 1 | mutation ChangeTodoStatusMutation($input: ChangeTodoStatusInput!) { 2 | changeTodoStatus(input: $input) { 3 | todo { 4 | id 5 | complete 6 | } 7 | user { 8 | id 9 | completedCount 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/todo-app-example/src/mutations/ChangeTodoStatusMutation.ts: -------------------------------------------------------------------------------- 1 | import { 2 | useChangeTodoStatusMutationMutation, 3 | ChangeTodoStatusInput, 4 | Todo_TodoFragment, 5 | Todo_UserFragment, 6 | ChangeTodoStatusMutationMutation 7 | } from "../generated-types"; 8 | import { useCallback } from "react"; 9 | 10 | const createOptimisticResponse = ( 11 | complete: boolean, 12 | todo: Todo_TodoFragment, 13 | user: Todo_UserFragment 14 | ): ChangeTodoStatusMutationMutation => ({ 15 | __typename: "Mutation", 16 | changeTodoStatus: { 17 | __typename: "ChangeTodoStatusPayload", 18 | todo: { 19 | __typename: "Todo", 20 | complete: complete, 21 | id: todo.id 22 | }, 23 | user: { 24 | __typename: "User", 25 | id: user.id, 26 | completedCount: complete 27 | ? user.completedCount + 1 28 | : user.completedCount - 1 29 | } 30 | } 31 | }); 32 | 33 | export const useChangeTodoStatusMutation = () => { 34 | const [mutate] = useChangeTodoStatusMutationMutation(); 35 | 36 | return useCallback( 37 | (complete: boolean, todo: Todo_TodoFragment, user: Todo_UserFragment) => { 38 | const input: ChangeTodoStatusInput = { 39 | complete, 40 | userId: user.userId, 41 | id: todo.id 42 | }; 43 | 44 | return mutate({ 45 | variables: { input }, 46 | optimisticResponse: createOptimisticResponse(complete, todo, user) 47 | }); 48 | }, 49 | [mutate] 50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /packages/todo-app-example/src/mutations/MarkAllTodosMutation.mutation.graphql: -------------------------------------------------------------------------------- 1 | mutation MarkAllTodosMutation($input: MarkAllTodosInput!) { 2 | markAllTodos(input: $input) { 3 | changedTodos { 4 | id 5 | complete 6 | } 7 | user { 8 | id 9 | completedCount 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/todo-app-example/src/mutations/MarkAllTodosMutation.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { 3 | useMarkAllTodosMutationMutation, 4 | TodoList_UserFragment, 5 | MarkAllTodosInput, 6 | MarkAllTodosMutationMutation, 7 | } from "../generated-types"; 8 | import { None, Some } from "../Option"; 9 | 10 | type Todos = Exclude; 11 | type ChangedTodos = Exclude< 12 | Exclude["changedTodos"], 13 | None 14 | >; 15 | type ChangedTodo = ChangedTodos[number]; 16 | 17 | function emptyEdgeFilter( 18 | value: TValue 19 | ): value is Some { 20 | return Boolean(value); 21 | } 22 | 23 | const createOptimisticResponse = ( 24 | complete: boolean, 25 | todos: Todos, 26 | user: TodoList_UserFragment 27 | ): MarkAllTodosMutationMutation => { 28 | const changedTodos: ChangedTodos = todos.edges 29 | ? todos.edges 30 | .filter(emptyEdgeFilter) 31 | .map((edge) => edge.node) 32 | .filter(emptyEdgeFilter) 33 | .filter((node) => node.complete !== complete) 34 | .map( 35 | (node): ChangedTodo => ({ 36 | complete, 37 | id: node.id, 38 | }) 39 | ) 40 | : []; 41 | 42 | return { 43 | __typename: "Mutation", 44 | markAllTodos: { 45 | __typename: "MarkAllTodosPayload", 46 | changedTodos, 47 | user: { 48 | __typename: "User", 49 | id: user.id, 50 | completedCount: complete ? user.totalCount : 0, 51 | }, 52 | }, 53 | }; 54 | }; 55 | 56 | export const useMarkAllTodosMutation = () => { 57 | const [mutate] = useMarkAllTodosMutationMutation(); 58 | return React.useCallback( 59 | (complete: boolean, todos: Todos, user: TodoList_UserFragment) => { 60 | const input: MarkAllTodosInput = { 61 | complete, 62 | userId: user.userId, 63 | }; 64 | 65 | return mutate({ 66 | variables: { 67 | input, 68 | }, 69 | optimisticResponse: createOptimisticResponse(complete, todos, user), 70 | }); 71 | }, 72 | [mutate] 73 | ); 74 | }; 75 | -------------------------------------------------------------------------------- /packages/todo-app-example/src/mutations/RemoveCompletedTodosMutation.mutation.graphql: -------------------------------------------------------------------------------- 1 | mutation RemoveCompletedTodosMutation($input: RemoveCompletedTodosInput!) { 2 | removeCompletedTodos(input: $input) { 3 | deletedTodoIds 4 | user { 5 | id 6 | completedCount 7 | totalCount 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/todo-app-example/src/mutations/RemoveCompletedTodosMutation.ts: -------------------------------------------------------------------------------- 1 | import { 2 | RemoveCompletedTodosInput, 3 | useRemoveCompletedTodosMutationMutation, 4 | TodoListFooter_UserFragment, 5 | RemoveCompletedTodosMutationMutation, 6 | TodoListFooter_UserFragmentDoc, 7 | } from "../generated-types"; 8 | import { useCallback } from "react"; 9 | import * as Option from "../Option"; 10 | import { MutationUpdaterFn } from "apollo-client"; 11 | 12 | type Todos = Exclude["todos"]; 13 | 14 | const createOptimisticResponse = ( 15 | todos: Todos, 16 | user: TodoListFooter_UserFragment 17 | ): RemoveCompletedTodosMutationMutation => { 18 | const deletedTodoIds = (todos?.edges ?? []) 19 | .map((edge) => edge?.node) 20 | .filter(Option.isSome) 21 | .filter((node) => node.complete === true) 22 | .map((node) => node.id); 23 | 24 | return { 25 | __typename: "Mutation", 26 | removeCompletedTodos: { 27 | __typename: "RemoveCompletedTodosPayload", 28 | deletedTodoIds, 29 | user: { 30 | __typename: "User", 31 | id: user.id, 32 | completedCount: user.completedCount - deletedTodoIds.length, 33 | totalCount: user.totalCount - deletedTodoIds.length, 34 | }, 35 | }, 36 | }; 37 | }; 38 | 39 | const update: MutationUpdaterFn = ( 40 | dataProxy, 41 | result 42 | ) => { 43 | const deletedTodoIds = result?.data?.removeCompletedTodos?.deletedTodoIds; 44 | 45 | const userId = result?.data?.removeCompletedTodos?.user.id; 46 | if (Option.isNone(deletedTodoIds) || Option.isNone(userId)) { 47 | return; 48 | } 49 | 50 | const data = dataProxy.readFragment({ 51 | id: `User:${userId}`, 52 | fragment: TodoListFooter_UserFragmentDoc, 53 | fragmentName: "TodoListFooter_user", 54 | }); 55 | 56 | if ( 57 | Option.isNone(data) || 58 | Option.isNone(data.todos) || 59 | Option.isNone(data.todos.edges) 60 | ) { 61 | return; 62 | } 63 | 64 | const newData = { 65 | ...data, 66 | todos: { 67 | ...data.todos, 68 | edges: data.todos.edges.filter((edge) => { 69 | if (!edge || !edge.node) { 70 | return true; 71 | } 72 | return !deletedTodoIds.includes(edge.node.id); 73 | }), 74 | }, 75 | }; 76 | 77 | dataProxy.writeFragment({ 78 | id: `User:${userId}`, 79 | fragment: TodoListFooter_UserFragmentDoc, 80 | fragmentName: "TodoListFooter_user", 81 | data: newData, 82 | }); 83 | }; 84 | 85 | export const useRemoveCompletedTodosMutation = () => { 86 | const [mutate] = useRemoveCompletedTodosMutationMutation(); 87 | 88 | return useCallback( 89 | (todos: Todos, user: TodoListFooter_UserFragment) => { 90 | const input: RemoveCompletedTodosInput = { 91 | userId: user.userId, 92 | }; 93 | 94 | return mutate({ 95 | variables: { input }, 96 | optimisticResponse: createOptimisticResponse(todos, user), 97 | update, 98 | }); 99 | }, 100 | [mutate] 101 | ); 102 | }; 103 | -------------------------------------------------------------------------------- /packages/todo-app-example/src/mutations/RemoveTodoMutation.mutation.graphql: -------------------------------------------------------------------------------- 1 | mutation RemoveTodoMutation($input: RemoveTodoInput!) { 2 | removeTodo(input: $input) { 3 | deletedTodoId 4 | user { 5 | id 6 | completedCount 7 | totalCount 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/todo-app-example/src/mutations/RemoveTodoMutation.ts: -------------------------------------------------------------------------------- 1 | import { 2 | useRemoveTodoMutationMutation, 3 | RemoveTodoMutationMutation, 4 | Todo_UserFragment, 5 | Todo_TodoFragment, 6 | RemoveTodoInput, 7 | TodoList_UserFragmentDoc, 8 | TodoList_UserFragment, 9 | } from "../generated-types"; 10 | import * as Option from "../Option"; 11 | import { useCallback } from "react"; 12 | import { MutationUpdaterFn } from "apollo-client"; 13 | 14 | const createOptimisticResponse = ( 15 | todo: Todo_TodoFragment, 16 | user: Todo_UserFragment 17 | ): RemoveTodoMutationMutation => ({ 18 | __typename: "Mutation", 19 | removeTodo: { 20 | __typename: "RemoveTodoPayload", 21 | deletedTodoId: todo.id, 22 | user: { 23 | __typename: "User", 24 | id: user.id, 25 | completedCount: user.completedCount - (todo.complete ? 1 : 0), 26 | totalCount: user.totalCount - 1, 27 | }, 28 | }, 29 | }); 30 | 31 | const update: MutationUpdaterFn = ( 32 | dataProxy, 33 | result 34 | ) => { 35 | const removedItemId = result?.data?.removeTodo?.deletedTodoId; 36 | const userId = result?.data?.removeTodo?.user.id; 37 | if (Option.isNone(removedItemId) || Option.isNone(userId)) { 38 | return; 39 | } 40 | const data = dataProxy.readFragment({ 41 | fragment: TodoList_UserFragmentDoc, 42 | fragmentName: "TodoList_user", 43 | id: `User:${userId}`, 44 | }); 45 | if ( 46 | Option.isNone(data) || 47 | Option.isNone(data.todos) || 48 | Option.isNone(data.todos.edges) 49 | ) { 50 | return; 51 | } 52 | 53 | const newData = { 54 | ...data, 55 | todos: { 56 | ...data.todos, 57 | edges: data.todos.edges 58 | .filter(Option.isSome) 59 | .filter((edge) => (edge.node ? edge.node.id !== removedItemId : true)), 60 | }, 61 | }; 62 | 63 | dataProxy.writeFragment({ 64 | fragment: TodoList_UserFragmentDoc, 65 | fragmentName: "TodoList_user", 66 | id: `User:${userId}`, 67 | data: newData, 68 | }); 69 | }; 70 | 71 | export const useRemoveTodoMutation = () => { 72 | const [mutate] = useRemoveTodoMutationMutation(); 73 | 74 | return useCallback( 75 | (todo: Todo_TodoFragment, user: Todo_UserFragment) => { 76 | const input: RemoveTodoInput = { 77 | id: todo.id, 78 | userId: user.userId, 79 | }; 80 | 81 | return mutate({ 82 | variables: { input }, 83 | optimisticResponse: createOptimisticResponse(todo, user), 84 | update, 85 | }); 86 | }, 87 | [mutate] 88 | ); 89 | }; 90 | -------------------------------------------------------------------------------- /packages/todo-app-example/src/mutations/RenameTodoMutation.mutation.graphql: -------------------------------------------------------------------------------- 1 | mutation RenameTodoMutation($input: RenameTodoInput!) { 2 | renameTodo(input: $input) { 3 | todo { 4 | id 5 | text 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/todo-app-example/src/mutations/RenameTodoMutation.ts: -------------------------------------------------------------------------------- 1 | import { 2 | useRenameTodoMutationMutation, 3 | Todo_TodoFragment, 4 | RenameTodoInput, 5 | RenameTodoMutationMutation 6 | } from "../generated-types"; 7 | import { useCallback } from "react"; 8 | 9 | const createOptimisticResponse = ( 10 | text: string, 11 | todo: Todo_TodoFragment 12 | ): RenameTodoMutationMutation => ({ 13 | __typename: "Mutation", 14 | renameTodo: { 15 | __typename: "RenameTodoPayload", 16 | todo: { 17 | __typename: "Todo", 18 | id: todo.id, 19 | text 20 | } 21 | } 22 | }); 23 | 24 | export const useRenameTodoMutation = () => { 25 | const [mutate] = useRenameTodoMutationMutation(); 26 | 27 | return useCallback( 28 | (text: string, todo: Todo_TodoFragment) => { 29 | const input: RenameTodoInput = { 30 | text: text, 31 | id: todo.id 32 | }; 33 | return mutate({ 34 | variables: { input }, 35 | optimisticResponse: createOptimisticResponse(text, todo) 36 | }); 37 | }, 38 | [mutate] 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /packages/todo-app-example/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/todo-app-example/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 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "preserve" 17 | }, 18 | "include": ["src"] 19 | } 20 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "circleci": true 4 | } 5 | --------------------------------------------------------------------------------