├── .gitignore ├── README.md ├── app ├── .eslintrc ├── .gitignore ├── README.md ├── build │ └── babelRelayPlugin.js ├── data │ └── schema.json ├── js │ ├── app.js │ ├── components │ │ ├── Todo.js │ │ ├── TodoApp.js │ │ ├── TodoList.js │ │ ├── TodoListFooter.js │ │ └── TodoTextInput.js │ ├── mutations │ │ ├── AddTodoMutation.js │ │ ├── ChangeTodoStatusMutation.js │ │ ├── MarkAllTodosMutation.js │ │ ├── RemoveCompletedTodosMutation.js │ │ ├── RemoveTodoMutation.js │ │ └── RenameTodoMutation.js │ └── routes │ │ └── TodoAppHomeRoute.js ├── package.json ├── public │ ├── index.html │ └── learn.json └── server.js ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src └── main └── java └── todomvc ├── GraphQLController.java ├── Main.java ├── Todo.java ├── TodoSchema.java ├── TodoSchemaMutations.java └── User.java /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | .idea 3 | *.iml 4 | node_modules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Relay TodoMVC with a Java Server 2 | 3 | This is a port of the [Relay TodoMVC example](https://github.com/facebook/relay/tree/master/examples/todo) 4 | where the GraphQL JavaScript server is replaced with a Java server based on [graphql-java](https://github.com/andimarek/graphql-java). 5 | 6 | 7 | ### Running 8 | 9 | Starting the frontend: 10 | 11 | ```bash 12 | cd app 13 | npm install 14 | npm start 15 | ``` 16 | 17 | Starting the backend: (running on port 8080) 18 | 19 | ```bash 20 | ./gradlew start 21 | ``` 22 | 23 | The app is now available at http://localhost:3000 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/.eslintrc: -------------------------------------------------------------------------------- 1 | --- 2 | parser: babel-eslint 3 | 4 | plugins: 5 | - react 6 | 7 | env: 8 | node: true 9 | es6: true 10 | 11 | globals: 12 | document: false 13 | React: false 14 | Relay: false 15 | 16 | arrowFunctions: true 17 | blockBindings: true 18 | classes: true 19 | defaultParams: true 20 | destructuring: true 21 | forOf: true 22 | generators: true 23 | modules: true 24 | objectLiteralComputedProperties: true 25 | objectLiteralShorthandMethods: true 26 | objectLiteralShorthandProperties: true 27 | spread: true 28 | templateStrings: true 29 | 30 | rules: 31 | # ERRORS 32 | brace-style: [2, 1tbs, allowSingleLine: true] 33 | camelcase: [2, properties: always] 34 | comma-style: [2, last] 35 | curly: [2, all] 36 | eol-last: 2 37 | eqeqeq: 2 38 | guard-for-in: 2 39 | handle-callback-err: [2, error] 40 | indent: [2, 2, SwitchCase: 1] 41 | key-spacing: [2, {beforeColon: false, afterColon: true}] 42 | max-len: [2, 80, 4, ignorePattern: "^(\\s*var\\s.+=\\s*require\\s*\\(|import )"] 43 | new-parens: 2 44 | no-alert: 2 45 | no-array-constructor: 2 46 | no-caller: 2 47 | no-catch-shadow: 2 48 | no-cond-assign: 2 49 | no-constant-condition: 2 50 | no-delete-var: 2 51 | no-div-regex: 2 52 | no-dupe-args: 2 53 | no-dupe-keys: 2 54 | no-duplicate-case: 2 55 | no-empty-character-class: 2 56 | no-empty-label: 2 57 | no-empty: 2 58 | no-eval: 2 59 | no-ex-assign: 2 60 | no-extend-native: 2 61 | no-extra-bind: 2 62 | no-extra-boolean-cast: 2 63 | no-extra-semi: 2 64 | no-fallthrough: 2 65 | no-floating-decimal: 2 66 | no-func-assign: 2 67 | no-implied-eval: 2 68 | no-inner-declarations: [2, functions] 69 | no-invalid-regexp: 2 70 | no-irregular-whitespace: 2 71 | no-iterator: 2 72 | no-label-var: 2 73 | no-lonely-if: 2 74 | no-mixed-requires: [2, true] 75 | no-mixed-spaces-and-tabs: 2 76 | no-multi-spaces: 2 77 | no-multi-str: 2 78 | no-negated-in-lhs: 2 79 | no-new-object: 2 80 | no-new-require: 2 81 | no-new-wrappers: 2 82 | no-new: 2 83 | no-obj-calls: 2 84 | no-octal-escape: 2 85 | no-octal: 2 86 | no-param-reassign: 2 87 | no-path-concat: 2 88 | no-proto: 2 89 | no-redeclare: 2 90 | no-regex-spaces: 2 91 | no-return-assign: 2 92 | no-script-url: 2 93 | no-sequences: 2 94 | no-shadow-restricted-names: 2 95 | no-shadow: 2 96 | no-spaced-func: 2 97 | no-sparse-arrays: 2 98 | no-sync: 2 99 | no-throw-literal: 2 100 | no-trailing-spaces: 2 101 | no-undef-init: 2 102 | no-undef: 2 103 | no-unreachable: 2 104 | no-unused-expressions: 2 105 | no-unused-vars: [2, {vars: all, args: after-used}] 106 | no-void: 2 107 | no-with: 2 108 | one-var: [2, never] 109 | operator-assignment: [2, always] 110 | quote-props: [2, as-needed] 111 | quotes: [2, single] 112 | radix: 2 113 | semi-spacing: [2, {before: false, after: true}] 114 | semi: [2, always] 115 | space-after-keywords: [2, always] 116 | space-before-blocks: [2, always] 117 | space-before-function-paren: [2, {anonymous: always, named: never}] 118 | space-infix-ops: [2, int32Hint: false] 119 | space-return-throw-case: 2 120 | space-unary-ops: [2, {words: true, nonwords: false}] 121 | spaced-comment: [2, always] 122 | use-isnan: 2 123 | valid-typeof: 2 124 | wrap-iife: 2 125 | yoda: [2, never, exceptRange: true] 126 | 127 | # WARNINGS 128 | 129 | # DISABLED 130 | block-scoped-var: 0 131 | comma-dangle: 0 132 | complexity: 0 133 | consistent-return: 0 134 | consistent-this: 0 135 | default-case: 0 136 | dot-notation: 0 137 | func-names: 0 138 | func-style: 0 139 | max-nested-callbacks: 0 140 | new-cap: 0 141 | newline-after-var: 0 142 | no-console: 0 143 | no-control-regex: 0 144 | no-debugger: 0 145 | no-eq-null: 0 146 | no-inline-comments: 0 147 | no-labels: 0 148 | no-lone-blocks: 0 149 | no-loop-func: 0 150 | no-multiple-empty-lines: 0 151 | no-native-reassign: 0 152 | no-nested-ternary: 0 153 | no-new-func: 0 154 | no-process-env: 0 155 | no-process-exit: 0 156 | no-reserved-keys: 0 157 | no-restricted-modules: 0 158 | no-self-compare: 0 159 | no-ternary: 0 160 | no-undefined: 0 161 | no-underscore-dangle: 0 162 | no-use-before-define: 0 163 | no-var: 0 164 | no-warning-comments: 0 165 | padded-blocks: 0 166 | sort-vars: 0 167 | space-in-brackets: 0 168 | space-in-parens: 0 169 | strict: 0 170 | valid-jsdoc: 0 171 | vars-on-top: 0 172 | wrap-regex: 0 173 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *log 3 | -------------------------------------------------------------------------------- /app/README.md: -------------------------------------------------------------------------------- 1 | # This is a modified copy of the Relay TodoMVC Example from https://github.com/facebook/relay 2 | 3 | ## Installation 4 | 5 | ``` 6 | npm install 7 | ``` 8 | 9 | ## Running 10 | 11 | Start a local client server on port 3000: 12 | 13 | ``` 14 | npm start 15 | ``` 16 | 17 | ## License 18 | 19 | This file provided by Facebook is for non-commercial testing and evaluation 20 | purposes only. Facebook reserves all rights not expressly granted. 21 | 22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 25 | FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 26 | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 27 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 28 | -------------------------------------------------------------------------------- /app/build/babelRelayPlugin.js: -------------------------------------------------------------------------------- 1 | var getBabelRelayPlugin = require('babel-relay-plugin'); 2 | var schema = require('../data/schema.json'); 3 | 4 | module.exports = getBabelRelayPlugin(schema.data); 5 | -------------------------------------------------------------------------------- /app/data/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "__schema": { 4 | "queryType": { 5 | "name": "Root" 6 | }, 7 | "mutationType": { 8 | "name": "Mutation" 9 | }, 10 | "types": [ 11 | { 12 | "kind": "OBJECT", 13 | "name": "Root", 14 | "description": null, 15 | "fields": [ 16 | { 17 | "name": "viewer", 18 | "description": null, 19 | "args": [], 20 | "type": { 21 | "kind": "OBJECT", 22 | "name": "User", 23 | "ofType": null 24 | }, 25 | "isDeprecated": false, 26 | "deprecationReason": null 27 | }, 28 | { 29 | "name": "node", 30 | "description": "Fetches an object given its ID", 31 | "args": [ 32 | { 33 | "name": "id", 34 | "description": "The ID of an object", 35 | "type": { 36 | "kind": "NON_NULL", 37 | "name": null, 38 | "ofType": { 39 | "kind": "SCALAR", 40 | "name": "ID", 41 | "ofType": null 42 | } 43 | }, 44 | "defaultValue": null 45 | } 46 | ], 47 | "type": { 48 | "kind": "INTERFACE", 49 | "name": "Node", 50 | "ofType": null 51 | }, 52 | "isDeprecated": false, 53 | "deprecationReason": null 54 | } 55 | ], 56 | "inputFields": null, 57 | "interfaces": [], 58 | "enumValues": null, 59 | "possibleTypes": null 60 | }, 61 | { 62 | "kind": "OBJECT", 63 | "name": "User", 64 | "description": null, 65 | "fields": [ 66 | { 67 | "name": "id", 68 | "description": "The ID of an object", 69 | "args": [], 70 | "type": { 71 | "kind": "NON_NULL", 72 | "name": null, 73 | "ofType": { 74 | "kind": "SCALAR", 75 | "name": "ID", 76 | "ofType": null 77 | } 78 | }, 79 | "isDeprecated": false, 80 | "deprecationReason": null 81 | }, 82 | { 83 | "name": "todos", 84 | "description": null, 85 | "args": [ 86 | { 87 | "name": "before", 88 | "description": null, 89 | "type": { 90 | "kind": "SCALAR", 91 | "name": "String", 92 | "ofType": null 93 | }, 94 | "defaultValue": null 95 | }, 96 | { 97 | "name": "after", 98 | "description": null, 99 | "type": { 100 | "kind": "SCALAR", 101 | "name": "String", 102 | "ofType": null 103 | }, 104 | "defaultValue": null 105 | }, 106 | { 107 | "name": "first", 108 | "description": null, 109 | "type": { 110 | "kind": "SCALAR", 111 | "name": "Int", 112 | "ofType": null 113 | }, 114 | "defaultValue": null 115 | }, 116 | { 117 | "name": "last", 118 | "description": null, 119 | "type": { 120 | "kind": "SCALAR", 121 | "name": "Int", 122 | "ofType": null 123 | }, 124 | "defaultValue": null 125 | } 126 | ], 127 | "type": { 128 | "kind": "OBJECT", 129 | "name": "TodoConnection", 130 | "ofType": null 131 | }, 132 | "isDeprecated": false, 133 | "deprecationReason": null 134 | } 135 | ], 136 | "inputFields": null, 137 | "interfaces": [ 138 | { 139 | "kind": "INTERFACE", 140 | "name": "Node", 141 | "ofType": null 142 | } 143 | ], 144 | "enumValues": null, 145 | "possibleTypes": null 146 | }, 147 | { 148 | "kind": "INTERFACE", 149 | "name": "Node", 150 | "description": "An object with an ID", 151 | "fields": [ 152 | { 153 | "name": "id", 154 | "description": "The id of the object.", 155 | "args": [], 156 | "type": { 157 | "kind": "NON_NULL", 158 | "name": null, 159 | "ofType": { 160 | "kind": "SCALAR", 161 | "name": "ID", 162 | "ofType": null 163 | } 164 | }, 165 | "isDeprecated": false, 166 | "deprecationReason": null 167 | } 168 | ], 169 | "inputFields": null, 170 | "interfaces": null, 171 | "enumValues": null, 172 | "possibleTypes": [ 173 | { 174 | "kind": "OBJECT", 175 | "name": "Todo", 176 | "ofType": null 177 | }, 178 | { 179 | "kind": "OBJECT", 180 | "name": "User", 181 | "ofType": null 182 | } 183 | ] 184 | }, 185 | { 186 | "kind": "OBJECT", 187 | "name": "Todo", 188 | "description": null, 189 | "fields": [ 190 | { 191 | "name": "id", 192 | "description": "The ID of an object", 193 | "args": [], 194 | "type": { 195 | "kind": "NON_NULL", 196 | "name": null, 197 | "ofType": { 198 | "kind": "SCALAR", 199 | "name": "ID", 200 | "ofType": null 201 | } 202 | }, 203 | "isDeprecated": false, 204 | "deprecationReason": null 205 | }, 206 | { 207 | "name": "text", 208 | "description": null, 209 | "args": [], 210 | "type": { 211 | "kind": "SCALAR", 212 | "name": "String", 213 | "ofType": null 214 | }, 215 | "isDeprecated": false, 216 | "deprecationReason": null 217 | }, 218 | { 219 | "name": "complete", 220 | "description": null, 221 | "args": [], 222 | "type": { 223 | "kind": "SCALAR", 224 | "name": "Boolean", 225 | "ofType": null 226 | }, 227 | "isDeprecated": false, 228 | "deprecationReason": null 229 | } 230 | ], 231 | "inputFields": null, 232 | "interfaces": [ 233 | { 234 | "kind": "INTERFACE", 235 | "name": "Node", 236 | "ofType": null 237 | } 238 | ], 239 | "enumValues": null, 240 | "possibleTypes": null 241 | }, 242 | { 243 | "kind": "SCALAR", 244 | "name": "ID", 245 | "description": null, 246 | "fields": null, 247 | "inputFields": null, 248 | "interfaces": null, 249 | "enumValues": null, 250 | "possibleTypes": null 251 | }, 252 | { 253 | "kind": "SCALAR", 254 | "name": "String", 255 | "description": null, 256 | "fields": null, 257 | "inputFields": null, 258 | "interfaces": null, 259 | "enumValues": null, 260 | "possibleTypes": null 261 | }, 262 | { 263 | "kind": "SCALAR", 264 | "name": "Boolean", 265 | "description": null, 266 | "fields": null, 267 | "inputFields": null, 268 | "interfaces": null, 269 | "enumValues": null, 270 | "possibleTypes": null 271 | }, 272 | { 273 | "kind": "SCALAR", 274 | "name": "Int", 275 | "description": null, 276 | "fields": null, 277 | "inputFields": null, 278 | "interfaces": null, 279 | "enumValues": null, 280 | "possibleTypes": null 281 | }, 282 | { 283 | "kind": "OBJECT", 284 | "name": "TodoConnection", 285 | "description": "A connection to a list of items.", 286 | "fields": [ 287 | { 288 | "name": "pageInfo", 289 | "description": "Information to aid in pagination.", 290 | "args": [], 291 | "type": { 292 | "kind": "NON_NULL", 293 | "name": null, 294 | "ofType": { 295 | "kind": "OBJECT", 296 | "name": "PageInfo", 297 | "ofType": null 298 | } 299 | }, 300 | "isDeprecated": false, 301 | "deprecationReason": null 302 | }, 303 | { 304 | "name": "edges", 305 | "description": "Information to aid in pagination.", 306 | "args": [], 307 | "type": { 308 | "kind": "LIST", 309 | "name": null, 310 | "ofType": { 311 | "kind": "OBJECT", 312 | "name": "TodoEdge", 313 | "ofType": null 314 | } 315 | }, 316 | "isDeprecated": false, 317 | "deprecationReason": null 318 | }, 319 | { 320 | "name": "totalCount", 321 | "description": null, 322 | "args": [], 323 | "type": { 324 | "kind": "SCALAR", 325 | "name": "Int", 326 | "ofType": null 327 | }, 328 | "isDeprecated": false, 329 | "deprecationReason": null 330 | }, 331 | { 332 | "name": "completedCount", 333 | "description": null, 334 | "args": [], 335 | "type": { 336 | "kind": "SCALAR", 337 | "name": "Int", 338 | "ofType": null 339 | }, 340 | "isDeprecated": false, 341 | "deprecationReason": null 342 | } 343 | ], 344 | "inputFields": null, 345 | "interfaces": [], 346 | "enumValues": null, 347 | "possibleTypes": null 348 | }, 349 | { 350 | "kind": "OBJECT", 351 | "name": "PageInfo", 352 | "description": "Information about pagination in a connection.", 353 | "fields": [ 354 | { 355 | "name": "hasNextPage", 356 | "description": "When paginating forwards, are there more items?", 357 | "args": [], 358 | "type": { 359 | "kind": "NON_NULL", 360 | "name": null, 361 | "ofType": { 362 | "kind": "SCALAR", 363 | "name": "Boolean", 364 | "ofType": null 365 | } 366 | }, 367 | "isDeprecated": false, 368 | "deprecationReason": null 369 | }, 370 | { 371 | "name": "hasPreviousPage", 372 | "description": "When paginating backwards, are there more items?", 373 | "args": [], 374 | "type": { 375 | "kind": "NON_NULL", 376 | "name": null, 377 | "ofType": { 378 | "kind": "SCALAR", 379 | "name": "Boolean", 380 | "ofType": null 381 | } 382 | }, 383 | "isDeprecated": false, 384 | "deprecationReason": null 385 | }, 386 | { 387 | "name": "startCursor", 388 | "description": "When paginating backwards, the cursor to continue.", 389 | "args": [], 390 | "type": { 391 | "kind": "SCALAR", 392 | "name": "String", 393 | "ofType": null 394 | }, 395 | "isDeprecated": false, 396 | "deprecationReason": null 397 | }, 398 | { 399 | "name": "endCursor", 400 | "description": "When paginating forwards, the cursor to continue.", 401 | "args": [], 402 | "type": { 403 | "kind": "SCALAR", 404 | "name": "String", 405 | "ofType": null 406 | }, 407 | "isDeprecated": false, 408 | "deprecationReason": null 409 | } 410 | ], 411 | "inputFields": null, 412 | "interfaces": [], 413 | "enumValues": null, 414 | "possibleTypes": null 415 | }, 416 | { 417 | "kind": "OBJECT", 418 | "name": "TodoEdge", 419 | "description": "An edge in a connection.", 420 | "fields": [ 421 | { 422 | "name": "node", 423 | "description": "The item at the end of the edge", 424 | "args": [], 425 | "type": { 426 | "kind": "OBJECT", 427 | "name": "Todo", 428 | "ofType": null 429 | }, 430 | "isDeprecated": false, 431 | "deprecationReason": null 432 | }, 433 | { 434 | "name": "cursor", 435 | "description": "A cursor for use in pagination", 436 | "args": [], 437 | "type": { 438 | "kind": "NON_NULL", 439 | "name": null, 440 | "ofType": { 441 | "kind": "SCALAR", 442 | "name": "String", 443 | "ofType": null 444 | } 445 | }, 446 | "isDeprecated": false, 447 | "deprecationReason": null 448 | } 449 | ], 450 | "inputFields": null, 451 | "interfaces": [], 452 | "enumValues": null, 453 | "possibleTypes": null 454 | }, 455 | { 456 | "kind": "OBJECT", 457 | "name": "Mutation", 458 | "description": null, 459 | "fields": [ 460 | { 461 | "name": "addTodo", 462 | "description": null, 463 | "args": [ 464 | { 465 | "name": "input", 466 | "description": null, 467 | "type": { 468 | "kind": "NON_NULL", 469 | "name": null, 470 | "ofType": { 471 | "kind": "INPUT_OBJECT", 472 | "name": "AddTodoInput", 473 | "ofType": null 474 | } 475 | }, 476 | "defaultValue": null 477 | } 478 | ], 479 | "type": { 480 | "kind": "OBJECT", 481 | "name": "AddTodoPayload", 482 | "ofType": null 483 | }, 484 | "isDeprecated": false, 485 | "deprecationReason": null 486 | }, 487 | { 488 | "name": "changeTodoStatus", 489 | "description": null, 490 | "args": [ 491 | { 492 | "name": "input", 493 | "description": null, 494 | "type": { 495 | "kind": "NON_NULL", 496 | "name": null, 497 | "ofType": { 498 | "kind": "INPUT_OBJECT", 499 | "name": "ChangeTodoStatusInput", 500 | "ofType": null 501 | } 502 | }, 503 | "defaultValue": null 504 | } 505 | ], 506 | "type": { 507 | "kind": "OBJECT", 508 | "name": "ChangeTodoStatusPayload", 509 | "ofType": null 510 | }, 511 | "isDeprecated": false, 512 | "deprecationReason": null 513 | }, 514 | { 515 | "name": "markAllTodos", 516 | "description": null, 517 | "args": [ 518 | { 519 | "name": "input", 520 | "description": null, 521 | "type": { 522 | "kind": "NON_NULL", 523 | "name": null, 524 | "ofType": { 525 | "kind": "INPUT_OBJECT", 526 | "name": "MarkAllTodosInput", 527 | "ofType": null 528 | } 529 | }, 530 | "defaultValue": null 531 | } 532 | ], 533 | "type": { 534 | "kind": "OBJECT", 535 | "name": "MarkAllTodosPayload", 536 | "ofType": null 537 | }, 538 | "isDeprecated": false, 539 | "deprecationReason": null 540 | }, 541 | { 542 | "name": "removeCompletedTodos", 543 | "description": null, 544 | "args": [ 545 | { 546 | "name": "input", 547 | "description": null, 548 | "type": { 549 | "kind": "NON_NULL", 550 | "name": null, 551 | "ofType": { 552 | "kind": "INPUT_OBJECT", 553 | "name": "RemoveCompletedTodosInput", 554 | "ofType": null 555 | } 556 | }, 557 | "defaultValue": null 558 | } 559 | ], 560 | "type": { 561 | "kind": "OBJECT", 562 | "name": "RemoveCompletedTodosPayload", 563 | "ofType": null 564 | }, 565 | "isDeprecated": false, 566 | "deprecationReason": null 567 | }, 568 | { 569 | "name": "removeTodo", 570 | "description": null, 571 | "args": [ 572 | { 573 | "name": "input", 574 | "description": null, 575 | "type": { 576 | "kind": "NON_NULL", 577 | "name": null, 578 | "ofType": { 579 | "kind": "INPUT_OBJECT", 580 | "name": "RemoveTodoInput", 581 | "ofType": null 582 | } 583 | }, 584 | "defaultValue": null 585 | } 586 | ], 587 | "type": { 588 | "kind": "OBJECT", 589 | "name": "RemoveTodoPayload", 590 | "ofType": null 591 | }, 592 | "isDeprecated": false, 593 | "deprecationReason": null 594 | }, 595 | { 596 | "name": "renameTodo", 597 | "description": null, 598 | "args": [ 599 | { 600 | "name": "input", 601 | "description": null, 602 | "type": { 603 | "kind": "NON_NULL", 604 | "name": null, 605 | "ofType": { 606 | "kind": "INPUT_OBJECT", 607 | "name": "RenameTodoInput", 608 | "ofType": null 609 | } 610 | }, 611 | "defaultValue": null 612 | } 613 | ], 614 | "type": { 615 | "kind": "OBJECT", 616 | "name": "RenameTodoPayload", 617 | "ofType": null 618 | }, 619 | "isDeprecated": false, 620 | "deprecationReason": null 621 | } 622 | ], 623 | "inputFields": null, 624 | "interfaces": [], 625 | "enumValues": null, 626 | "possibleTypes": null 627 | }, 628 | { 629 | "kind": "INPUT_OBJECT", 630 | "name": "AddTodoInput", 631 | "description": null, 632 | "fields": null, 633 | "inputFields": [ 634 | { 635 | "name": "text", 636 | "description": null, 637 | "type": { 638 | "kind": "NON_NULL", 639 | "name": null, 640 | "ofType": { 641 | "kind": "SCALAR", 642 | "name": "String", 643 | "ofType": null 644 | } 645 | }, 646 | "defaultValue": null 647 | }, 648 | { 649 | "name": "clientMutationId", 650 | "description": null, 651 | "type": { 652 | "kind": "NON_NULL", 653 | "name": null, 654 | "ofType": { 655 | "kind": "SCALAR", 656 | "name": "String", 657 | "ofType": null 658 | } 659 | }, 660 | "defaultValue": null 661 | } 662 | ], 663 | "interfaces": null, 664 | "enumValues": null, 665 | "possibleTypes": null 666 | }, 667 | { 668 | "kind": "OBJECT", 669 | "name": "AddTodoPayload", 670 | "description": null, 671 | "fields": [ 672 | { 673 | "name": "todoEdge", 674 | "description": null, 675 | "args": [], 676 | "type": { 677 | "kind": "OBJECT", 678 | "name": "TodoEdge", 679 | "ofType": null 680 | }, 681 | "isDeprecated": false, 682 | "deprecationReason": null 683 | }, 684 | { 685 | "name": "viewer", 686 | "description": null, 687 | "args": [], 688 | "type": { 689 | "kind": "OBJECT", 690 | "name": "User", 691 | "ofType": null 692 | }, 693 | "isDeprecated": false, 694 | "deprecationReason": null 695 | }, 696 | { 697 | "name": "clientMutationId", 698 | "description": null, 699 | "args": [], 700 | "type": { 701 | "kind": "NON_NULL", 702 | "name": null, 703 | "ofType": { 704 | "kind": "SCALAR", 705 | "name": "String", 706 | "ofType": null 707 | } 708 | }, 709 | "isDeprecated": false, 710 | "deprecationReason": null 711 | } 712 | ], 713 | "inputFields": null, 714 | "interfaces": [], 715 | "enumValues": null, 716 | "possibleTypes": null 717 | }, 718 | { 719 | "kind": "INPUT_OBJECT", 720 | "name": "ChangeTodoStatusInput", 721 | "description": null, 722 | "fields": null, 723 | "inputFields": [ 724 | { 725 | "name": "complete", 726 | "description": null, 727 | "type": { 728 | "kind": "NON_NULL", 729 | "name": null, 730 | "ofType": { 731 | "kind": "SCALAR", 732 | "name": "Boolean", 733 | "ofType": null 734 | } 735 | }, 736 | "defaultValue": null 737 | }, 738 | { 739 | "name": "id", 740 | "description": null, 741 | "type": { 742 | "kind": "NON_NULL", 743 | "name": null, 744 | "ofType": { 745 | "kind": "SCALAR", 746 | "name": "ID", 747 | "ofType": null 748 | } 749 | }, 750 | "defaultValue": null 751 | }, 752 | { 753 | "name": "clientMutationId", 754 | "description": null, 755 | "type": { 756 | "kind": "NON_NULL", 757 | "name": null, 758 | "ofType": { 759 | "kind": "SCALAR", 760 | "name": "String", 761 | "ofType": null 762 | } 763 | }, 764 | "defaultValue": null 765 | } 766 | ], 767 | "interfaces": null, 768 | "enumValues": null, 769 | "possibleTypes": null 770 | }, 771 | { 772 | "kind": "OBJECT", 773 | "name": "ChangeTodoStatusPayload", 774 | "description": null, 775 | "fields": [ 776 | { 777 | "name": "todo", 778 | "description": null, 779 | "args": [], 780 | "type": { 781 | "kind": "OBJECT", 782 | "name": "Todo", 783 | "ofType": null 784 | }, 785 | "isDeprecated": false, 786 | "deprecationReason": null 787 | }, 788 | { 789 | "name": "viewer", 790 | "description": null, 791 | "args": [], 792 | "type": { 793 | "kind": "OBJECT", 794 | "name": "User", 795 | "ofType": null 796 | }, 797 | "isDeprecated": false, 798 | "deprecationReason": null 799 | }, 800 | { 801 | "name": "clientMutationId", 802 | "description": null, 803 | "args": [], 804 | "type": { 805 | "kind": "NON_NULL", 806 | "name": null, 807 | "ofType": { 808 | "kind": "SCALAR", 809 | "name": "String", 810 | "ofType": null 811 | } 812 | }, 813 | "isDeprecated": false, 814 | "deprecationReason": null 815 | } 816 | ], 817 | "inputFields": null, 818 | "interfaces": [], 819 | "enumValues": null, 820 | "possibleTypes": null 821 | }, 822 | { 823 | "kind": "INPUT_OBJECT", 824 | "name": "MarkAllTodosInput", 825 | "description": null, 826 | "fields": null, 827 | "inputFields": [ 828 | { 829 | "name": "complete", 830 | "description": null, 831 | "type": { 832 | "kind": "NON_NULL", 833 | "name": null, 834 | "ofType": { 835 | "kind": "SCALAR", 836 | "name": "Boolean", 837 | "ofType": null 838 | } 839 | }, 840 | "defaultValue": null 841 | }, 842 | { 843 | "name": "clientMutationId", 844 | "description": null, 845 | "type": { 846 | "kind": "NON_NULL", 847 | "name": null, 848 | "ofType": { 849 | "kind": "SCALAR", 850 | "name": "String", 851 | "ofType": null 852 | } 853 | }, 854 | "defaultValue": null 855 | } 856 | ], 857 | "interfaces": null, 858 | "enumValues": null, 859 | "possibleTypes": null 860 | }, 861 | { 862 | "kind": "OBJECT", 863 | "name": "MarkAllTodosPayload", 864 | "description": null, 865 | "fields": [ 866 | { 867 | "name": "changedTodos", 868 | "description": null, 869 | "args": [], 870 | "type": { 871 | "kind": "LIST", 872 | "name": null, 873 | "ofType": { 874 | "kind": "OBJECT", 875 | "name": "Todo", 876 | "ofType": null 877 | } 878 | }, 879 | "isDeprecated": false, 880 | "deprecationReason": null 881 | }, 882 | { 883 | "name": "viewer", 884 | "description": null, 885 | "args": [], 886 | "type": { 887 | "kind": "OBJECT", 888 | "name": "User", 889 | "ofType": null 890 | }, 891 | "isDeprecated": false, 892 | "deprecationReason": null 893 | }, 894 | { 895 | "name": "clientMutationId", 896 | "description": null, 897 | "args": [], 898 | "type": { 899 | "kind": "NON_NULL", 900 | "name": null, 901 | "ofType": { 902 | "kind": "SCALAR", 903 | "name": "String", 904 | "ofType": null 905 | } 906 | }, 907 | "isDeprecated": false, 908 | "deprecationReason": null 909 | } 910 | ], 911 | "inputFields": null, 912 | "interfaces": [], 913 | "enumValues": null, 914 | "possibleTypes": null 915 | }, 916 | { 917 | "kind": "INPUT_OBJECT", 918 | "name": "RemoveCompletedTodosInput", 919 | "description": null, 920 | "fields": null, 921 | "inputFields": [ 922 | { 923 | "name": "clientMutationId", 924 | "description": null, 925 | "type": { 926 | "kind": "NON_NULL", 927 | "name": null, 928 | "ofType": { 929 | "kind": "SCALAR", 930 | "name": "String", 931 | "ofType": null 932 | } 933 | }, 934 | "defaultValue": null 935 | } 936 | ], 937 | "interfaces": null, 938 | "enumValues": null, 939 | "possibleTypes": null 940 | }, 941 | { 942 | "kind": "OBJECT", 943 | "name": "RemoveCompletedTodosPayload", 944 | "description": null, 945 | "fields": [ 946 | { 947 | "name": "deletedTodoIds", 948 | "description": null, 949 | "args": [], 950 | "type": { 951 | "kind": "LIST", 952 | "name": null, 953 | "ofType": { 954 | "kind": "SCALAR", 955 | "name": "String", 956 | "ofType": null 957 | } 958 | }, 959 | "isDeprecated": false, 960 | "deprecationReason": null 961 | }, 962 | { 963 | "name": "viewer", 964 | "description": null, 965 | "args": [], 966 | "type": { 967 | "kind": "OBJECT", 968 | "name": "User", 969 | "ofType": null 970 | }, 971 | "isDeprecated": false, 972 | "deprecationReason": null 973 | }, 974 | { 975 | "name": "clientMutationId", 976 | "description": null, 977 | "args": [], 978 | "type": { 979 | "kind": "NON_NULL", 980 | "name": null, 981 | "ofType": { 982 | "kind": "SCALAR", 983 | "name": "String", 984 | "ofType": null 985 | } 986 | }, 987 | "isDeprecated": false, 988 | "deprecationReason": null 989 | } 990 | ], 991 | "inputFields": null, 992 | "interfaces": [], 993 | "enumValues": null, 994 | "possibleTypes": null 995 | }, 996 | { 997 | "kind": "INPUT_OBJECT", 998 | "name": "RemoveTodoInput", 999 | "description": null, 1000 | "fields": null, 1001 | "inputFields": [ 1002 | { 1003 | "name": "id", 1004 | "description": null, 1005 | "type": { 1006 | "kind": "NON_NULL", 1007 | "name": null, 1008 | "ofType": { 1009 | "kind": "SCALAR", 1010 | "name": "ID", 1011 | "ofType": null 1012 | } 1013 | }, 1014 | "defaultValue": null 1015 | }, 1016 | { 1017 | "name": "clientMutationId", 1018 | "description": null, 1019 | "type": { 1020 | "kind": "NON_NULL", 1021 | "name": null, 1022 | "ofType": { 1023 | "kind": "SCALAR", 1024 | "name": "String", 1025 | "ofType": null 1026 | } 1027 | }, 1028 | "defaultValue": null 1029 | } 1030 | ], 1031 | "interfaces": null, 1032 | "enumValues": null, 1033 | "possibleTypes": null 1034 | }, 1035 | { 1036 | "kind": "OBJECT", 1037 | "name": "RemoveTodoPayload", 1038 | "description": null, 1039 | "fields": [ 1040 | { 1041 | "name": "deletedTodoId", 1042 | "description": null, 1043 | "args": [], 1044 | "type": { 1045 | "kind": "SCALAR", 1046 | "name": "ID", 1047 | "ofType": null 1048 | }, 1049 | "isDeprecated": false, 1050 | "deprecationReason": null 1051 | }, 1052 | { 1053 | "name": "viewer", 1054 | "description": null, 1055 | "args": [], 1056 | "type": { 1057 | "kind": "OBJECT", 1058 | "name": "User", 1059 | "ofType": null 1060 | }, 1061 | "isDeprecated": false, 1062 | "deprecationReason": null 1063 | }, 1064 | { 1065 | "name": "clientMutationId", 1066 | "description": null, 1067 | "args": [], 1068 | "type": { 1069 | "kind": "NON_NULL", 1070 | "name": null, 1071 | "ofType": { 1072 | "kind": "SCALAR", 1073 | "name": "String", 1074 | "ofType": null 1075 | } 1076 | }, 1077 | "isDeprecated": false, 1078 | "deprecationReason": null 1079 | } 1080 | ], 1081 | "inputFields": null, 1082 | "interfaces": [], 1083 | "enumValues": null, 1084 | "possibleTypes": null 1085 | }, 1086 | { 1087 | "kind": "INPUT_OBJECT", 1088 | "name": "RenameTodoInput", 1089 | "description": null, 1090 | "fields": null, 1091 | "inputFields": [ 1092 | { 1093 | "name": "id", 1094 | "description": null, 1095 | "type": { 1096 | "kind": "NON_NULL", 1097 | "name": null, 1098 | "ofType": { 1099 | "kind": "SCALAR", 1100 | "name": "ID", 1101 | "ofType": null 1102 | } 1103 | }, 1104 | "defaultValue": null 1105 | }, 1106 | { 1107 | "name": "text", 1108 | "description": null, 1109 | "type": { 1110 | "kind": "NON_NULL", 1111 | "name": null, 1112 | "ofType": { 1113 | "kind": "SCALAR", 1114 | "name": "String", 1115 | "ofType": null 1116 | } 1117 | }, 1118 | "defaultValue": null 1119 | }, 1120 | { 1121 | "name": "clientMutationId", 1122 | "description": null, 1123 | "type": { 1124 | "kind": "NON_NULL", 1125 | "name": null, 1126 | "ofType": { 1127 | "kind": "SCALAR", 1128 | "name": "String", 1129 | "ofType": null 1130 | } 1131 | }, 1132 | "defaultValue": null 1133 | } 1134 | ], 1135 | "interfaces": null, 1136 | "enumValues": null, 1137 | "possibleTypes": null 1138 | }, 1139 | { 1140 | "kind": "OBJECT", 1141 | "name": "RenameTodoPayload", 1142 | "description": null, 1143 | "fields": [ 1144 | { 1145 | "name": "todo", 1146 | "description": null, 1147 | "args": [], 1148 | "type": { 1149 | "kind": "OBJECT", 1150 | "name": "Todo", 1151 | "ofType": null 1152 | }, 1153 | "isDeprecated": false, 1154 | "deprecationReason": null 1155 | }, 1156 | { 1157 | "name": "clientMutationId", 1158 | "description": null, 1159 | "args": [], 1160 | "type": { 1161 | "kind": "NON_NULL", 1162 | "name": null, 1163 | "ofType": { 1164 | "kind": "SCALAR", 1165 | "name": "String", 1166 | "ofType": null 1167 | } 1168 | }, 1169 | "isDeprecated": false, 1170 | "deprecationReason": null 1171 | } 1172 | ], 1173 | "inputFields": null, 1174 | "interfaces": [], 1175 | "enumValues": null, 1176 | "possibleTypes": null 1177 | }, 1178 | { 1179 | "kind": "OBJECT", 1180 | "name": "__Schema", 1181 | "description": "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query and mutation operations.", 1182 | "fields": [ 1183 | { 1184 | "name": "types", 1185 | "description": "A list of all types supported by this server.", 1186 | "args": [], 1187 | "type": { 1188 | "kind": "NON_NULL", 1189 | "name": null, 1190 | "ofType": { 1191 | "kind": "LIST", 1192 | "name": null, 1193 | "ofType": { 1194 | "kind": "NON_NULL", 1195 | "name": null, 1196 | "ofType": { 1197 | "kind": "OBJECT", 1198 | "name": "__Type" 1199 | } 1200 | } 1201 | } 1202 | }, 1203 | "isDeprecated": false, 1204 | "deprecationReason": null 1205 | }, 1206 | { 1207 | "name": "queryType", 1208 | "description": "The type that query operations will be rooted at.", 1209 | "args": [], 1210 | "type": { 1211 | "kind": "NON_NULL", 1212 | "name": null, 1213 | "ofType": { 1214 | "kind": "OBJECT", 1215 | "name": "__Type", 1216 | "ofType": null 1217 | } 1218 | }, 1219 | "isDeprecated": false, 1220 | "deprecationReason": null 1221 | }, 1222 | { 1223 | "name": "mutationType", 1224 | "description": "If this server supports mutation, the type that mutation operations will be rooted at.", 1225 | "args": [], 1226 | "type": { 1227 | "kind": "OBJECT", 1228 | "name": "__Type", 1229 | "ofType": null 1230 | }, 1231 | "isDeprecated": false, 1232 | "deprecationReason": null 1233 | }, 1234 | { 1235 | "name": "directives", 1236 | "description": "A list of all directives supported by this server.", 1237 | "args": [], 1238 | "type": { 1239 | "kind": "NON_NULL", 1240 | "name": null, 1241 | "ofType": { 1242 | "kind": "LIST", 1243 | "name": null, 1244 | "ofType": { 1245 | "kind": "NON_NULL", 1246 | "name": null, 1247 | "ofType": { 1248 | "kind": "OBJECT", 1249 | "name": "__Directive" 1250 | } 1251 | } 1252 | } 1253 | }, 1254 | "isDeprecated": false, 1255 | "deprecationReason": null 1256 | } 1257 | ], 1258 | "inputFields": null, 1259 | "interfaces": [], 1260 | "enumValues": null, 1261 | "possibleTypes": null 1262 | }, 1263 | { 1264 | "kind": "OBJECT", 1265 | "name": "__Type", 1266 | "description": null, 1267 | "fields": [ 1268 | { 1269 | "name": "kind", 1270 | "description": null, 1271 | "args": [], 1272 | "type": { 1273 | "kind": "NON_NULL", 1274 | "name": null, 1275 | "ofType": { 1276 | "kind": "ENUM", 1277 | "name": "__TypeKind", 1278 | "ofType": null 1279 | } 1280 | }, 1281 | "isDeprecated": false, 1282 | "deprecationReason": null 1283 | }, 1284 | { 1285 | "name": "name", 1286 | "description": null, 1287 | "args": [], 1288 | "type": { 1289 | "kind": "SCALAR", 1290 | "name": "String", 1291 | "ofType": null 1292 | }, 1293 | "isDeprecated": false, 1294 | "deprecationReason": null 1295 | }, 1296 | { 1297 | "name": "description", 1298 | "description": null, 1299 | "args": [], 1300 | "type": { 1301 | "kind": "SCALAR", 1302 | "name": "String", 1303 | "ofType": null 1304 | }, 1305 | "isDeprecated": false, 1306 | "deprecationReason": null 1307 | }, 1308 | { 1309 | "name": "fields", 1310 | "description": null, 1311 | "args": [ 1312 | { 1313 | "name": "includeDeprecated", 1314 | "description": null, 1315 | "type": { 1316 | "kind": "SCALAR", 1317 | "name": "Boolean", 1318 | "ofType": null 1319 | }, 1320 | "defaultValue": "false" 1321 | } 1322 | ], 1323 | "type": { 1324 | "kind": "LIST", 1325 | "name": null, 1326 | "ofType": { 1327 | "kind": "NON_NULL", 1328 | "name": null, 1329 | "ofType": { 1330 | "kind": "OBJECT", 1331 | "name": "__Field", 1332 | "ofType": null 1333 | } 1334 | } 1335 | }, 1336 | "isDeprecated": false, 1337 | "deprecationReason": null 1338 | }, 1339 | { 1340 | "name": "interfaces", 1341 | "description": null, 1342 | "args": [], 1343 | "type": { 1344 | "kind": "LIST", 1345 | "name": null, 1346 | "ofType": { 1347 | "kind": "NON_NULL", 1348 | "name": null, 1349 | "ofType": { 1350 | "kind": "OBJECT", 1351 | "name": "__Type", 1352 | "ofType": null 1353 | } 1354 | } 1355 | }, 1356 | "isDeprecated": false, 1357 | "deprecationReason": null 1358 | }, 1359 | { 1360 | "name": "possibleTypes", 1361 | "description": null, 1362 | "args": [], 1363 | "type": { 1364 | "kind": "LIST", 1365 | "name": null, 1366 | "ofType": { 1367 | "kind": "NON_NULL", 1368 | "name": null, 1369 | "ofType": { 1370 | "kind": "OBJECT", 1371 | "name": "__Type", 1372 | "ofType": null 1373 | } 1374 | } 1375 | }, 1376 | "isDeprecated": false, 1377 | "deprecationReason": null 1378 | }, 1379 | { 1380 | "name": "enumValues", 1381 | "description": null, 1382 | "args": [ 1383 | { 1384 | "name": "includeDeprecated", 1385 | "description": null, 1386 | "type": { 1387 | "kind": "SCALAR", 1388 | "name": "Boolean", 1389 | "ofType": null 1390 | }, 1391 | "defaultValue": "false" 1392 | } 1393 | ], 1394 | "type": { 1395 | "kind": "LIST", 1396 | "name": null, 1397 | "ofType": { 1398 | "kind": "NON_NULL", 1399 | "name": null, 1400 | "ofType": { 1401 | "kind": "OBJECT", 1402 | "name": "__EnumValue", 1403 | "ofType": null 1404 | } 1405 | } 1406 | }, 1407 | "isDeprecated": false, 1408 | "deprecationReason": null 1409 | }, 1410 | { 1411 | "name": "inputFields", 1412 | "description": null, 1413 | "args": [], 1414 | "type": { 1415 | "kind": "LIST", 1416 | "name": null, 1417 | "ofType": { 1418 | "kind": "NON_NULL", 1419 | "name": null, 1420 | "ofType": { 1421 | "kind": "OBJECT", 1422 | "name": "__InputValue", 1423 | "ofType": null 1424 | } 1425 | } 1426 | }, 1427 | "isDeprecated": false, 1428 | "deprecationReason": null 1429 | }, 1430 | { 1431 | "name": "ofType", 1432 | "description": null, 1433 | "args": [], 1434 | "type": { 1435 | "kind": "OBJECT", 1436 | "name": "__Type", 1437 | "ofType": null 1438 | }, 1439 | "isDeprecated": false, 1440 | "deprecationReason": null 1441 | } 1442 | ], 1443 | "inputFields": null, 1444 | "interfaces": [], 1445 | "enumValues": null, 1446 | "possibleTypes": null 1447 | }, 1448 | { 1449 | "kind": "ENUM", 1450 | "name": "__TypeKind", 1451 | "description": "An enum describing what kind of type a given __Type is", 1452 | "fields": null, 1453 | "inputFields": null, 1454 | "interfaces": null, 1455 | "enumValues": [ 1456 | { 1457 | "name": "SCALAR", 1458 | "description": "Indicates this type is a scalar.", 1459 | "isDeprecated": false, 1460 | "deprecationReason": null 1461 | }, 1462 | { 1463 | "name": "OBJECT", 1464 | "description": "Indicates this type is an object. `fields` and `interfaces` are valid fields.", 1465 | "isDeprecated": false, 1466 | "deprecationReason": null 1467 | }, 1468 | { 1469 | "name": "INTERFACE", 1470 | "description": "Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.", 1471 | "isDeprecated": false, 1472 | "deprecationReason": null 1473 | }, 1474 | { 1475 | "name": "UNION", 1476 | "description": "Indicates this type is a union. `possibleTypes` is a valid field.", 1477 | "isDeprecated": false, 1478 | "deprecationReason": null 1479 | }, 1480 | { 1481 | "name": "ENUM", 1482 | "description": "Indicates this type is an enum. `enumValues` is a valid field.", 1483 | "isDeprecated": false, 1484 | "deprecationReason": null 1485 | }, 1486 | { 1487 | "name": "INPUT_OBJECT", 1488 | "description": "Indicates this type is an input object. `inputFields` is a valid field.", 1489 | "isDeprecated": false, 1490 | "deprecationReason": null 1491 | }, 1492 | { 1493 | "name": "LIST", 1494 | "description": "Indicates this type is a list. `ofType` is a valid field.", 1495 | "isDeprecated": false, 1496 | "deprecationReason": null 1497 | }, 1498 | { 1499 | "name": "NON_NULL", 1500 | "description": "Indicates this type is a non-null. `ofType` is a valid field.", 1501 | "isDeprecated": false, 1502 | "deprecationReason": null 1503 | } 1504 | ], 1505 | "possibleTypes": null 1506 | }, 1507 | { 1508 | "kind": "OBJECT", 1509 | "name": "__Field", 1510 | "description": null, 1511 | "fields": [ 1512 | { 1513 | "name": "name", 1514 | "description": null, 1515 | "args": [], 1516 | "type": { 1517 | "kind": "NON_NULL", 1518 | "name": null, 1519 | "ofType": { 1520 | "kind": "SCALAR", 1521 | "name": "String", 1522 | "ofType": null 1523 | } 1524 | }, 1525 | "isDeprecated": false, 1526 | "deprecationReason": null 1527 | }, 1528 | { 1529 | "name": "description", 1530 | "description": null, 1531 | "args": [], 1532 | "type": { 1533 | "kind": "SCALAR", 1534 | "name": "String", 1535 | "ofType": null 1536 | }, 1537 | "isDeprecated": false, 1538 | "deprecationReason": null 1539 | }, 1540 | { 1541 | "name": "args", 1542 | "description": null, 1543 | "args": [], 1544 | "type": { 1545 | "kind": "NON_NULL", 1546 | "name": null, 1547 | "ofType": { 1548 | "kind": "LIST", 1549 | "name": null, 1550 | "ofType": { 1551 | "kind": "NON_NULL", 1552 | "name": null, 1553 | "ofType": { 1554 | "kind": "OBJECT", 1555 | "name": "__InputValue" 1556 | } 1557 | } 1558 | } 1559 | }, 1560 | "isDeprecated": false, 1561 | "deprecationReason": null 1562 | }, 1563 | { 1564 | "name": "type", 1565 | "description": null, 1566 | "args": [], 1567 | "type": { 1568 | "kind": "NON_NULL", 1569 | "name": null, 1570 | "ofType": { 1571 | "kind": "OBJECT", 1572 | "name": "__Type", 1573 | "ofType": null 1574 | } 1575 | }, 1576 | "isDeprecated": false, 1577 | "deprecationReason": null 1578 | }, 1579 | { 1580 | "name": "isDeprecated", 1581 | "description": null, 1582 | "args": [], 1583 | "type": { 1584 | "kind": "NON_NULL", 1585 | "name": null, 1586 | "ofType": { 1587 | "kind": "SCALAR", 1588 | "name": "Boolean", 1589 | "ofType": null 1590 | } 1591 | }, 1592 | "isDeprecated": false, 1593 | "deprecationReason": null 1594 | }, 1595 | { 1596 | "name": "deprecationReason", 1597 | "description": null, 1598 | "args": [], 1599 | "type": { 1600 | "kind": "SCALAR", 1601 | "name": "String", 1602 | "ofType": null 1603 | }, 1604 | "isDeprecated": false, 1605 | "deprecationReason": null 1606 | } 1607 | ], 1608 | "inputFields": null, 1609 | "interfaces": [], 1610 | "enumValues": null, 1611 | "possibleTypes": null 1612 | }, 1613 | { 1614 | "kind": "OBJECT", 1615 | "name": "__InputValue", 1616 | "description": null, 1617 | "fields": [ 1618 | { 1619 | "name": "name", 1620 | "description": null, 1621 | "args": [], 1622 | "type": { 1623 | "kind": "NON_NULL", 1624 | "name": null, 1625 | "ofType": { 1626 | "kind": "SCALAR", 1627 | "name": "String", 1628 | "ofType": null 1629 | } 1630 | }, 1631 | "isDeprecated": false, 1632 | "deprecationReason": null 1633 | }, 1634 | { 1635 | "name": "description", 1636 | "description": null, 1637 | "args": [], 1638 | "type": { 1639 | "kind": "SCALAR", 1640 | "name": "String", 1641 | "ofType": null 1642 | }, 1643 | "isDeprecated": false, 1644 | "deprecationReason": null 1645 | }, 1646 | { 1647 | "name": "type", 1648 | "description": null, 1649 | "args": [], 1650 | "type": { 1651 | "kind": "NON_NULL", 1652 | "name": null, 1653 | "ofType": { 1654 | "kind": "OBJECT", 1655 | "name": "__Type", 1656 | "ofType": null 1657 | } 1658 | }, 1659 | "isDeprecated": false, 1660 | "deprecationReason": null 1661 | }, 1662 | { 1663 | "name": "defaultValue", 1664 | "description": null, 1665 | "args": [], 1666 | "type": { 1667 | "kind": "SCALAR", 1668 | "name": "String", 1669 | "ofType": null 1670 | }, 1671 | "isDeprecated": false, 1672 | "deprecationReason": null 1673 | } 1674 | ], 1675 | "inputFields": null, 1676 | "interfaces": [], 1677 | "enumValues": null, 1678 | "possibleTypes": null 1679 | }, 1680 | { 1681 | "kind": "OBJECT", 1682 | "name": "__EnumValue", 1683 | "description": null, 1684 | "fields": [ 1685 | { 1686 | "name": "name", 1687 | "description": null, 1688 | "args": [], 1689 | "type": { 1690 | "kind": "NON_NULL", 1691 | "name": null, 1692 | "ofType": { 1693 | "kind": "SCALAR", 1694 | "name": "String", 1695 | "ofType": null 1696 | } 1697 | }, 1698 | "isDeprecated": false, 1699 | "deprecationReason": null 1700 | }, 1701 | { 1702 | "name": "description", 1703 | "description": null, 1704 | "args": [], 1705 | "type": { 1706 | "kind": "SCALAR", 1707 | "name": "String", 1708 | "ofType": null 1709 | }, 1710 | "isDeprecated": false, 1711 | "deprecationReason": null 1712 | }, 1713 | { 1714 | "name": "isDeprecated", 1715 | "description": null, 1716 | "args": [], 1717 | "type": { 1718 | "kind": "NON_NULL", 1719 | "name": null, 1720 | "ofType": { 1721 | "kind": "SCALAR", 1722 | "name": "Boolean", 1723 | "ofType": null 1724 | } 1725 | }, 1726 | "isDeprecated": false, 1727 | "deprecationReason": null 1728 | }, 1729 | { 1730 | "name": "deprecationReason", 1731 | "description": null, 1732 | "args": [], 1733 | "type": { 1734 | "kind": "SCALAR", 1735 | "name": "String", 1736 | "ofType": null 1737 | }, 1738 | "isDeprecated": false, 1739 | "deprecationReason": null 1740 | } 1741 | ], 1742 | "inputFields": null, 1743 | "interfaces": [], 1744 | "enumValues": null, 1745 | "possibleTypes": null 1746 | }, 1747 | { 1748 | "kind": "OBJECT", 1749 | "name": "__Directive", 1750 | "description": null, 1751 | "fields": [ 1752 | { 1753 | "name": "name", 1754 | "description": null, 1755 | "args": [], 1756 | "type": { 1757 | "kind": "NON_NULL", 1758 | "name": null, 1759 | "ofType": { 1760 | "kind": "SCALAR", 1761 | "name": "String", 1762 | "ofType": null 1763 | } 1764 | }, 1765 | "isDeprecated": false, 1766 | "deprecationReason": null 1767 | }, 1768 | { 1769 | "name": "description", 1770 | "description": null, 1771 | "args": [], 1772 | "type": { 1773 | "kind": "SCALAR", 1774 | "name": "String", 1775 | "ofType": null 1776 | }, 1777 | "isDeprecated": false, 1778 | "deprecationReason": null 1779 | }, 1780 | { 1781 | "name": "args", 1782 | "description": null, 1783 | "args": [], 1784 | "type": { 1785 | "kind": "NON_NULL", 1786 | "name": null, 1787 | "ofType": { 1788 | "kind": "LIST", 1789 | "name": null, 1790 | "ofType": { 1791 | "kind": "NON_NULL", 1792 | "name": null, 1793 | "ofType": { 1794 | "kind": "OBJECT", 1795 | "name": "__InputValue" 1796 | } 1797 | } 1798 | } 1799 | }, 1800 | "isDeprecated": false, 1801 | "deprecationReason": null 1802 | }, 1803 | { 1804 | "name": "onOperation", 1805 | "description": null, 1806 | "args": [], 1807 | "type": { 1808 | "kind": "NON_NULL", 1809 | "name": null, 1810 | "ofType": { 1811 | "kind": "SCALAR", 1812 | "name": "Boolean", 1813 | "ofType": null 1814 | } 1815 | }, 1816 | "isDeprecated": false, 1817 | "deprecationReason": null 1818 | }, 1819 | { 1820 | "name": "onFragment", 1821 | "description": null, 1822 | "args": [], 1823 | "type": { 1824 | "kind": "NON_NULL", 1825 | "name": null, 1826 | "ofType": { 1827 | "kind": "SCALAR", 1828 | "name": "Boolean", 1829 | "ofType": null 1830 | } 1831 | }, 1832 | "isDeprecated": false, 1833 | "deprecationReason": null 1834 | }, 1835 | { 1836 | "name": "onField", 1837 | "description": null, 1838 | "args": [], 1839 | "type": { 1840 | "kind": "NON_NULL", 1841 | "name": null, 1842 | "ofType": { 1843 | "kind": "SCALAR", 1844 | "name": "Boolean", 1845 | "ofType": null 1846 | } 1847 | }, 1848 | "isDeprecated": false, 1849 | "deprecationReason": null 1850 | } 1851 | ], 1852 | "inputFields": null, 1853 | "interfaces": [], 1854 | "enumValues": null, 1855 | "possibleTypes": null 1856 | } 1857 | ], 1858 | "directives": [ 1859 | { 1860 | "name": "include", 1861 | "description": "Directs the executor to include this field or fragment only when the `if` argument is true.", 1862 | "args": [ 1863 | { 1864 | "name": "if", 1865 | "description": "Included when true.", 1866 | "type": { 1867 | "kind": "NON_NULL", 1868 | "name": null, 1869 | "ofType": { 1870 | "kind": "SCALAR", 1871 | "name": "Boolean", 1872 | "ofType": null 1873 | } 1874 | }, 1875 | "defaultValue": null 1876 | } 1877 | ], 1878 | "onOperation": false, 1879 | "onFragment": true, 1880 | "onField": true 1881 | }, 1882 | { 1883 | "name": "skip", 1884 | "description": "Directs the executor to skip this field or fragment when the `if` argument is true.", 1885 | "args": [ 1886 | { 1887 | "name": "if", 1888 | "description": "Skipped when true.", 1889 | "type": { 1890 | "kind": "NON_NULL", 1891 | "name": null, 1892 | "ofType": { 1893 | "kind": "SCALAR", 1894 | "name": "Boolean", 1895 | "ofType": null 1896 | } 1897 | }, 1898 | "defaultValue": null 1899 | } 1900 | ], 1901 | "onOperation": false, 1902 | "onFragment": true, 1903 | "onField": true 1904 | } 1905 | ] 1906 | } 1907 | } 1908 | } -------------------------------------------------------------------------------- /app/js/app.js: -------------------------------------------------------------------------------- 1 | import 'babel/polyfill'; 2 | import TodoApp from './components/TodoApp'; 3 | import TodoAppHomeRoute from './routes/TodoAppHomeRoute'; 4 | 5 | React.render( 6 | , 7 | document.getElementById('root') 8 | ); 9 | -------------------------------------------------------------------------------- /app/js/components/Todo.js: -------------------------------------------------------------------------------- 1 | import ChangeTodoStatusMutation from '../mutations/ChangeTodoStatusMutation'; 2 | import RemoveTodoMutation from '../mutations/RemoveTodoMutation'; 3 | import RenameTodoMutation from '../mutations/RenameTodoMutation'; 4 | import TodoTextInput from './TodoTextInput'; 5 | 6 | import classnames from 'classnames'; 7 | 8 | class Todo extends React.Component { 9 | state = { 10 | isEditing: false, 11 | }; 12 | _handleCompleteChange = (e) => { 13 | var complete = e.target.checked; 14 | Relay.Store.update( 15 | new ChangeTodoStatusMutation({ 16 | complete, 17 | todo: this.props.todo, 18 | viewer: this.props.viewer, 19 | }) 20 | ); 21 | } 22 | _handleDestroyClick = () => { 23 | this._removeTodo(); 24 | } 25 | _handleLabelDoubleClick = () => { 26 | this._setEditMode(true); 27 | } 28 | _handleTextInputCancel = () => { 29 | this._setEditMode(false); 30 | } 31 | _handleTextInputDelete = () => { 32 | this._setEditMode(false); 33 | this._removeTodo(); 34 | } 35 | _handleTextInputSave = (text) => { 36 | this._setEditMode(false); 37 | Relay.Store.update( 38 | new RenameTodoMutation({todo: this.props.todo, text}) 39 | ); 40 | } 41 | _removeTodo() { 42 | Relay.Store.update( 43 | new RemoveTodoMutation({todo: this.props.todo, viewer: this.props.viewer}) 44 | ); 45 | } 46 | _setEditMode = (shouldEdit) => { 47 | this.setState({isEditing: shouldEdit}); 48 | } 49 | renderTextInput() { 50 | return ( 51 | 59 | ); 60 | } 61 | render() { 62 | return ( 63 |
  • 68 |
    69 | 75 | 78 |
    83 | {this.state.isEditing && this.renderTextInput()} 84 |
  • 85 | ); 86 | } 87 | } 88 | 89 | export default Relay.createContainer(Todo, { 90 | fragments: { 91 | todo: () => Relay.QL` 92 | fragment on Todo { 93 | complete, 94 | id, 95 | text, 96 | ${ChangeTodoStatusMutation.getFragment('todo')}, 97 | ${RemoveTodoMutation.getFragment('todo')}, 98 | ${RenameTodoMutation.getFragment('todo')}, 99 | } 100 | `, 101 | viewer: () => Relay.QL` 102 | fragment on User { 103 | ${ChangeTodoStatusMutation.getFragment('viewer')}, 104 | ${RemoveTodoMutation.getFragment('viewer')}, 105 | } 106 | `, 107 | }, 108 | }); 109 | -------------------------------------------------------------------------------- /app/js/components/TodoApp.js: -------------------------------------------------------------------------------- 1 | import AddTodoMutation from '../mutations/AddTodoMutation'; 2 | import TodoList from './TodoList'; 3 | import TodoListFooter from './TodoListFooter'; 4 | import TodoTextInput from './TodoTextInput'; 5 | 6 | class TodoApp extends React.Component { 7 | _handleTextInputSave = (text) => { 8 | Relay.Store.update( 9 | new AddTodoMutation({text, viewer: this.props.viewer}) 10 | ); 11 | } 12 | render() { 13 | var hasTodos = this.props.viewer.todos.totalCount > 0; 14 | return ( 15 |
    16 |
    17 |
    18 |

    19 | todos 20 |

    21 | 27 |
    28 | {hasTodos && 29 | 33 | } 34 | {hasTodos && 35 | 39 | } 40 |
    41 | 54 |
    55 | ); 56 | } 57 | } 58 | 59 | export default Relay.createContainer(TodoApp, { 60 | fragments: { 61 | viewer: () => Relay.QL` 62 | fragment on User { 63 | todos(first: 90071) { 64 | edges { 65 | node { 66 | id, 67 | }, 68 | }, 69 | totalCount, 70 | ${TodoList.getFragment('todos')}, 71 | ${TodoListFooter.getFragment('todos')}, 72 | }, 73 | ${AddTodoMutation.getFragment('viewer')}, 74 | ${TodoList.getFragment('viewer')}, 75 | ${TodoListFooter.getFragment('viewer')}, 76 | } 77 | `, 78 | }, 79 | }); 80 | -------------------------------------------------------------------------------- /app/js/components/TodoList.js: -------------------------------------------------------------------------------- 1 | import MarkAllTodosMutation from '../mutations/MarkAllTodosMutation'; 2 | import Todo from './Todo'; 3 | 4 | class TodoList extends React.Component { 5 | _handleMarkAllChange = (e) => { 6 | var complete = e.target.checked; 7 | Relay.Store.update( 8 | new MarkAllTodosMutation({ 9 | complete, 10 | todos: this.props.todos, 11 | viewer: this.props.viewer, 12 | }) 13 | ); 14 | } 15 | renderTodos() { 16 | return this.props.todos.edges.map(edge => 17 | 22 | ); 23 | } 24 | render() { 25 | var numTodos = this.props.todos.totalCount; 26 | var numCompletedTodos = this.props.todos.completedCount; 27 | return ( 28 |
    29 | 35 | 38 |
      39 | {this.renderTodos()} 40 |
    41 |
    42 | ); 43 | } 44 | } 45 | 46 | export default Relay.createContainer(TodoList, { 47 | fragments: { 48 | todos: () => Relay.QL` 49 | fragment on TodoConnection { 50 | completedCount, 51 | edges { 52 | node { 53 | complete, 54 | id, 55 | ${Todo.getFragment('todo')}, 56 | }, 57 | }, 58 | totalCount, 59 | ${MarkAllTodosMutation.getFragment('todos')}, 60 | } 61 | `, 62 | viewer: () => Relay.QL` 63 | fragment on User { 64 | ${MarkAllTodosMutation.getFragment('viewer')}, 65 | ${Todo.getFragment('viewer')}, 66 | } 67 | `, 68 | }, 69 | }); 70 | -------------------------------------------------------------------------------- /app/js/components/TodoListFooter.js: -------------------------------------------------------------------------------- 1 | import RemoveCompletedTodosMutation from '../mutations/RemoveCompletedTodosMutation'; 2 | 3 | class TodoListFooter extends React.Component { 4 | _handleRemoveCompletedTodosClick = () => { 5 | Relay.Store.update( 6 | new RemoveCompletedTodosMutation({ 7 | todos: this.props.todos, 8 | viewer: this.props.viewer, 9 | }) 10 | ); 11 | } 12 | render() { 13 | var numTodos = this.props.todos.totalCount; 14 | var numCompletedTodos = this.props.todos.completedCount; 15 | return ( 16 |
    17 | 18 | {numTodos} item{numTodos === 1 ? '' : 's'} left 19 | 20 | { /* TODO: Implement routing 21 | 32 | */ } 33 | {numCompletedTodos > 0 && 34 | 39 | } 40 |
    41 | ); 42 | } 43 | } 44 | 45 | export default Relay.createContainer(TodoListFooter, { 46 | fragments: { 47 | todos: () => Relay.QL` 48 | fragment on TodoConnection { 49 | completedCount, 50 | edges { 51 | node { 52 | complete, 53 | }, 54 | }, 55 | totalCount, 56 | ${RemoveCompletedTodosMutation.getFragment('todos')}, 57 | } 58 | `, 59 | viewer: () => Relay.QL` 60 | fragment on User { 61 | ${RemoveCompletedTodosMutation.getFragment('viewer')}, 62 | } 63 | `, 64 | }, 65 | }); 66 | -------------------------------------------------------------------------------- /app/js/components/TodoTextInput.js: -------------------------------------------------------------------------------- 1 | var {PropTypes} = React; 2 | 3 | var ENTER_KEY_CODE = 13; 4 | var ESC_KEY_CODE = 27; 5 | 6 | export default class TodoTextInput extends React.Component { 7 | static defaultProps = { 8 | commitOnBlur: false, 9 | } 10 | static propTypes = { 11 | className: PropTypes.string, 12 | commitOnBlur: PropTypes.bool.isRequired, 13 | initialValue: PropTypes.string, 14 | onCancel: PropTypes.func, 15 | onDelete: PropTypes.func, 16 | onSave: PropTypes.func.isRequired, 17 | placeholder: PropTypes.string, 18 | } 19 | state = { 20 | isEditing: false, 21 | text: this.props.initialValue || '', 22 | }; 23 | componentDidMount() { 24 | React.findDOMNode(this).focus(); 25 | } 26 | _commitChanges = () => { 27 | var newText = this.state.text.trim(); 28 | if (this.props.onDelete && newText === '') { 29 | this.props.onDelete(); 30 | } else if (this.props.onCancel && newText === this.props.initialValue) { 31 | this.props.onCancel(); 32 | } else if (newText !== '') { 33 | this.props.onSave(newText); 34 | this.setState({text: ''}); 35 | } 36 | } 37 | _handleBlur = () => { 38 | if (this.props.commitOnBlur) { 39 | this._commitChanges(); 40 | } 41 | } 42 | _handleChange = (e) => { 43 | this.setState({text: e.target.value}); 44 | } 45 | _handleKeyDown = (e) => { 46 | if (this.props.onCancel && e.keyCode === ESC_KEY_CODE) { 47 | this.props.onCancel(); 48 | } else if (e.keyCode === ENTER_KEY_CODE) { 49 | this._commitChanges(); 50 | } 51 | } 52 | render() { 53 | return ( 54 | 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/js/mutations/AddTodoMutation.js: -------------------------------------------------------------------------------- 1 | export default class AddTodoMutation extends Relay.Mutation { 2 | static fragments = { 3 | viewer: () => Relay.QL` 4 | fragment on User { 5 | id, 6 | todos { 7 | totalCount, 8 | }, 9 | } 10 | `, 11 | }; 12 | getMutation() { 13 | return Relay.QL`mutation{addTodo}`; 14 | } 15 | getFatQuery() { 16 | return Relay.QL` 17 | fragment on AddTodoPayload { 18 | todoEdge, 19 | viewer { 20 | todos { 21 | totalCount, 22 | }, 23 | }, 24 | } 25 | `; 26 | } 27 | getConfigs() { 28 | return [{ 29 | type: 'RANGE_ADD', 30 | parentName: 'viewer', 31 | parentID: this.props.viewer.id, 32 | connectionName: 'todos', 33 | edgeName: 'todoEdge', 34 | rangeBehaviors: { 35 | '': 'append', 36 | }, 37 | }]; 38 | } 39 | getVariables() { 40 | return { 41 | text: this.props.text, 42 | }; 43 | } 44 | getOptimisticResponse() { 45 | return { 46 | // FIXME: totalCount gets updated optimistically, but this edge does not 47 | // get added until the server responds 48 | todoEdge: { 49 | node: { 50 | complete: false, 51 | text: this.props.text, 52 | }, 53 | }, 54 | viewer: { 55 | id: this.props.viewer.id, 56 | todos: { 57 | totalCount: this.props.viewer.todos.totalCount + 1, 58 | }, 59 | }, 60 | }; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/js/mutations/ChangeTodoStatusMutation.js: -------------------------------------------------------------------------------- 1 | export default class ChangeTodoStatusMutation extends Relay.Mutation { 2 | static fragments = { 3 | todo: () => Relay.QL` 4 | fragment on Todo { 5 | id, 6 | } 7 | `, 8 | // TODO: Mark completedCount optional 9 | viewer: () => Relay.QL` 10 | fragment on User { 11 | id, 12 | todos { 13 | completedCount, 14 | }, 15 | } 16 | `, 17 | }; 18 | getMutation() { 19 | return Relay.QL`mutation{changeTodoStatus}`; 20 | } 21 | getFatQuery() { 22 | return Relay.QL` 23 | fragment on ChangeTodoStatusPayload { 24 | todo { 25 | complete, 26 | }, 27 | viewer { 28 | todos { 29 | completedCount, 30 | }, 31 | }, 32 | } 33 | `; 34 | } 35 | getConfigs() { 36 | return [{ 37 | type: 'FIELDS_CHANGE', 38 | fieldIDs: { 39 | todo: this.props.todo.id, 40 | viewer: this.props.viewer.id, 41 | }, 42 | }]; 43 | } 44 | getVariables() { 45 | return { 46 | complete: this.props.complete, 47 | id: this.props.todo.id, 48 | }; 49 | } 50 | getOptimisticResponse() { 51 | var viewerPayload; 52 | if (this.props.viewer.todos) { 53 | viewerPayload = {id: this.props.viewer.id, todos: {}}; 54 | if (this.props.viewer.todos.completedCount != null) { 55 | viewerPayload.todos.completedCount = this.props.complete 56 | ? this.props.viewer.todos.completedCount + 1 57 | : this.props.viewer.todos.completedCount - 1; 58 | } 59 | } 60 | return { 61 | todo: { 62 | complete: this.props.complete, 63 | id: this.props.todo.id, 64 | }, 65 | viewer: viewerPayload, 66 | }; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/js/mutations/MarkAllTodosMutation.js: -------------------------------------------------------------------------------- 1 | export default class MarkAllTodosMutation extends Relay.Mutation { 2 | static fragments = { 3 | // TODO: Mark edges and totalCount optional 4 | todos: () => Relay.QL` 5 | fragment on TodoConnection { 6 | edges { 7 | node { 8 | complete, 9 | id, 10 | }, 11 | }, 12 | totalCount, 13 | } 14 | `, 15 | viewer: () => Relay.QL` 16 | fragment on User { 17 | id, 18 | } 19 | `, 20 | }; 21 | getMutation() { 22 | return Relay.QL`mutation{markAllTodos}`; 23 | } 24 | getFatQuery() { 25 | return Relay.QL` 26 | fragment on MarkAllTodosPayload { 27 | viewer { 28 | todos { 29 | completedCount, 30 | edges { 31 | node { 32 | complete, 33 | }, 34 | }, 35 | }, 36 | }, 37 | } 38 | `; 39 | } 40 | getConfigs() { 41 | return [{ 42 | type: 'FIELDS_CHANGE', 43 | fieldIDs: { 44 | viewer: this.props.viewer.id, 45 | }, 46 | }]; 47 | } 48 | getVariables() { 49 | return { 50 | complete: this.props.complete, 51 | }; 52 | } 53 | getOptimisticResponse() { 54 | var viewerPayload; 55 | if (this.props.todos) { 56 | viewerPayload = {id: this.props.viewer.id, todos: {}}; 57 | if (this.props.todos.edges) { 58 | viewerPayload.todos.edges = this.props.todos.edges 59 | .filter(edge => edge.node.complete !== this.props.complete) 60 | .map(edge => ({ 61 | node: { 62 | complete: this.props.complete, 63 | id: edge.node.id, 64 | }, 65 | })); 66 | } 67 | if (this.props.todos.totalCount != null) { 68 | viewerPayload.todos.completedCount = this.props.complete 69 | ? this.props.todos.totalCount 70 | : 0; 71 | } 72 | } 73 | return { 74 | viewer: viewerPayload, 75 | }; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app/js/mutations/RemoveCompletedTodosMutation.js: -------------------------------------------------------------------------------- 1 | export default class RemoveCompletedTodosMutation extends Relay.Mutation { 2 | static fragments = { 3 | // TODO: Make completedCount, edges, and totalCount optional 4 | todos: () => Relay.QL` 5 | fragment on TodoConnection { 6 | completedCount, 7 | edges { 8 | node { 9 | complete, 10 | id, 11 | }, 12 | }, 13 | totalCount, 14 | } 15 | `, 16 | viewer: () => Relay.QL` 17 | fragment on User { 18 | id, 19 | } 20 | `, 21 | }; 22 | getMutation() { 23 | return Relay.QL`mutation{removeCompletedTodos}`; 24 | } 25 | getFatQuery() { 26 | return Relay.QL` 27 | fragment on RemoveCompletedTodosPayload { 28 | deletedTodoIds, 29 | viewer { 30 | todos { 31 | completedCount, 32 | totalCount, 33 | }, 34 | }, 35 | } 36 | `; 37 | } 38 | getConfigs() { 39 | return [{ 40 | type: 'NODE_DELETE', 41 | parentName: 'viewer', 42 | parentID: this.props.viewer.id, 43 | connectionName: 'todos', 44 | deletedIDFieldName: 'deletedTodoIds', 45 | }]; 46 | } 47 | getVariables() { 48 | return {}; 49 | } 50 | getOptimisticResponse() { 51 | var deletedTodoIds; 52 | var newTotalCount; 53 | if (this.props.todos) { 54 | var {completedCount, totalCount} = this.props.todos; 55 | if (completedCount != null && totalCount != null) { 56 | newTotalCount = totalCount - completedCount; 57 | } 58 | if (this.props.todos.edges) { 59 | deletedTodoIds = this.props.todos.edges 60 | .filter(edge => edge.node.complete) 61 | .map(edge => edge.node.id); 62 | } 63 | } 64 | return { 65 | deletedTodoIds, 66 | viewer: { 67 | id: this.props.viewer.id, 68 | todos: { 69 | completedCount: 0, 70 | totalCount: newTotalCount, 71 | }, 72 | }, 73 | }; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /app/js/mutations/RemoveTodoMutation.js: -------------------------------------------------------------------------------- 1 | export default class RemoveTodoMutation extends Relay.Mutation { 2 | static fragments = { 3 | // TODO: Mark complete as optional 4 | todo: () => Relay.QL` 5 | fragment on Todo { 6 | complete, 7 | id, 8 | } 9 | `, 10 | // TODO: Mark completedCount and totalCount as optional 11 | viewer: () => Relay.QL` 12 | fragment on User { 13 | id, 14 | todos { 15 | completedCount, 16 | totalCount, 17 | }, 18 | } 19 | `, 20 | }; 21 | getMutation() { 22 | return Relay.QL`mutation{removeTodo}`; 23 | } 24 | getFatQuery() { 25 | return Relay.QL` 26 | fragment on RemoveTodoPayload { 27 | deletedTodoId, 28 | viewer { 29 | todos { 30 | completedCount, 31 | totalCount, 32 | }, 33 | }, 34 | } 35 | `; 36 | } 37 | getConfigs() { 38 | return [{ 39 | type: 'NODE_DELETE', 40 | parentName: 'viewer', 41 | parentID: this.props.viewer.id, 42 | connectionName: 'todos', 43 | deletedIDFieldName: 'deletedTodoId', 44 | }]; 45 | } 46 | getVariables() { 47 | return { 48 | id: this.props.todo.id, 49 | }; 50 | } 51 | getOptimisticResponse() { 52 | var viewerPayload; 53 | if (this.props.viewer.todos) { 54 | viewerPayload = {id: this.props.viewer.id, todos: {}}; 55 | if (this.props.viewer.todos.completedCount != null) { 56 | viewerPayload.todos.completedCount = this.props.todo.complete === true 57 | ? this.props.viewer.todos.completedCount - 1 58 | : this.props.viewer.todos.completedCount; 59 | } 60 | if (this.props.viewer.todos.totalCount != null) { 61 | viewerPayload.todos.totalCount = this.props.viewer.todos.totalCount - 1; 62 | } 63 | } 64 | return { 65 | deletedTodoId: this.props.todo.id, 66 | viewer: viewerPayload, 67 | }; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/js/mutations/RenameTodoMutation.js: -------------------------------------------------------------------------------- 1 | export default class RenameTodoMutation extends Relay.Mutation { 2 | static fragments = { 3 | todo: () => Relay.QL` 4 | fragment on Todo { 5 | id, 6 | } 7 | `, 8 | }; 9 | getMutation() { 10 | return Relay.QL`mutation{renameTodo}`; 11 | } 12 | getFatQuery() { 13 | return Relay.QL` 14 | fragment on RenameTodoPayload { 15 | todo { 16 | text, 17 | } 18 | } 19 | `; 20 | } 21 | getConfigs() { 22 | return [{ 23 | type: 'FIELDS_CHANGE', 24 | fieldIDs: { 25 | todo: this.props.todo.id, 26 | }, 27 | }]; 28 | } 29 | getVariables() { 30 | return { 31 | id: this.props.todo.id, 32 | text: this.props.text, 33 | }; 34 | } 35 | getOptimisticResponse() { 36 | return { 37 | todo: { 38 | id: this.props.todo.id, 39 | text: this.props.text, 40 | }, 41 | }; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/js/routes/TodoAppHomeRoute.js: -------------------------------------------------------------------------------- 1 | export default class extends Relay.Route { 2 | static path = '/'; 3 | static queries = { 4 | viewer: (Component) => Relay.QL` 5 | query RootQuery { 6 | viewer { 7 | ${Component.getFragment('viewer')}, 8 | }, 9 | } 10 | `, 11 | }; 12 | static routeName = 'TodosHomeRoute'; 13 | } 14 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "start": "babel-node ./server.js" 5 | }, 6 | "dependencies": { 7 | "babel": "5.8.21", 8 | "babel-eslint": "^4.0.5", 9 | "babel-loader": "5.3.2", 10 | "babel-relay-plugin": "0.1.2", 11 | "classnames": "^2.1.3", 12 | "eslint": "^1.0.0", 13 | "eslint-loader": "^1.0.0", 14 | "eslint-plugin-react": "^3.2.0", 15 | "express": "^4.13.1", 16 | "react": "^0.14.0-beta3", 17 | "react-relay": "0.1.1", 18 | "todomvc-app-css": "^2.0.1", 19 | "todomvc-common": "^1.0.2", 20 | "webpack": "^1.10.5", 21 | "webpack-dev-server": "^1.10.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Relay • TodoMVC 7 | 8 | 9 | 10 | 11 |
    12 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/public/learn.json: -------------------------------------------------------------------------------- 1 | { 2 | "framework": { 3 | "name": "Relay", 4 | "description": "A JavaScript framework for building data-driven React applications", 5 | "homepage": "facebook.github.io/relay/", 6 | "source_path": [{ 7 | "name": "Relay + express-graphql Example", 8 | "url": "examples/relay-express-graphql" 9 | }], 10 | "link_groups": [{ 11 | "heading": "Official Resources", 12 | "links": [{ 13 | "name": "Documentation", 14 | "url": "https://facebook.github.io/relay/docs/getting-started.html" 15 | }, { 16 | "name": "API Reference", 17 | "url": "https://facebook.github.io/relay/docs/api-reference-relay.html" 18 | }, { 19 | "name": "Relay on GitHub", 20 | "url": "https://github.com/facebook/relay" 21 | }] 22 | }, { 23 | "heading": "Community", 24 | "links": [{ 25 | "name": "Relay on StackOverflow", 26 | "url": "https://stackoverflow.com/questions/tagged/react-relay" 27 | }] 28 | }] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/server.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import path from 'path'; 3 | import webpack from 'webpack'; 4 | import WebpackDevServer from 'webpack-dev-server'; 5 | 6 | const APP_PORT = 3000; 7 | const GRAPHQL_PORT = 8080; 8 | 9 | // Serve the Relay app 10 | var compiler = webpack({ 11 | entry: path.resolve(__dirname, 'js', 'app.js'), 12 | eslint: { 13 | configFile: '.eslintrc' 14 | }, 15 | module: { 16 | loaders: [ 17 | { 18 | test: /\.js$/, 19 | loader: 'babel', 20 | query: { 21 | stage: 0, 22 | plugins: ['./build/babelRelayPlugin'] 23 | } 24 | }, 25 | { 26 | test: /\.js$/, 27 | loader: 'eslint' 28 | } 29 | ] 30 | }, 31 | output: {filename: 'app.js', path: '/'} 32 | }); 33 | var app = new WebpackDevServer(compiler, { 34 | contentBase: '/public/', 35 | proxy: {'/graphql': `http://localhost:${GRAPHQL_PORT}`}, 36 | publicPath: '/js/', 37 | stats: {colors: true} 38 | }); 39 | // Serve static resources 40 | app.use('/', express.static('public')); 41 | app.use('/node_modules', express.static('node_modules')); 42 | app.listen(APP_PORT, () => { 43 | console.log(`Relay TodoMVC is now running on http://localhost:${APP_PORT}`); 44 | }); 45 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | group 'com.andimarek' 2 | version '1.0-SNAPSHOT' 3 | 4 | apply plugin: 'java' 5 | 6 | sourceCompatibility = 1.8 7 | 8 | repositories { 9 | mavenCentral() 10 | maven { url "http://dl.bintray.com/andimarek/graphql-java" } 11 | } 12 | 13 | task(start, dependsOn: 'build', type: JavaExec) { 14 | main = 'todomvc.Main' 15 | classpath = sourceSets.main.runtimeClasspath 16 | } 17 | 18 | dependencies { 19 | compile 'com.graphql-java:graphql-java:2015-08-16T21-37-10' 20 | compile("org.springframework.boot:spring-boot-starter-web:1.2.5.RELEASE") 21 | testCompile group: 'junit', name: 'junit', version: '4.11' 22 | } 23 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-java/todomvc-relay-java/5c37d571a77bf88e02598f916515374fdceb5b45/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Aug 15 02:36:07 CEST 2015 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.6-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'todomvc-relay-java' 2 | 3 | -------------------------------------------------------------------------------- /src/main/java/todomvc/GraphQLController.java: -------------------------------------------------------------------------------- 1 | package todomvc; 2 | 3 | 4 | import graphql.ExecutionResult; 5 | import graphql.GraphQL; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 9 | import org.springframework.http.MediaType; 10 | import org.springframework.stereotype.Controller; 11 | import org.springframework.web.bind.annotation.RequestBody; 12 | import org.springframework.web.bind.annotation.RequestMapping; 13 | import org.springframework.web.bind.annotation.RequestMethod; 14 | import org.springframework.web.bind.annotation.ResponseBody; 15 | 16 | import java.util.LinkedHashMap; 17 | import java.util.Map; 18 | 19 | @Controller 20 | @EnableAutoConfiguration 21 | public class GraphQLController { 22 | 23 | 24 | TodoSchema todoSchema = new TodoSchema(); 25 | GraphQL graphql = new GraphQL(todoSchema.getSchema()); 26 | 27 | private static final Logger log = LoggerFactory.getLogger(GraphQLController.class); 28 | 29 | @RequestMapping(value = "/graphql", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) 30 | @ResponseBody 31 | public Object executeOperation(@RequestBody Map body) { 32 | String query = (String) body.get("query"); 33 | Map variables = (Map) body.get("variables"); 34 | ExecutionResult executionResult = graphql.execute(query, (Object) null, variables); 35 | Map result = new LinkedHashMap<>(); 36 | if (executionResult.getErrors().size() > 0) { 37 | result.put("errors", executionResult.getErrors()); 38 | log.error("Errors: {}", executionResult.getErrors()); 39 | } 40 | result.put("data", executionResult.getData()); 41 | return result; 42 | } 43 | 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/todomvc/Main.java: -------------------------------------------------------------------------------- 1 | package todomvc; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | 5 | public class Main { 6 | 7 | public static void main(String[] args) throws Exception { 8 | SpringApplication.run(GraphQLController.class, args); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/todomvc/Todo.java: -------------------------------------------------------------------------------- 1 | package todomvc; 2 | 3 | 4 | public class Todo { 5 | 6 | private String id; 7 | 8 | private String text; 9 | 10 | private boolean complete; 11 | 12 | public String getId() { 13 | return id; 14 | } 15 | 16 | public void setId(String id) { 17 | this.id = id; 18 | } 19 | 20 | public String getText() { 21 | return text; 22 | } 23 | 24 | public void setText(String text) { 25 | this.text = text; 26 | } 27 | 28 | public boolean isComplete() { 29 | return complete; 30 | } 31 | 32 | public void setComplete(boolean complete) { 33 | this.complete = complete; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/todomvc/TodoSchema.java: -------------------------------------------------------------------------------- 1 | package todomvc; 2 | 3 | 4 | import graphql.Scalars; 5 | import graphql.relay.Connection; 6 | import graphql.relay.Relay; 7 | import graphql.relay.SimpleListConnection; 8 | import graphql.schema.*; 9 | 10 | import java.util.ArrayList; 11 | import java.util.Arrays; 12 | import java.util.Collections; 13 | import java.util.List; 14 | import java.util.stream.Collectors; 15 | 16 | import static graphql.Scalars.GraphQLID; 17 | import static graphql.Scalars.GraphQLInt; 18 | import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition; 19 | import static graphql.schema.GraphQLObjectType.newObject; 20 | import static graphql.schema.GraphQLSchema.newSchema; 21 | 22 | public class TodoSchema { 23 | 24 | 25 | private GraphQLSchema schema; 26 | 27 | private GraphQLObjectType todoType; 28 | 29 | 30 | private GraphQLObjectType userType; 31 | 32 | private GraphQLObjectType connectionFromUserToTodos; 33 | private GraphQLInterfaceType nodeInterface; 34 | 35 | 36 | private GraphQLObjectType todosEdge; 37 | 38 | 39 | private User theOnlyUser = new User(); 40 | private List todos = new ArrayList<>(); 41 | 42 | 43 | private SimpleListConnection simpleConnection; 44 | 45 | 46 | private Relay relay = new Relay(); 47 | 48 | int nextTodoId = 0; 49 | 50 | 51 | public TodoSchema() { 52 | addTodo("Do Something"); 53 | addTodo("Other todo"); 54 | createSchema(); 55 | } 56 | 57 | 58 | private void createSchema() { 59 | TypeResolverProxy typeResolverProxy = new TypeResolverProxy(); 60 | nodeInterface = relay.nodeInterface(typeResolverProxy); 61 | simpleConnection = new SimpleListConnection(todos); 62 | 63 | createTodoType(); 64 | createConnectionFromUserToTodos(); 65 | createUserType(); 66 | 67 | 68 | typeResolverProxy.setTypeResolver(object -> { 69 | if (object instanceof User) { 70 | return userType; 71 | } else if (object instanceof Todo) { 72 | return todoType; 73 | } 74 | return null; 75 | }); 76 | 77 | 78 | DataFetcher todoDataFetcher = environment -> { 79 | String id = environment.getArgument("id"); 80 | return new User(); 81 | }; 82 | 83 | GraphQLObjectType QueryRoot = newObject() 84 | .name("Root") 85 | .field(newFieldDefinition() 86 | .name("viewer") 87 | .type(userType) 88 | .staticValue(theOnlyUser) 89 | .build()) 90 | .field(relay.nodeField(nodeInterface, todoDataFetcher)) 91 | .build(); 92 | 93 | TodoSchemaMutations todoSchemaMutations = new TodoSchemaMutations(this); 94 | 95 | GraphQLObjectType mutationType = newObject() 96 | .name("Mutation") 97 | .fields(todoSchemaMutations.getFields()) 98 | .build(); 99 | 100 | schema = newSchema() 101 | .query(QueryRoot) 102 | .mutation(mutationType) 103 | .build(); 104 | } 105 | 106 | private void createUserType() { 107 | userType = newObject() 108 | .name("User") 109 | .field(newFieldDefinition() 110 | .name("id") 111 | .type(new GraphQLNonNull(GraphQLID)) 112 | .dataFetcher(environment -> { 113 | User user = (User) environment.getSource(); 114 | return relay.toGlobalId("User", user.getId()); 115 | } 116 | ) 117 | .build()) 118 | .field(newFieldDefinition() 119 | .name("todos") 120 | .type(connectionFromUserToTodos) 121 | .argument(relay.getConnectionFieldArguments()) 122 | .dataFetcher(simpleConnection) 123 | .build()) 124 | .withInterface(nodeInterface) 125 | .build(); 126 | } 127 | 128 | private void createTodoType() { 129 | todoType = newObject() 130 | .name("Todo") 131 | .field(newFieldDefinition() 132 | .name("id") 133 | .type(new GraphQLNonNull(GraphQLID)) 134 | .dataFetcher(environment -> { 135 | Todo todo = (Todo) environment.getSource(); 136 | return relay.toGlobalId("Todo", todo.getId()); 137 | } 138 | ) 139 | .build()) 140 | .field(newFieldDefinition() 141 | .name("text") 142 | .type(Scalars.GraphQLString) 143 | .build()) 144 | .field(newFieldDefinition() 145 | .name("complete") 146 | .type(Scalars.GraphQLBoolean) 147 | .build()) 148 | .withInterface(nodeInterface) 149 | .build(); 150 | } 151 | 152 | private void createConnectionFromUserToTodos() { 153 | todosEdge = relay.edgeType("Todo", todoType, nodeInterface, Collections.emptyList()); 154 | GraphQLFieldDefinition totalCount = newFieldDefinition() 155 | .name("totalCount") 156 | .type(GraphQLInt) 157 | .dataFetcher(environment -> { 158 | Connection connection = (Connection) environment.getSource(); 159 | return connection.getEdges().size(); 160 | }) 161 | .build(); 162 | 163 | GraphQLFieldDefinition completedCount = newFieldDefinition() 164 | .name("completedCount") 165 | .type(GraphQLInt) 166 | .dataFetcher(environment -> { 167 | Connection connection = (Connection) environment.getSource(); 168 | return (int) connection.getEdges().stream().filter(edge -> ((Todo) edge.getNode()).isComplete()).count(); 169 | }) 170 | .build(); 171 | connectionFromUserToTodos = relay.connectionType("Todo", todosEdge, Arrays.asList(totalCount, completedCount)); 172 | } 173 | 174 | public User getTheOnlyUser() { 175 | return theOnlyUser; 176 | } 177 | 178 | 179 | public SimpleListConnection getSimpleConnection() { 180 | return simpleConnection; 181 | } 182 | 183 | 184 | public GraphQLObjectType getUserType() { 185 | return userType; 186 | } 187 | 188 | public GraphQLObjectType getTodoType() { 189 | return todoType; 190 | } 191 | 192 | public GraphQLObjectType getTodosEdge() { 193 | return todosEdge; 194 | } 195 | 196 | 197 | public Relay getRelay() { 198 | return relay; 199 | } 200 | 201 | 202 | public String addTodo(String text) { 203 | Todo newTodo = new Todo(); 204 | newTodo.setId(Integer.toString(nextTodoId++)); 205 | newTodo.setText(text); 206 | todos.add(newTodo); 207 | return newTodo.getId(); 208 | } 209 | 210 | 211 | public void removeTodo(String id) { 212 | Todo del = todos.stream().filter(todo -> todo.getId().equals(id)).findFirst().get(); 213 | todos.remove(del); 214 | } 215 | 216 | public void renameTodo(String id, String text) { 217 | Todo matchedTodo = todos.stream().filter(todo -> todo.getId().equals(id)).findFirst().get(); 218 | matchedTodo.setText(text); 219 | } 220 | 221 | 222 | public List removeCompletedTodos() { 223 | List toDelete = todos.stream().filter(Todo::isComplete).map(Todo::getId).collect(Collectors.toList()); 224 | todos.removeIf(todo -> toDelete.contains(todo.getId())); 225 | return toDelete; 226 | } 227 | 228 | public List markAllTodos(boolean complete) { 229 | List changed = new ArrayList<>(); 230 | todos.stream().filter(todo -> complete != todo.isComplete()).forEach(todo -> { 231 | changed.add(todo.getId()); 232 | todo.setComplete(complete); 233 | }); 234 | 235 | return changed; 236 | } 237 | 238 | public void changeTodoStatus(String id, boolean complete) { 239 | Todo todo = getTodo(id); 240 | todo.setComplete(complete); 241 | } 242 | 243 | 244 | public Todo getTodo(String id) { 245 | return todos.stream().filter(todo -> todo.getId().equals(id)).findFirst().get(); 246 | } 247 | 248 | public List getTodos(List ids) { 249 | return todos.stream().filter(todo -> ids.contains(todo.getId())).collect(Collectors.toList()); 250 | } 251 | 252 | 253 | public GraphQLSchema getSchema() { 254 | return schema; 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /src/main/java/todomvc/TodoSchemaMutations.java: -------------------------------------------------------------------------------- 1 | package todomvc; 2 | 3 | 4 | import graphql.relay.Edge; 5 | import graphql.schema.*; 6 | 7 | import java.util.Arrays; 8 | import java.util.LinkedHashMap; 9 | import java.util.List; 10 | import java.util.Map; 11 | import java.util.stream.Collectors; 12 | 13 | import static graphql.Scalars.GraphQLBoolean; 14 | import static graphql.Scalars.GraphQLID; 15 | import static graphql.Scalars.GraphQLString; 16 | import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition; 17 | import static graphql.schema.GraphQLInputObjectField.newInputObjectField; 18 | 19 | public class TodoSchemaMutations { 20 | 21 | 22 | private GraphQLFieldDefinition addTodo; 23 | private GraphQLFieldDefinition changeStatus; 24 | private GraphQLFieldDefinition markAll; 25 | private GraphQLFieldDefinition removeCompleted; 26 | private GraphQLFieldDefinition removeTodo; 27 | private GraphQLFieldDefinition renameTodo; 28 | 29 | private TodoSchema todoSchema; 30 | 31 | 32 | public TodoSchemaMutations(TodoSchema todoSchema) { 33 | this.todoSchema = todoSchema; 34 | 35 | createAddTodoMutation(); 36 | createChangeTodoStatusMutation(); 37 | createMarkAllTodosMutation(); 38 | createRemoveCompletedTodosMutation(); 39 | createRemoveTodoMutation(); 40 | createRenameTodoMutation(); 41 | } 42 | 43 | private GraphQLFieldDefinition getViewerField() { 44 | GraphQLFieldDefinition viewer = newFieldDefinition() 45 | .name("viewer") 46 | .type(todoSchema.getUserType()) 47 | .staticValue(todoSchema.getTheOnlyUser()) 48 | .build(); 49 | return viewer; 50 | } 51 | 52 | public List getFields() { 53 | return Arrays.asList(addTodo, changeStatus, markAll, removeCompleted, removeTodo, renameTodo); 54 | } 55 | 56 | 57 | private void createAddTodoMutation() { 58 | GraphQLInputObjectField textField = newInputObjectField() 59 | .name("text") 60 | .type(new GraphQLNonNull(GraphQLString)) 61 | .build(); 62 | 63 | List inputFields = Arrays.asList(textField); 64 | 65 | GraphQLFieldDefinition todoEdge = newFieldDefinition() 66 | .name("todoEdge") 67 | .type(todoSchema.getTodosEdge()) 68 | .dataFetcher(environment -> { 69 | Map source = (Map) environment.getSource(); 70 | String todoId = (String) source.get("todoId"); 71 | Todo todo = todoSchema.getTodo(todoId); 72 | return new Edge(todo, todoSchema.getSimpleConnection().cursorForObjectInConnection(todo)); 73 | }) 74 | .build(); 75 | 76 | 77 | List outputFields = Arrays.asList(todoEdge, getViewerField()); 78 | 79 | DataFetcher mutate = environment -> { 80 | Map input = environment.getArgument("input"); 81 | String text = (String) input.get("text"); 82 | String newId = todoSchema.addTodo(text); 83 | Map result = new LinkedHashMap<>(); 84 | result.put("clientMutationId", (String) input.get("clientMutationId")); 85 | result.put("todoId", newId); 86 | return result; 87 | }; 88 | 89 | 90 | addTodo = todoSchema.getRelay().mutationWithClientMutationId("AddTodo", "addTodo", inputFields, outputFields, mutate); 91 | } 92 | 93 | private void createChangeTodoStatusMutation() { 94 | GraphQLInputObjectField completeField = newInputObjectField() 95 | .name("complete") 96 | .type(new GraphQLNonNull(GraphQLBoolean)) 97 | .build(); 98 | GraphQLInputObjectField idField = newInputObjectField() 99 | .name("id") 100 | .type(new GraphQLNonNull(GraphQLID)) 101 | .build(); 102 | 103 | List inputFields = Arrays.asList(completeField, idField); 104 | 105 | GraphQLFieldDefinition todoField = newFieldDefinition() 106 | .name("todo") 107 | .type(todoSchema.getTodoType()) 108 | .dataFetcher(environment -> { 109 | Map source = (Map) environment.getSource(); 110 | String todoId = (String) source.get("todoId"); 111 | Todo todo = todoSchema.getTodo(todoId); 112 | return todo; 113 | }) 114 | .build(); 115 | 116 | List outputFields = Arrays.asList(todoField, getViewerField()); 117 | 118 | DataFetcher mutate = environment -> { 119 | Map input = environment.getArgument("input"); 120 | String id = (String) input.get("id"); 121 | boolean complete = (boolean) input.get("complete"); 122 | String localId = todoSchema.getRelay().fromGlobalId(id).id; 123 | todoSchema.changeTodoStatus(localId, complete); 124 | Map result = new LinkedHashMap<>(); 125 | result.put("clientMutationId", (String) input.get("clientMutationId")); 126 | result.put("todoId", localId); 127 | return result; 128 | }; 129 | 130 | 131 | changeStatus = todoSchema.getRelay().mutationWithClientMutationId("ChangeTodoStatus", "changeTodoStatus", inputFields, outputFields, mutate); 132 | } 133 | 134 | private void createMarkAllTodosMutation() { 135 | GraphQLInputObjectField completeField = newInputObjectField() 136 | .name("complete") 137 | .type(new GraphQLNonNull(GraphQLBoolean)) 138 | .build(); 139 | List inputFields = Arrays.asList(completeField); 140 | 141 | GraphQLFieldDefinition changedTodos = newFieldDefinition() 142 | .name("changedTodos") 143 | .type(new GraphQLList(todoSchema.getTodoType())) 144 | .dataFetcher(environment -> { 145 | Map source = (Map) environment.getSource(); 146 | return todoSchema.getTodos((List) source.get("todIds")); 147 | }) 148 | .build(); 149 | 150 | 151 | List outputFields = Arrays.asList(changedTodos, getViewerField()); 152 | 153 | DataFetcher mutate = environment -> { 154 | Map input = environment.getArgument("input"); 155 | boolean complete = (boolean) input.get("complete"); 156 | List ids = todoSchema.markAllTodos(complete); 157 | Map result = new LinkedHashMap<>(); 158 | result.put("clientMutationId", (String) input.get("clientMutationId")); 159 | result.put("todIds", ids); 160 | return result; 161 | }; 162 | 163 | 164 | markAll = todoSchema.getRelay().mutationWithClientMutationId("MarkAllTodos", "markAllTodos", inputFields, outputFields, mutate); 165 | } 166 | 167 | private void createRemoveCompletedTodosMutation() { 168 | List inputFields = Arrays.asList(); 169 | 170 | GraphQLFieldDefinition todoEdge = newFieldDefinition() 171 | .name("deletedTodoIds") 172 | .type(new GraphQLList(GraphQLString)) 173 | .build(); 174 | 175 | List outputFields = Arrays.asList(todoEdge, getViewerField()); 176 | 177 | DataFetcher mutate = environment -> { 178 | Map input = environment.getArgument("input"); 179 | List deletedTodoIds = todoSchema.removeCompletedTodos(); 180 | 181 | Map result = new LinkedHashMap<>(); 182 | result.put("clientMutationId", (String) input.get("clientMutationId")); 183 | result.put("deletedTodoIds", deletedTodoIds.stream().map(s -> todoSchema.getRelay().toGlobalId("Todo",s)).collect(Collectors.toList())); 184 | return result; 185 | }; 186 | 187 | 188 | removeCompleted = todoSchema.getRelay().mutationWithClientMutationId("RemoveCompletedTodos", "removeCompletedTodos", inputFields, outputFields, mutate); 189 | } 190 | 191 | private void createRemoveTodoMutation() { 192 | GraphQLInputObjectField idField = newInputObjectField() 193 | .name("id") 194 | .type(new GraphQLNonNull(GraphQLID)) 195 | .build(); 196 | 197 | List inputFields = Arrays.asList(idField); 198 | 199 | GraphQLFieldDefinition todoEdge = newFieldDefinition() 200 | .name("deletedTodoId") 201 | .type(GraphQLID) 202 | .build(); 203 | 204 | List outputFields = Arrays.asList(todoEdge, getViewerField()); 205 | 206 | DataFetcher mutate = environment -> { 207 | Map input = environment.getArgument("input"); 208 | String id = (String) input.get("id"); 209 | String localId = todoSchema.getRelay().fromGlobalId(id).id; 210 | todoSchema.removeTodo(localId); 211 | Map result = new LinkedHashMap<>(); 212 | result.put("clientMutationId", (String) input.get("clientMutationId")); 213 | result.put("deletedTodoId", id); 214 | return result; 215 | }; 216 | 217 | 218 | removeTodo = todoSchema.getRelay().mutationWithClientMutationId("RemoveTodo", "removeTodo", inputFields, outputFields, mutate); 219 | } 220 | 221 | private void createRenameTodoMutation() { 222 | GraphQLInputObjectField idField = newInputObjectField() 223 | .name("id") 224 | .type(new GraphQLNonNull(GraphQLID)) 225 | .build(); 226 | GraphQLInputObjectField textField = newInputObjectField() 227 | .name("text") 228 | .type(new GraphQLNonNull(GraphQLString)) 229 | .build(); 230 | 231 | List inputFields = Arrays.asList(idField, textField); 232 | 233 | GraphQLFieldDefinition todoField = newFieldDefinition() 234 | .name("todo") 235 | .type(todoSchema.getTodoType()) 236 | .dataFetcher(environment -> { 237 | Map source = (Map) environment.getSource(); 238 | String todoId = (String) source.get("localId"); 239 | Todo todo = todoSchema.getTodo(todoId); 240 | return todo; 241 | }) 242 | .build(); 243 | 244 | List outputFields = Arrays.asList(todoField, getViewerField()); 245 | 246 | DataFetcher mutate = environment -> { 247 | Map input = environment.getArgument("input"); 248 | String id = (String) input.get("id"); 249 | String text = (String) input.get("text"); 250 | String localId = todoSchema.getRelay().fromGlobalId(id).id; 251 | todoSchema.renameTodo(localId, text); 252 | Map result = new LinkedHashMap<>(); 253 | result.put("clientMutationId", (String) input.get("clientMutationId")); 254 | result.put("localId", localId); 255 | return result; 256 | }; 257 | 258 | 259 | renameTodo = todoSchema.getRelay().mutationWithClientMutationId("RenameTodo", "renameTodo", inputFields, outputFields, mutate); 260 | } 261 | 262 | } 263 | -------------------------------------------------------------------------------- /src/main/java/todomvc/User.java: -------------------------------------------------------------------------------- 1 | package todomvc; 2 | 3 | 4 | public class User { 5 | 6 | private String id = "someId"; 7 | 8 | public String getId() { 9 | return id; 10 | } 11 | 12 | public void setId(String id) { 13 | this.id = id; 14 | } 15 | } 16 | --------------------------------------------------------------------------------