├── src ├── client │ ├── TodoApp │ │ ├── relay │ │ │ ├── handlerProvider.js │ │ │ ├── store.js │ │ │ ├── constants.js │ │ │ ├── environment.js │ │ │ └── network.js │ │ ├── containers │ │ │ ├── TodoListHeader.js │ │ │ ├── TodoListCard.js │ │ │ ├── TodoListSelect.js │ │ │ ├── __generated__ │ │ │ │ ├── AddTodoItemInput_todoList.graphql.js │ │ │ │ ├── TodoListHeader_todoList.graphql.js │ │ │ │ ├── TodoListCard_todoList.graphql.js │ │ │ │ ├── TodoListItemsWithFilter_todoList.graphql.js │ │ │ │ ├── TodoListFooter_todoList.graphql.js │ │ │ │ ├── TodoItem_todoItem.graphql.js │ │ │ │ ├── TodoListSelect_user.graphql.js │ │ │ │ └── TodoListItems_todoList.graphql.js │ │ │ ├── TodoListFooter.js │ │ │ ├── AddTodoItemInput.js │ │ │ ├── TodoListItemsWithFilter.js │ │ │ ├── TodoItem.js │ │ │ └── TodoListItems.js │ │ ├── registrations │ │ │ └── todoListItemsConnectionNames.js │ │ ├── components │ │ │ ├── TodoListCard.js │ │ │ ├── TodoListHeader.js │ │ │ ├── TodoListSelect.js │ │ │ ├── AddTodoItemInput.js │ │ │ ├── TodoListFooter.js │ │ │ ├── TodoListItemsWithFilter.js │ │ │ ├── TodoListItems.js │ │ │ └── TodoItem.js │ │ ├── TodoListSelect.js │ │ ├── updaters │ │ │ ├── todoItemsUpdatedUpdater.js │ │ │ ├── todoItemDeletedUpdater.js │ │ │ ├── todoItemsDeletedUpdater.js │ │ │ ├── todoItemUpdatedUpdater.js │ │ │ └── todoItemCreatedUpdater.js │ │ ├── index.js │ │ ├── TodoListCard.js │ │ ├── subscriptions │ │ │ ├── ItemsOnTodoListDeletedSubscription.js │ │ │ ├── ItemOnTodoListDeletedSubscription.js │ │ │ ├── ItemOnTodoListUpdatedSubscription.js │ │ │ ├── ItemsOnTodoListUpdatedSubscription.js │ │ │ ├── _Subscription.js │ │ │ ├── ItemOnTodoListCreatedSubscription.js │ │ │ └── __generated__ │ │ │ │ ├── ItemsOnTodoListDeletedSubscription.graphql.js │ │ │ │ ├── ItemOnTodoListDeletedSubscription.graphql.js │ │ │ │ ├── ItemsOnTodoListUpdatedSubscription.graphql.js │ │ │ │ └── ItemOnTodoListUpdatedSubscription.graphql.js │ │ ├── mutations │ │ │ ├── DeleteTodoItemMutation.js │ │ │ ├── UpdateTodoItemMutation.js │ │ │ ├── CreateTodoItemMutation.js │ │ │ ├── DeleteCompletedItemsOnTodoListMutation.js │ │ │ ├── UpdateAllItemsOnTodoListMutation.js │ │ │ ├── __generated__ │ │ │ │ ├── UpdateAllItemsOnTodoListMutation.graphql.js │ │ │ │ ├── DeleteCompletedItemsOnTodoListMutation.graphql.js │ │ │ │ ├── DeleteTodoItemMutation.graphql.js │ │ │ │ ├── UpdateTodoItemMutation.graphql.js │ │ │ │ └── CreateTodoItemMutation.graphql.js │ │ │ └── _Mutation.js │ │ └── __generated__ │ │ │ └── TodoListSelectQuery.graphql.js │ ├── static │ │ └── index.html │ └── webpack-entry.js ├── server │ ├── subscriptions │ │ ├── pubsub │ │ │ ├── index.js │ │ │ └── event-types.js │ │ ├── _getSubscriptionFieldsFromMutation.js │ │ ├── subscriptionType.js │ │ ├── itemOnTodoListCreatedSubscription.js │ │ ├── itemOnTodoListDeletedSubscription.js │ │ ├── itemOnTodoListUpdatedSubscription.js │ │ ├── itemsOnTodoListDeletedSubscription.js │ │ └── itemsOnTodoListUpdatedSubscription.js │ ├── schema.js │ ├── types │ │ ├── queryType.js │ │ ├── _getType.js │ │ ├── todoItemType.js │ │ ├── userType.js │ │ └── todoListType.js │ ├── mutations │ │ ├── mutationType.js │ │ ├── deleteTodoItemMutation.js │ │ ├── updateTodoItemMutation.js │ │ ├── deleteCompletedItemsOnTodoListMutation.js │ │ ├── createTodoItemMutation.js │ │ └── updateAllItemsOnTodoListMutation.js │ ├── relay.js │ └── index.js └── vendor │ └── express-graphql │ ├── parseBody.js │ └── renderGraphiQL.js ├── app.json ├── .flowconfig ├── scripts ├── relay-dev-compiler.js └── update-schema.js ├── .babelrc ├── webpack.config.js ├── README.md ├── .gitignore ├── package.json └── schema.graphql /src/client/TodoApp/relay/handlerProvider.js: -------------------------------------------------------------------------------- 1 | const handlerProvider = null 2 | export default handlerProvider 3 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-todomvc", 3 | "buildpacks": [ 4 | { 5 | "url": "heroku/nodejs" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /src/server/subscriptions/pubsub/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { PubSub } from 'graphql-subscriptions' 4 | 5 | const pubsub = new PubSub() 6 | export default pubsub 7 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | 7 | [lints] 8 | 9 | [options] 10 | module.ignore_non_literal_requires=true 11 | unsafe.enable_getters_and_setters=true 12 | 13 | [strict] 14 | -------------------------------------------------------------------------------- /scripts/relay-dev-compiler.js: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process' 2 | import './update-schema' 3 | 4 | execSync('relay-compiler --src ./src/client --schema ./schema.graphql --watch', { stdio: [0, 1, 2] }) 5 | -------------------------------------------------------------------------------- /src/client/TodoApp/relay/store.js: -------------------------------------------------------------------------------- 1 | import { 2 | RecordSource, 3 | Store, 4 | } from 'relay-runtime' 5 | 6 | const source = new RecordSource() 7 | const store = new Store(source) 8 | 9 | export default store 10 | -------------------------------------------------------------------------------- /src/client/TodoApp/relay/constants.js: -------------------------------------------------------------------------------- 1 | export const GRAPHQL_ENDPOINT = `${window.location.protocol}//${window.location.host}/graphql` 2 | export const GRAPHQL_SUBSCRIPTION_ENDPOINT = 3 | `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/graphql/subscriptions` 4 | -------------------------------------------------------------------------------- /src/client/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | TodoMVC 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/server/subscriptions/pubsub/event-types.js: -------------------------------------------------------------------------------- 1 | export const TODO_ITEM_CREATED = 'TODO_ITEM_CREATED' 2 | export const TODO_ITEM_UPDATED = 'TODO_ITEM_UPDATED' 3 | export const TODO_ITEM_DELETED = 'TODO_ITEM_DELETED' 4 | export const TODO_ITEMS_UPDATED = 'TODO_ITEMS_UPDATED' 5 | export const TODO_ITEMS_DELETED = 'TODO_ITEMS_DELETED' 6 | -------------------------------------------------------------------------------- /src/client/webpack-entry.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | 4 | import TodoApp from './TodoApp' 5 | 6 | document.addEventListener('DOMContentLoaded', () => { 7 | if (!document.body) return 8 | 9 | ReactDOM.render( 10 | , 11 | document.body.appendChild(document.createElement('div')), 12 | ) 13 | }) 14 | -------------------------------------------------------------------------------- /src/client/TodoApp/relay/environment.js: -------------------------------------------------------------------------------- 1 | import { 2 | Environment, 3 | } from 'relay-runtime' 4 | 5 | import network from './network' 6 | import handlerProvider from './handlerProvider' 7 | import store from './store' 8 | 9 | const environment = new Environment({ 10 | handlerProvider, 11 | network, 12 | store, 13 | }) 14 | 15 | export default environment 16 | -------------------------------------------------------------------------------- /src/server/schema.js: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema } from 'graphql' 2 | 3 | import queryType from './types/queryType' 4 | import mutationType from './mutations/mutationType' 5 | import subscriptionType from './subscriptions/subscriptionType' 6 | 7 | const schema = new GraphQLSchema({ 8 | query: queryType, 9 | mutation: mutationType, 10 | subscription: subscriptionType, 11 | }) 12 | 13 | export default schema 14 | -------------------------------------------------------------------------------- /src/client/TodoApp/containers/TodoListHeader.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { graphql, createFragmentContainer } from 'react-relay' 4 | 5 | import TodoListHeaderComponent from '../components/TodoListHeader' 6 | 7 | export default createFragmentContainer( 8 | TodoListHeaderComponent, 9 | graphql` 10 | fragment TodoListHeader_todoList on TodoList { 11 | name 12 | ...AddTodoItemInput_todoList 13 | } 14 | `, 15 | ) 16 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "webpack": { 4 | "presets": [ 5 | ["env", { 6 | "targets": { 7 | "browsers": ["> 10%"] 8 | } 9 | }] 10 | ] 11 | } 12 | }, 13 | "presets": [ 14 | ["env", { 15 | "targets": { 16 | "node": "9.0.0" 17 | } 18 | }], 19 | "stage-1", 20 | "react", 21 | "flow" 22 | ], 23 | "plugins": [ 24 | "relay" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/server/types/queryType.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { GraphQLObjectType } from 'graphql' 4 | 5 | import { nodeField } from '../relay' 6 | import userType from './userType' 7 | 8 | const queryType = new GraphQLObjectType({ 9 | name: 'Query', 10 | fields: { 11 | node: nodeField, 12 | viewer: { 13 | type: userType, 14 | resolve(obj, args, context) { 15 | return context.viewer 16 | }, 17 | }, 18 | }, 19 | }) 20 | 21 | export default queryType 22 | -------------------------------------------------------------------------------- /src/client/TodoApp/containers/TodoListCard.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { graphql, createFragmentContainer } from 'react-relay' 4 | 5 | import TodoListCardComponent from '../components/TodoListCard' 6 | 7 | export default createFragmentContainer( 8 | TodoListCardComponent, 9 | graphql` 10 | fragment TodoListCard_todoList on TodoList { 11 | ...TodoListHeader_todoList 12 | ...TodoListItemsWithFilter_todoList 13 | ...TodoListFooter_todoList 14 | } 15 | `, 16 | ) 17 | -------------------------------------------------------------------------------- /scripts/update-schema.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import { graphql } from 'graphql' 4 | import { introspectionQuery, printSchema } from 'graphql/utilities' 5 | import schema from '../src/server/schema' 6 | 7 | // Save user readable type system shorthand of schema 8 | const schemaFilePath = path.join(__dirname, '..', 'schema.graphql') 9 | fs.writeFileSync( 10 | schemaFilePath, 11 | printSchema(schema), 12 | ) 13 | console.log(`GraphQL schema updated: ${schemaFilePath}`) 14 | -------------------------------------------------------------------------------- /src/server/types/_getType.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dynamic get a type to avoid circular dependency. 3 | * 4 | * @flow 5 | */ 6 | 7 | import type { GraphQLObjectType } from 'graphql' 8 | 9 | /** 10 | * Get a type by it's name. 11 | * 12 | * @example getType('userType') // returns the userType 13 | */ 14 | const getType = (name: string): GraphQLObjectType => { 15 | /* eslint-disable global-require, import/no-dynamic-require */ 16 | return require(`./${name}`).default 17 | } 18 | 19 | export default getType 20 | -------------------------------------------------------------------------------- /src/client/TodoApp/containers/TodoListSelect.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { graphql, createFragmentContainer } from 'react-relay' 4 | 5 | import TodoListSelectComponent from '../components/TodoListSelect' 6 | 7 | export default createFragmentContainer( 8 | TodoListSelectComponent, 9 | graphql` 10 | fragment TodoListSelect_user on User { 11 | todoLists { 12 | edges { 13 | node { 14 | id 15 | name 16 | } 17 | } 18 | } 19 | } 20 | `, 21 | ) 22 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: [ 3 | 'babel-polyfill', 4 | './src/client/webpack-entry.js', 5 | ], 6 | output: { 7 | filename: './dist/client/bundle.js', 8 | }, 9 | module: { 10 | loaders: [ 11 | { 12 | test: /\.js$/, 13 | exclude: /node_modules/, 14 | use: { 15 | loader: 'babel-loader', 16 | options: { 17 | forceEnv: 'webpack', 18 | }, 19 | }, 20 | }, 21 | ], 22 | }, 23 | plugins: [], 24 | } 25 | -------------------------------------------------------------------------------- /src/server/subscriptions/_getSubscriptionFieldsFromMutation.js: -------------------------------------------------------------------------------- 1 | const objToKeyValAry = obj => Object.keys(obj).map(k => [k, obj[k]]) 2 | const reduceKeyValAryToObjArgs = [(o, [k, v]) => ({ ...o, [k]: v }), {}] 3 | 4 | const getSubscriptionFieldsFromMutation = mutation => ( 5 | objToKeyValAry(mutation.type.getFields()) 6 | .filter(([k]) => k !== 'clientMutationId') 7 | .map(([k, { name, type, resolve }]) => [k, { name, type, resolve }]) 8 | .reduce(...reduceKeyValAryToObjArgs) 9 | ) 10 | 11 | export default getSubscriptionFieldsFromMutation 12 | -------------------------------------------------------------------------------- /src/client/TodoApp/registrations/todoListItemsConnectionNames.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | /** 4 | * Updaters can use this registration to iterate over each connection and 5 | * update them. 6 | */ 7 | const todoListItemsConnectionNames: Array = [] 8 | export default todoListItemsConnectionNames 9 | 10 | /** 11 | * Containers that queries the 'todoItems' connection on a 'TodoList' node 12 | * should register their key with this function. 13 | */ 14 | export const registerTodoListItemsConnectionName = 15 | (key: string): void => { todoListItemsConnectionNames.push(key) } 16 | -------------------------------------------------------------------------------- /src/client/TodoApp/components/TodoListCard.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | import TodoListHeader from '../containers/TodoListHeader' 4 | import TodoListItemsWithFilter from '../containers/TodoListItemsWithFilter' 5 | import TodoListFooter from '../containers/TodoListFooter' 6 | 7 | export default class TodoListCard extends Component { 8 | render() { 9 | return ( 10 |
11 | 12 | 13 | 14 |
15 | ) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/client/TodoApp/components/TodoListHeader.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import AddTodoItemInput from '../containers/AddTodoItemInput' 5 | 6 | export default class TodoListHeader extends Component { 7 | static propTypes = { 8 | todoList: PropTypes.shape({ 9 | name: PropTypes.string.isRequired, 10 | }).isRequired, 11 | } 12 | 13 | render() { 14 | const { todoList } = this.props 15 | 16 | return ( 17 |
18 |

{todoList.name}

19 | 20 |
21 | ) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GraphQL TodoMVC 2 | 3 | This project is a demonstration of building realtime data driven application with GraphQL. 4 | 5 | 6 | ## Install 7 | 8 | ```bash 9 | npm install 10 | ``` 11 | 12 | 13 | ## Build & Run 14 | 15 | Build the application: 16 | 17 | ```bash 18 | npm run build 19 | ``` 20 | 21 | Start server: 22 | 23 | ```bash 24 | npm start 25 | ``` 26 | 27 | 28 | ## Develop 29 | 30 | ```bash 31 | npm run dev 32 | ``` 33 | 34 | 35 | ## Deploy 36 | 37 | [![Deploy to now](https://deploy.now.sh/static/button.svg)](https://deploy.now.sh/?repo=https://github.com/zetavg/graphql-todomvc) 38 | [![Deploy to Heroku](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) 39 | -------------------------------------------------------------------------------- /src/client/TodoApp/containers/__generated__/AddTodoItemInput_todoList.graphql.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | */ 4 | 5 | /* eslint-disable */ 6 | 7 | 'use strict'; 8 | 9 | /*:: 10 | import type {ConcreteFragment} from 'relay-runtime'; 11 | export type AddTodoItemInput_todoList = {| 12 | +id: string; 13 | |}; 14 | */ 15 | 16 | 17 | const fragment /*: ConcreteFragment*/ = { 18 | "argumentDefinitions": [], 19 | "kind": "Fragment", 20 | "metadata": null, 21 | "name": "AddTodoItemInput_todoList", 22 | "selections": [ 23 | { 24 | "kind": "ScalarField", 25 | "alias": null, 26 | "args": null, 27 | "name": "id", 28 | "storageKey": null 29 | } 30 | ], 31 | "type": "TodoList" 32 | }; 33 | 34 | module.exports = fragment; 35 | -------------------------------------------------------------------------------- /src/client/TodoApp/TodoListSelect.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { QueryRenderer, graphql } from 'react-relay' 3 | import environment from './relay/environment' 4 | 5 | import TodoListSelectContainer from './containers/TodoListSelect' 6 | 7 | const TodoListSelect = componentProps => ( 8 | { 18 | if (error) { 19 | throw error 20 | } else if (props) { 21 | return 22 | } 23 | return
Loading
24 | }} 25 | /> 26 | ) 27 | 28 | export default TodoListSelect 29 | -------------------------------------------------------------------------------- /src/client/TodoApp/updaters/todoItemsUpdatedUpdater.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import type { RelayRecordSourceSelectorProxy } from 'relay-runtime' 4 | 5 | const todoItemsUpdatedUpdater = (store: RelayRecordSourceSelectorProxy, { 6 | updatedTodoItemIDs, 7 | changes, 8 | }: { 9 | updatedTodoItemIDs: Iterable, 10 | changes: {| completed?: boolean, title?: string |}, 11 | }) => { 12 | for (const todoItemID of updatedTodoItemIDs) { 13 | const todoItem = store.get(todoItemID) 14 | if (!todoItem) continue 15 | 16 | if (typeof changes.completed === 'boolean') { 17 | todoItem.setValue(changes.completed, 'completed') 18 | } 19 | 20 | if (typeof changes.title === 'string') { 21 | todoItem.setValue(changes.title, 'title') 22 | } 23 | } 24 | } 25 | 26 | export default todoItemsUpdatedUpdater 27 | -------------------------------------------------------------------------------- /src/client/TodoApp/containers/__generated__/TodoListHeader_todoList.graphql.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | */ 4 | 5 | /* eslint-disable */ 6 | 7 | 'use strict'; 8 | 9 | /*:: 10 | import type {ConcreteFragment} from 'relay-runtime'; 11 | export type TodoListHeader_todoList = {| 12 | +name: string; 13 | |}; 14 | */ 15 | 16 | 17 | const fragment /*: ConcreteFragment*/ = { 18 | "argumentDefinitions": [], 19 | "kind": "Fragment", 20 | "metadata": null, 21 | "name": "TodoListHeader_todoList", 22 | "selections": [ 23 | { 24 | "kind": "ScalarField", 25 | "alias": null, 26 | "args": null, 27 | "name": "name", 28 | "storageKey": null 29 | }, 30 | { 31 | "kind": "FragmentSpread", 32 | "name": "AddTodoItemInput_todoList", 33 | "args": null 34 | } 35 | ], 36 | "type": "TodoList" 37 | }; 38 | 39 | module.exports = fragment; 40 | -------------------------------------------------------------------------------- /src/server/mutations/mutationType.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { GraphQLObjectType } from 'graphql' 4 | 5 | import createTodoItemMutation from './createTodoItemMutation' 6 | import updateTodoItemMutation from './updateTodoItemMutation' 7 | import deleteTodoItemMutation from './deleteTodoItemMutation' 8 | import updateAllItemsOnTodoListMutation from './updateAllItemsOnTodoListMutation' 9 | import deleteCompletedItemsOnTodoListMutation from './deleteCompletedItemsOnTodoListMutation' 10 | 11 | const mutationType = new GraphQLObjectType({ 12 | name: 'Mutation', 13 | fields: { 14 | createTodoItem: createTodoItemMutation, 15 | updateTodoItem: updateTodoItemMutation, 16 | deleteTodoItem: deleteTodoItemMutation, 17 | updateAllItemsOnTodoList: updateAllItemsOnTodoListMutation, 18 | deleteCompletedItemsOnTodoList: deleteCompletedItemsOnTodoListMutation, 19 | }, 20 | }) 21 | 22 | export default mutationType 23 | -------------------------------------------------------------------------------- /src/client/TodoApp/containers/__generated__/TodoListCard_todoList.graphql.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | */ 4 | 5 | /* eslint-disable */ 6 | 7 | 'use strict'; 8 | 9 | /*:: 10 | import type {ConcreteFragment} from 'relay-runtime'; 11 | export type TodoListCard_todoList = {| |}; 12 | */ 13 | 14 | 15 | const fragment /*: ConcreteFragment*/ = { 16 | "argumentDefinitions": [], 17 | "kind": "Fragment", 18 | "metadata": null, 19 | "name": "TodoListCard_todoList", 20 | "selections": [ 21 | { 22 | "kind": "FragmentSpread", 23 | "name": "TodoListHeader_todoList", 24 | "args": null 25 | }, 26 | { 27 | "kind": "FragmentSpread", 28 | "name": "TodoListItemsWithFilter_todoList", 29 | "args": null 30 | }, 31 | { 32 | "kind": "FragmentSpread", 33 | "name": "TodoListFooter_todoList", 34 | "args": null 35 | } 36 | ], 37 | "type": "TodoList" 38 | }; 39 | 40 | module.exports = fragment; 41 | -------------------------------------------------------------------------------- /src/client/TodoApp/updaters/todoItemDeletedUpdater.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { ConnectionHandler } from 'relay-runtime' 4 | import type { 5 | DataID, 6 | RelayRecordSourceSelectorProxy, 7 | RelayRecordProxy, 8 | } from 'relay-runtime' 9 | import todoListItemsConnectionNames from '../registrations/todoListItemsConnectionNames' 10 | 11 | const todoItemDeletedUpdater = (store: RelayRecordSourceSelectorProxy, { 12 | todoList, 13 | deletedTodoItemID, 14 | }: { 15 | todoList: RelayRecordProxy, 16 | deletedTodoItemID: DataID, 17 | }) => { 18 | todoListItemsConnectionNames.forEach((connName) => { 19 | ['all', 'active', 'completed'].forEach((filter) => { 20 | const conn = ConnectionHandler.getConnection( 21 | todoList, 22 | connName, 23 | { filter }, 24 | ) 25 | if (!conn) return 26 | ConnectionHandler.deleteNode(conn, deletedTodoItemID) 27 | }) 28 | }) 29 | // TODO: Remove the todo item itself from store 30 | } 31 | 32 | export default todoItemDeletedUpdater 33 | -------------------------------------------------------------------------------- /src/client/TodoApp/containers/__generated__/TodoListItemsWithFilter_todoList.graphql.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | */ 4 | 5 | /* eslint-disable */ 6 | 7 | 'use strict'; 8 | 9 | /*:: 10 | import type {ConcreteFragment} from 'relay-runtime'; 11 | export type TodoListItemsWithFilter_todoList = {| |}; 12 | */ 13 | 14 | 15 | const fragment /*: ConcreteFragment*/ = { 16 | "argumentDefinitions": [ 17 | { 18 | "kind": "LocalArgument", 19 | "name": "filter", 20 | "type": "TodoListItemsFilterEnum", 21 | "defaultValue": "all" 22 | }, 23 | { 24 | "kind": "LocalArgument", 25 | "name": "count", 26 | "type": "Int", 27 | "defaultValue": 10 28 | } 29 | ], 30 | "kind": "Fragment", 31 | "metadata": null, 32 | "name": "TodoListItemsWithFilter_todoList", 33 | "selections": [ 34 | { 35 | "kind": "FragmentSpread", 36 | "name": "TodoListItems_todoList", 37 | "args": null 38 | } 39 | ], 40 | "type": "TodoList" 41 | }; 42 | 43 | module.exports = fragment; 44 | -------------------------------------------------------------------------------- /src/client/TodoApp/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | import TodoListCard from './TodoListCard' 4 | import TodoListSelect from './TodoListSelect' 5 | 6 | export default class TodoApp extends Component { 7 | constructor(props, context) { 8 | super(props, context) 9 | this.state = { 10 | todoListID: null, 11 | } 12 | } 13 | 14 | render() { 15 | return ( 16 |
17 | 18 |
19 |

Double-click to edit a todo

20 | Todo list:  21 | { this.setState({ todoListID }) }} 23 | /> 24 |

Template by Sindre Sorhus

25 |

Created by @zetavg

26 |

Part of TodoMVC

27 |
28 |
29 | ) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/server/subscriptions/subscriptionType.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { GraphQLObjectType } from 'graphql' 4 | 5 | import itemOnTodoListCreatedSubscription from './itemOnTodoListCreatedSubscription' 6 | import itemOnTodoListUpdatedSubscription from './itemOnTodoListUpdatedSubscription' 7 | import itemOnTodoListDeletedSubscription from './itemOnTodoListDeletedSubscription' 8 | import itemsOnTodoListUpdatedSubscription from './itemsOnTodoListUpdatedSubscription' 9 | import itemsOnTodoListDeletedSubscription from './itemsOnTodoListDeletedSubscription' 10 | 11 | // $FlowFixMe 12 | const subscriptionType = new GraphQLObjectType({ 13 | name: 'Subscription', 14 | fields: { 15 | itemOnTodoListCreated: itemOnTodoListCreatedSubscription, 16 | itemOnTodoListUpdated: itemOnTodoListUpdatedSubscription, 17 | itemOnTodoListDeleted: itemOnTodoListDeletedSubscription, 18 | itemsOnTodoListUpdated: itemsOnTodoListUpdatedSubscription, 19 | itemsOnTodoListDeleted: itemsOnTodoListDeletedSubscription, 20 | }, 21 | }) 22 | 23 | export default subscriptionType 24 | -------------------------------------------------------------------------------- /src/server/types/todoItemType.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { 4 | GraphQLNonNull, 5 | GraphQLObjectType, 6 | GraphQLID, 7 | GraphQLBoolean, 8 | GraphQLString, 9 | } from 'graphql' 10 | import { toGlobalId, globalIdField } from 'graphql-relay' 11 | 12 | import { nodeInterface } from '../relay' 13 | 14 | import getType from './_getType' 15 | 16 | import { getListFromTodoItem } from '../data' 17 | 18 | const todoItemType = new GraphQLObjectType({ 19 | name: 'TodoItem', 20 | interfaces: [nodeInterface], 21 | fields: () => ({ 22 | id: globalIdField(), 23 | completed: { 24 | type: new GraphQLNonNull(GraphQLBoolean), 25 | }, 26 | title: { 27 | type: new GraphQLNonNull(GraphQLString), 28 | }, 29 | listID: { 30 | type: new GraphQLNonNull(GraphQLID), 31 | resolve: todoItem => toGlobalId('TodoList', todoItem.listID), 32 | }, 33 | list: { 34 | type: new GraphQLNonNull(getType('todoListType')), 35 | resolve: getListFromTodoItem, 36 | }, 37 | }), 38 | }) 39 | 40 | export default todoItemType 41 | -------------------------------------------------------------------------------- /src/server/relay.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { nodeDefinitions } from 'graphql-relay' 4 | import atob from 'atob' 5 | 6 | import { getDataByID } from './data' 7 | import type { 8 | User, 9 | TodoList, 10 | TodoItem, 11 | } from './data' 12 | 13 | export const getTypeAndIDFromGlobalID = (gid: string) => { 14 | const [type, id] = atob(gid).split(':') 15 | return { type, id } 16 | } 17 | 18 | export const getObjectFromGlobalID = (gid: string) => { 19 | const { id } = getTypeAndIDFromGlobalID(gid) 20 | return getDataByID(id) 21 | } 22 | 23 | export const getTypeFromObject = (obj: User | TodoList | TodoItem) => { 24 | switch (obj.__type) { 25 | case 'User': 26 | return require('./types/userType').default 27 | case 'TodoList': 28 | return require('./types/todoListType').default 29 | case 'TodoItem': 30 | return require('./types/todoItemType').default 31 | default: 32 | return null 33 | } 34 | } 35 | 36 | export const { nodeInterface, nodeField } = nodeDefinitions( 37 | getObjectFromGlobalID, 38 | getTypeFromObject, 39 | ) 40 | -------------------------------------------------------------------------------- /src/client/TodoApp/updaters/todoItemsDeletedUpdater.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { ConnectionHandler } from 'relay-runtime' 4 | import type { 5 | RelayRecordSourceSelectorProxy, 6 | RelayRecordProxy, 7 | } from 'relay-runtime' 8 | import todoListItemsConnectionNames from '../registrations/todoListItemsConnectionNames' 9 | 10 | const todoItemsRemovedUpdater = (store: RelayRecordSourceSelectorProxy, { 11 | todoList, 12 | deletedTodoItemIDs, 13 | }: { 14 | deletedTodoItemIDs: Iterable, 15 | todoList: RelayRecordProxy, 16 | }) => { 17 | todoListItemsConnectionNames.forEach((connName) => { 18 | ['all', 'active', 'completed'].forEach((filter) => { 19 | const conn = ConnectionHandler.getConnection( 20 | todoList, 21 | connName, 22 | { filter }, 23 | ) 24 | if (!conn) return 25 | 26 | for (const id of deletedTodoItemIDs) { 27 | ConnectionHandler.deleteNode(conn, id) 28 | } 29 | }) 30 | }) 31 | // TODO: Remove the todo items itself from store 32 | } 33 | 34 | export default todoItemsRemovedUpdater 35 | -------------------------------------------------------------------------------- /src/server/subscriptions/itemOnTodoListCreatedSubscription.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { 4 | GraphQLNonNull, 5 | GraphQLObjectType, 6 | GraphQLID, 7 | } from 'graphql' 8 | 9 | import { withFilter } from 'graphql-subscriptions' 10 | 11 | import getSubscriptionFieldsFromMutation from './_getSubscriptionFieldsFromMutation' 12 | import createTodoItemMutation from '../mutations/createTodoItemMutation' 13 | 14 | import pubsub from './pubsub' 15 | import { TODO_ITEM_CREATED } from './pubsub/event-types' 16 | 17 | const itemOnTodoListCreatedSubscription = { 18 | args: { 19 | todoListID: { 20 | type: new GraphQLNonNull(GraphQLID), 21 | }, 22 | }, 23 | type: new GraphQLObjectType({ 24 | name: 'ItemOnTodoListCreated', 25 | fields: getSubscriptionFieldsFromMutation(createTodoItemMutation), 26 | }), 27 | subscribe: withFilter( 28 | () => pubsub.asyncIterator(TODO_ITEM_CREATED), 29 | (payload, args) => payload && payload.todoListID === args.todoListID, 30 | ), 31 | resolve: (payload: mixed) => payload, 32 | } 33 | 34 | export default itemOnTodoListCreatedSubscription 35 | -------------------------------------------------------------------------------- /src/server/subscriptions/itemOnTodoListDeletedSubscription.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { 4 | GraphQLNonNull, 5 | GraphQLObjectType, 6 | GraphQLID, 7 | } from 'graphql' 8 | 9 | import { withFilter } from 'graphql-subscriptions' 10 | 11 | import getSubscriptionFieldsFromMutation from './_getSubscriptionFieldsFromMutation' 12 | import deleteTodoItemMutation from '../mutations/deleteTodoItemMutation' 13 | 14 | import pubsub from './pubsub' 15 | import { TODO_ITEM_DELETED } from './pubsub/event-types' 16 | 17 | const itemOnTodoListDeletedSubscription = { 18 | args: { 19 | todoListID: { 20 | type: new GraphQLNonNull(GraphQLID), 21 | }, 22 | }, 23 | type: new GraphQLObjectType({ 24 | name: 'ItemOnTodoListDeleted', 25 | fields: getSubscriptionFieldsFromMutation(deleteTodoItemMutation), 26 | }), 27 | subscribe: withFilter( 28 | () => pubsub.asyncIterator(TODO_ITEM_DELETED), 29 | (payload, args) => payload && payload.todoListID === args.todoListID, 30 | ), 31 | resolve: (payload: mixed) => payload, 32 | } 33 | 34 | export default itemOnTodoListDeletedSubscription 35 | -------------------------------------------------------------------------------- /src/server/subscriptions/itemOnTodoListUpdatedSubscription.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { 4 | GraphQLNonNull, 5 | GraphQLObjectType, 6 | GraphQLID, 7 | } from 'graphql' 8 | 9 | import { withFilter } from 'graphql-subscriptions' 10 | 11 | import getSubscriptionFieldsFromMutation from './_getSubscriptionFieldsFromMutation' 12 | import updateTodoItemMutation from '../mutations/updateTodoItemMutation' 13 | 14 | import pubsub from './pubsub' 15 | import { TODO_ITEM_UPDATED } from './pubsub/event-types' 16 | 17 | const itemOnTodoListUpdatedSubscription = { 18 | args: { 19 | todoListID: { 20 | type: new GraphQLNonNull(GraphQLID), 21 | }, 22 | }, 23 | type: new GraphQLObjectType({ 24 | name: 'ItemOnTodoListUpdated', 25 | fields: getSubscriptionFieldsFromMutation(updateTodoItemMutation), 26 | }), 27 | subscribe: withFilter( 28 | () => pubsub.asyncIterator(TODO_ITEM_UPDATED), 29 | (payload, args) => payload && payload.todoListID === args.todoListID, 30 | ), 31 | resolve: (payload: mixed) => payload, 32 | } 33 | 34 | export default itemOnTodoListUpdatedSubscription 35 | -------------------------------------------------------------------------------- /src/client/TodoApp/TodoListCard.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { QueryRenderer, graphql } from 'react-relay' 3 | 4 | import environment from './relay/environment' 5 | 6 | import TodoListCardContainer from './containers/TodoListCard' 7 | 8 | const TodoListCard = ({ todoListID }) => ( 9 | { 31 | if (error) { 32 | throw error 33 | } else if (props) { 34 | return 35 | } 36 | return
Loading
37 | }} 38 | /> 39 | ) 40 | 41 | export default TodoListCard 42 | -------------------------------------------------------------------------------- /src/client/TodoApp/containers/__generated__/TodoListFooter_todoList.graphql.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | */ 4 | 5 | /* eslint-disable */ 6 | 7 | 'use strict'; 8 | 9 | /*:: 10 | import type {ConcreteFragment} from 'relay-runtime'; 11 | export type TodoListFooter_todoList = {| 12 | +id: string; 13 | +activeItemsCount: number; 14 | +completedItemsCount: number; 15 | |}; 16 | */ 17 | 18 | 19 | const fragment /*: ConcreteFragment*/ = { 20 | "argumentDefinitions": [], 21 | "kind": "Fragment", 22 | "metadata": null, 23 | "name": "TodoListFooter_todoList", 24 | "selections": [ 25 | { 26 | "kind": "ScalarField", 27 | "alias": null, 28 | "args": null, 29 | "name": "id", 30 | "storageKey": null 31 | }, 32 | { 33 | "kind": "ScalarField", 34 | "alias": null, 35 | "args": null, 36 | "name": "activeItemsCount", 37 | "storageKey": null 38 | }, 39 | { 40 | "kind": "ScalarField", 41 | "alias": null, 42 | "args": null, 43 | "name": "completedItemsCount", 44 | "storageKey": null 45 | } 46 | ], 47 | "type": "TodoList" 48 | }; 49 | 50 | module.exports = fragment; 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Compiled code 36 | /dist 37 | 38 | # Dependency directories 39 | node_modules/ 40 | jspm_packages/ 41 | 42 | # Typescript v1 declaration files 43 | typings/ 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env 62 | -------------------------------------------------------------------------------- /src/client/TodoApp/components/TodoListSelect.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | export default class TodoListSelect extends Component { 5 | static propTypes = { 6 | user: PropTypes.shape({ 7 | todoLists: PropTypes.shape({ 8 | edges: PropTypes.arrayOf(PropTypes.shape({ 9 | node: PropTypes.shape({ 10 | id: PropTypes.string.isRequired, 11 | name: PropTypes.string.isRequired, 12 | }).isRequired, 13 | }).isRequired).isRequired, 14 | }).isRequired, 15 | }).isRequired, 16 | onChangeID: PropTypes.func, 17 | } 18 | 19 | render() { 20 | const { user } = this.props 21 | 22 | return ( 23 | 36 | ) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/client/TodoApp/components/AddTodoItemInput.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const ENTER_KEY_CODE = 13 5 | 6 | export default class AddTodoItemInput extends Component { 7 | static propTypes = { 8 | todoItemNameValue: PropTypes.string, 9 | onChangeTodoItemName: PropTypes.func.isRequired, 10 | onSubmitEditing: PropTypes.func.isRequired, 11 | } 12 | 13 | static defaultProps = { 14 | todoItemNameValue: '', 15 | } 16 | 17 | componentDidMount() { 18 | this.input.focus() 19 | } 20 | 21 | _handleChange = (e) => { 22 | this.props.onChangeTodoItemName(e.target.value) 23 | } 24 | 25 | _handleKeyDown = (e) => { 26 | if (e.keyCode === ENTER_KEY_CODE) { 27 | this.props.onSubmitEditing() 28 | } 29 | } 30 | 31 | render() { 32 | const { todoItemNameValue } = this.props 33 | 34 | return ( 35 | this.input = ref} 39 | onChange={this._handleChange} 40 | onKeyDown={this._handleKeyDown} 41 | value={todoItemNameValue} 42 | /> 43 | ) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/client/TodoApp/components/TodoListFooter.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | export default class TodoListFooter extends Component { 5 | static propTypes = { 6 | todoList: PropTypes.shape({ 7 | activeItemsCount: PropTypes.number.isRequired, 8 | completedItemsCount: PropTypes.number.isRequired, 9 | }).isRequired, 10 | onClearCompletedPress: PropTypes.func.isRequired, 11 | } 12 | 13 | render() { 14 | const { 15 | todoList, 16 | onClearCompletedPress, 17 | } = this.props 18 | 19 | return ( 20 |
21 | 22 | {todoList.activeItemsCount}  23 | {todoList.activeItemsCount > 1 ? 'items' : 'item'} left 24 | 25 | {(() => { 26 | if (todoList.completedItemsCount > 0) { 27 | return ( 28 | 34 | ) 35 | } 36 | return null 37 | })()} 38 |
39 | ) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/client/TodoApp/updaters/todoItemUpdatedUpdater.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { ConnectionHandler } from 'relay-runtime' 4 | import type { 5 | RelayRecordSourceSelectorProxy, 6 | RelayRecordProxy, 7 | } from 'relay-runtime' 8 | import todoListItemsConnectionNames from '../registrations/todoListItemsConnectionNames' 9 | 10 | const todoItemUpdatedUpdater = (store: RelayRecordSourceSelectorProxy, { 11 | todoList, 12 | todoItem, 13 | }: { 14 | todoItem: RelayRecordProxy, 15 | todoList: RelayRecordProxy, 16 | }) => { 17 | const todoItemID = todoItem.getValue('id') 18 | if (typeof todoItemID !== 'string') return 19 | const completed = todoItem.getValue('completed') 20 | if (typeof completed !== 'boolean') return 21 | 22 | todoListItemsConnectionNames.forEach((connName) => { 23 | ['active', 'completed'].forEach((filter) => { 24 | const conn = ConnectionHandler.getConnection( 25 | todoList, 26 | connName, 27 | { filter }, 28 | ) 29 | if (!conn) return 30 | if ((filter === 'completed') === completed) { 31 | // TODO: Refetch the connection 32 | } else { 33 | ConnectionHandler.deleteNode(conn, todoItemID) 34 | } 35 | }) 36 | }) 37 | } 38 | 39 | export default todoItemUpdatedUpdater 40 | -------------------------------------------------------------------------------- /src/client/TodoApp/containers/__generated__/TodoItem_todoItem.graphql.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | */ 4 | 5 | /* eslint-disable */ 6 | 7 | 'use strict'; 8 | 9 | /*:: 10 | import type {ConcreteFragment} from 'relay-runtime'; 11 | export type TodoItem_todoItem = {| 12 | +id: string; 13 | +listID: string; 14 | +completed: boolean; 15 | +title: string; 16 | |}; 17 | */ 18 | 19 | 20 | const fragment /*: ConcreteFragment*/ = { 21 | "argumentDefinitions": [], 22 | "kind": "Fragment", 23 | "metadata": null, 24 | "name": "TodoItem_todoItem", 25 | "selections": [ 26 | { 27 | "kind": "ScalarField", 28 | "alias": null, 29 | "args": null, 30 | "name": "id", 31 | "storageKey": null 32 | }, 33 | { 34 | "kind": "ScalarField", 35 | "alias": null, 36 | "args": null, 37 | "name": "listID", 38 | "storageKey": null 39 | }, 40 | { 41 | "kind": "ScalarField", 42 | "alias": null, 43 | "args": null, 44 | "name": "completed", 45 | "storageKey": null 46 | }, 47 | { 48 | "kind": "ScalarField", 49 | "alias": null, 50 | "args": null, 51 | "name": "title", 52 | "storageKey": null 53 | } 54 | ], 55 | "type": "TodoItem" 56 | }; 57 | 58 | module.exports = fragment; 59 | -------------------------------------------------------------------------------- /src/client/TodoApp/containers/TodoListFooter.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React, { Component } from 'react' 4 | import { graphql, createFragmentContainer } from 'react-relay' 5 | 6 | import environment from '../relay/environment' 7 | 8 | import TodoListFooterComponent from '../components/TodoListFooter' 9 | 10 | import DeleteCompletedItemsOnTodoListMutation from '../mutations/DeleteCompletedItemsOnTodoListMutation' 11 | 12 | type Props = {| 13 | todoList: { 14 | id: string, 15 | }, 16 | |}; 17 | 18 | class TodoListFooterContainer extends Component { 19 | _getNewClearCompletedItemsMutation = () => { 20 | const { todoList } = this.props 21 | 22 | return new DeleteCompletedItemsOnTodoListMutation(environment, { 23 | todoListID: todoList.id, 24 | }) 25 | } 26 | 27 | _handleClearCompletedPress = () => { 28 | const mutation = this._getNewClearCompletedItemsMutation() 29 | mutation.commit() 30 | } 31 | 32 | render() { 33 | const { todoList } = this.props 34 | 35 | return ( 36 | 40 | ) 41 | } 42 | } 43 | 44 | export default createFragmentContainer( 45 | TodoListFooterContainer, 46 | graphql` 47 | fragment TodoListFooter_todoList on TodoList { 48 | id 49 | activeItemsCount 50 | completedItemsCount 51 | } 52 | `, 53 | ) 54 | -------------------------------------------------------------------------------- /src/server/subscriptions/itemsOnTodoListDeletedSubscription.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { 4 | GraphQLNonNull, 5 | GraphQLList, 6 | GraphQLObjectType, 7 | GraphQLID, 8 | } from 'graphql' 9 | 10 | import { withFilter } from 'graphql-subscriptions' 11 | 12 | import todoListType from '../types/todoListType' 13 | import todoItemType from '../types/todoItemType' 14 | 15 | import pubsub from './pubsub' 16 | import { TODO_ITEMS_DELETED } from './pubsub/event-types' 17 | 18 | const itemsOnTodoListDeletedSubscription = { 19 | args: { 20 | todoListID: { 21 | type: new GraphQLNonNull(GraphQLID), 22 | }, 23 | }, 24 | type: new GraphQLObjectType({ 25 | name: 'ItemsOnTodoListDeleted', 26 | fields: { 27 | todoList: { 28 | type: new GraphQLNonNull(todoListType), 29 | resolve: payload => payload.todoList, 30 | }, 31 | deletedTodoItemIDs: { 32 | type: new GraphQLNonNull(new GraphQLList(GraphQLID)), 33 | resolve: payload => payload.deletedTodoItemIDs, 34 | }, 35 | deletedTodoItems: { 36 | type: new GraphQLNonNull(new GraphQLList(todoItemType)), 37 | resolve: payload => payload.deletedTodoItems, 38 | }, 39 | }, 40 | }), 41 | subscribe: withFilter( 42 | () => pubsub.asyncIterator(TODO_ITEMS_DELETED), 43 | (payload, args) => payload && payload.todoListID === args.todoListID, 44 | ), 45 | resolve: (payload: mixed) => payload, 46 | } 47 | 48 | export default itemsOnTodoListDeletedSubscription 49 | -------------------------------------------------------------------------------- /src/client/TodoApp/relay/network.js: -------------------------------------------------------------------------------- 1 | import { Network } from 'relay-runtime' 2 | import { SubscriptionClient } from 'subscriptions-transport-ws' 3 | import { GRAPHQL_ENDPOINT, GRAPHQL_SUBSCRIPTION_ENDPOINT } from './constants' 4 | 5 | function fetchQuery( 6 | operation, 7 | variables, 8 | // cacheConfig, 9 | // uploadables, 10 | ) { 11 | return fetch(GRAPHQL_ENDPOINT, { 12 | method: 'POST', 13 | headers: { 14 | 'content-type': 'application/json', 15 | }, 16 | body: JSON.stringify({ 17 | query: operation.text, 18 | operationName: operation.name, 19 | variables, 20 | }), 21 | }).then(response => response.json()) 22 | } 23 | 24 | const wsSubscriptionClient = new SubscriptionClient(GRAPHQL_SUBSCRIPTION_ENDPOINT, { 25 | reconnect: true, 26 | }) 27 | 28 | function subscriptionHandler( 29 | operation, 30 | variables, 31 | cacheConfig, 32 | observer, 33 | ) { 34 | const subscription = wsSubscriptionClient.request({ 35 | query: operation.text, 36 | operationName: operation.name, 37 | variables, 38 | }).subscribe({ 39 | next(result) { 40 | observer.onNext(result) 41 | }, 42 | error(e) { 43 | observer.onError(e) 44 | }, 45 | complete() { 46 | observer.onCompleted() 47 | }, 48 | }) 49 | 50 | // Return an object for Relay to unsubscribe with 51 | return { 52 | dispose: () => { 53 | subscription.unsubscribe() 54 | }, 55 | } 56 | } 57 | 58 | const network = Network.create(fetchQuery, subscriptionHandler) 59 | 60 | export default network 61 | -------------------------------------------------------------------------------- /src/server/types/userType.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { 4 | GraphQLNonNull, 5 | GraphQLObjectType, 6 | GraphQLID, 7 | } from 'graphql' 8 | import { 9 | globalIdField, 10 | connectionDefinitions, 11 | connectionArgs, 12 | } from 'graphql-relay' 13 | 14 | import { nodeInterface, getTypeAndIDFromGlobalID } from '../relay' 15 | 16 | import todoListType from './todoListType' 17 | 18 | import { 19 | getTodoListsFromUser, 20 | getTodoListFromUser, 21 | getFirstTodoListFromUser, 22 | connectionFor, 23 | } from '../data' 24 | 25 | const { connectionType: todoListsConnection } = connectionDefinitions({ nodeType: todoListType }) 26 | 27 | const userType = new GraphQLObjectType({ 28 | name: 'User', 29 | interfaces: [nodeInterface], 30 | fields: { 31 | id: globalIdField(), 32 | todoLists: { 33 | type: new GraphQLNonNull(todoListsConnection), 34 | args: connectionArgs, 35 | resolve: async (user, args) => { 36 | const todoLists = await getTodoListsFromUser(user) 37 | return connectionFor( 38 | // $FlowFixMe 39 | todoLists, 40 | args, 41 | ) 42 | }, 43 | }, 44 | todoList: { 45 | type: todoListType, 46 | args: { 47 | id: { 48 | type: GraphQLID, 49 | }, 50 | }, 51 | resolve: async (user, { id }) => { 52 | if (id) { 53 | const { id: todoListID } = getTypeAndIDFromGlobalID(id) 54 | return getTodoListFromUser(user, todoListID) 55 | } 56 | 57 | return await getFirstTodoListFromUser(user) 58 | }, 59 | }, 60 | }, 61 | }) 62 | 63 | export default userType 64 | -------------------------------------------------------------------------------- /src/client/TodoApp/subscriptions/ItemsOnTodoListDeletedSubscription.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { graphql } from 'react-relay' 4 | import type { RecordSourceSelectorProxy } from 'relay-runtime' 5 | import Subscription from './_Subscription' 6 | 7 | import type { ItemsOnTodoListDeletedSubscriptionVariables } 8 | from './__generated__/ItemsOnTodoListDeletedSubscription.graphql' 9 | 10 | import todoItemsDeletedUpdater from '../updaters/todoItemsDeletedUpdater' 11 | 12 | export default class ItemsOnTodoListDeletedSubscription extends Subscription { 13 | static subscription = graphql` 14 | subscription ItemsOnTodoListDeletedSubscription( 15 | $todoListID: ID! 16 | ) { 17 | itemsOnTodoListDeleted(todoListID: $todoListID) { 18 | deletedTodoItemIDs 19 | todoList { 20 | id 21 | completedItemsCount 22 | activeItemsCount 23 | } 24 | } 25 | } 26 | ` 27 | 28 | getSubscriptionConfig() { 29 | const { todoListID } = this.variables 30 | 31 | return { 32 | updater: (store: RecordSourceSelectorProxy) => { 33 | const payload = store.getRootField('itemsOnTodoListDeleted') 34 | if (!payload) throw new Error('cannot get itemsOnTodoListDeleted') 35 | const deletedTodoItemIDs = payload.getValue('deletedTodoItemIDs') 36 | if (!deletedTodoItemIDs) throw new Error('cannot get deletedTodoItemIDs') 37 | const todoList = store.get(todoListID) 38 | if (!todoList) throw new Error('cannot get todoList') 39 | 40 | todoItemsDeletedUpdater(store, { 41 | todoList, 42 | deletedTodoItemIDs, 43 | }) 44 | }, 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/client/TodoApp/subscriptions/ItemOnTodoListDeletedSubscription.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { graphql } from 'react-relay' 4 | import type { RecordSourceSelectorProxy } from 'relay-runtime' 5 | 6 | import Subscription from './_Subscription' 7 | 8 | import type { ItemOnTodoListDeletedSubscriptionVariables } from './__generated__/ItemOnTodoListDeletedSubscription.graphql' 9 | 10 | import todoItemDeletedUpdater from '../updaters/todoItemDeletedUpdater' 11 | 12 | export default class ItemOnTodoListDeletedSubscription extends Subscription { 13 | static subscription = graphql` 14 | subscription ItemOnTodoListDeletedSubscription( 15 | $todoListID: ID! 16 | ) { 17 | itemOnTodoListDeleted(todoListID: $todoListID) { 18 | deletedTodoItemID 19 | todoList { 20 | id 21 | itemsCount 22 | completedItemsCount 23 | activeItemsCount 24 | } 25 | } 26 | } 27 | ` 28 | 29 | getSubscriptionConfig() { 30 | const { todoListID } = this.variables 31 | 32 | return { 33 | updater: (store: RecordSourceSelectorProxy) => { 34 | const payload = store.getRootField('itemOnTodoListDeleted') 35 | if (!payload) throw new Error('Cannot get itemOnTodoListDeleted') 36 | const deletedTodoItemID = payload.getValue('deletedTodoItemID') 37 | if (!deletedTodoItemID) throw new Error('cannot get deletedTodoItemID') 38 | const todoList = store.get(todoListID) 39 | if (!todoList) throw new Error('cannot get todoList') 40 | 41 | todoItemDeletedUpdater(store, { 42 | todoList, 43 | deletedTodoItemID, 44 | }) 45 | }, 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/client/TodoApp/components/TodoListItemsWithFilter.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import TodoListItems from '../containers/TodoListItems' 4 | 5 | export default class TodoListItemsWithFilter extends Component { 6 | static propTypes = { 7 | todoListItemsRef: PropTypes.any, 8 | todoList: PropTypes.shape({}).isRequired, 9 | filterValue: PropTypes.string.isRequired, 10 | onFilterPress: PropTypes.func.isRequired, 11 | } 12 | 13 | render() { 14 | const { 15 | todoListItemsRef, 16 | todoList, 17 | filterValue, 18 | onFilterPress, 19 | } = this.props 20 | 21 | return ( 22 |
23 | 27 |
    28 |
  • 29 | 35 |
  • 36 |
  • 37 | 43 |
  • 44 |
  • 45 | 51 |
  • 52 |
53 |
54 | ) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/client/TodoApp/updaters/todoItemCreatedUpdater.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { ConnectionHandler } from 'relay-runtime' 4 | import type { 5 | RelayRecordSourceSelectorProxy, 6 | RelayRecordProxy, 7 | } from 'relay-runtime' 8 | 9 | import todoListItemsConnectionNames from '../registrations/todoListItemsConnectionNames' 10 | 11 | const todoItemCreatedUpdater = (store: RelayRecordSourceSelectorProxy, { 12 | todoList, 13 | todoListItemsConnectionEdge, 14 | }: { 15 | todoList: RelayRecordProxy, 16 | todoListItemsConnectionEdge: RelayRecordProxy, 17 | }) => { 18 | const todoItem = todoListItemsConnectionEdge.getLinkedRecord('node') 19 | if (!todoItem) throw new Error('Cannot get node from todoListItemsConnectionEdge') 20 | const todoItemCompleted = todoItem.getValue('completed') 21 | 22 | todoListItemsConnectionNames.forEach((connName) => { 23 | ['all', 'active', 'completed'].forEach((filter) => { 24 | const conn = ConnectionHandler.getConnection( 25 | todoList, 26 | connName, 27 | { filter }, 28 | ) 29 | if (!conn) return 30 | 31 | if ( 32 | (filter === 'all') || 33 | (filter === 'active' && !todoItemCompleted) || 34 | (filter === 'completed' && todoItemCompleted) 35 | ) { 36 | const aleadyInConn = conn.getLinkedRecords('edges') 37 | .map(edge => edge.getValue('cursor')) 38 | .includes(todoListItemsConnectionEdge.getValue('cursor')) 39 | 40 | if (aleadyInConn) return 41 | 42 | ConnectionHandler.insertEdgeAfter( 43 | conn, 44 | ConnectionHandler.buildConnectionEdge(store, conn, todoListItemsConnectionEdge), 45 | ) 46 | } 47 | }) 48 | }) 49 | } 50 | 51 | export default todoItemCreatedUpdater 52 | -------------------------------------------------------------------------------- /src/client/TodoApp/subscriptions/ItemOnTodoListUpdatedSubscription.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { graphql } from 'react-relay' 4 | import type { RecordSourceSelectorProxy } from 'relay-runtime' 5 | 6 | import Subscription from './_Subscription' 7 | 8 | import todoItemUpdatedUpdater from '../updaters/todoItemUpdatedUpdater' 9 | 10 | import type { ItemOnTodoListUpdatedSubscriptionVariables } 11 | from './__generated__/ItemOnTodoListUpdatedSubscription.graphql' 12 | 13 | export default class ItemOnTodoListUpdatedSubscription 14 | extends Subscription { 15 | static subscription = graphql` 16 | subscription ItemOnTodoListUpdatedSubscription( 17 | $todoListID: ID! 18 | ) { 19 | itemOnTodoListUpdated(todoListID: $todoListID) { 20 | todoItem { 21 | id 22 | completed 23 | title 24 | listID 25 | } 26 | todoList { 27 | id 28 | completedItemsCount 29 | activeItemsCount 30 | } 31 | } 32 | } 33 | ` 34 | 35 | getSubscriptionConfig() { 36 | const { todoListID } = this.variables 37 | 38 | return { 39 | updater: (store: RecordSourceSelectorProxy) => { 40 | const payload = store.getRootField('itemOnTodoListUpdated') 41 | if (!payload) throw new Error('cannot get itemOnTodoListUpdated') 42 | const todoItem = payload.getLinkedRecord('todoItem') 43 | if (!todoItem) throw new Error('cannot get todoItem') 44 | const todoList = store.get(todoListID) 45 | if (!todoList) throw new Error('cannot get todoList') 46 | 47 | todoItemUpdatedUpdater(store, { 48 | todoList, 49 | todoItem, 50 | }) 51 | }, 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/server/mutations/deleteTodoItemMutation.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { 4 | GraphQLNonNull, 5 | GraphQLID, 6 | } from 'graphql' 7 | import { mutationWithClientMutationId, toGlobalId } from 'graphql-relay' 8 | 9 | import { getTypeAndIDFromGlobalID } from '../relay' 10 | 11 | import todoItemType from '../types/todoItemType' 12 | import todoListType from '../types/todoListType' 13 | 14 | import { deleteTodoItem, getListFromTodoItem } from '../data' 15 | 16 | import pubsub from '../subscriptions/pubsub' 17 | import { TODO_ITEM_DELETED } from '../subscriptions/pubsub/event-types' 18 | 19 | const deleteTodoItemMutation = mutationWithClientMutationId({ 20 | name: 'DeleteTodoItem', 21 | inputFields: { 22 | todoItemID: { 23 | type: new GraphQLNonNull(GraphQLID), 24 | }, 25 | }, 26 | outputFields: { 27 | deletedTodoItemID: { 28 | type: new GraphQLNonNull(GraphQLID), 29 | resolve: payload => payload.deletedTodoItemID, 30 | }, 31 | deletedtodoItem: { 32 | type: new GraphQLNonNull(todoItemType), 33 | resolve: payload => payload.todoItem, 34 | }, 35 | todoList: { 36 | type: new GraphQLNonNull(todoListType), 37 | resolve: async payload => await getListFromTodoItem(payload.todoItem), 38 | }, 39 | }, 40 | mutateAndGetPayload: async ({ todoItemID: todoItemGlobalID }) => { 41 | const { id: todoItemID } = getTypeAndIDFromGlobalID(todoItemGlobalID) 42 | 43 | const todoItem = await deleteTodoItem(todoItemID) 44 | 45 | const payload = { 46 | todoItem, 47 | deletedTodoItemID: todoItemGlobalID, 48 | } 49 | 50 | pubsub.publish(TODO_ITEM_DELETED, { 51 | todoListID: toGlobalId('TodoList', todoItem.listID), 52 | ...payload, 53 | }) 54 | 55 | return payload 56 | }, 57 | }) 58 | 59 | export default deleteTodoItemMutation 60 | -------------------------------------------------------------------------------- /src/server/mutations/updateTodoItemMutation.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { 4 | GraphQLNonNull, 5 | GraphQLID, 6 | GraphQLString, 7 | GraphQLBoolean, 8 | } from 'graphql' 9 | import { mutationWithClientMutationId, toGlobalId } from 'graphql-relay' 10 | 11 | import { getTypeAndIDFromGlobalID } from '../relay' 12 | 13 | import todoItemType from '../types/todoItemType' 14 | import todoListType from '../types/todoListType' 15 | 16 | import { updateTodoItem, getListFromTodoItem } from '../data' 17 | 18 | import pubsub from '../subscriptions/pubsub' 19 | import { TODO_ITEM_UPDATED } from '../subscriptions/pubsub/event-types' 20 | 21 | const updateTodoItemMutation = mutationWithClientMutationId({ 22 | name: 'UpdateTodoItem', 23 | inputFields: { 24 | todoItemID: { 25 | type: new GraphQLNonNull(GraphQLID), 26 | }, 27 | title: { 28 | type: GraphQLString, 29 | }, 30 | completed: { 31 | type: GraphQLBoolean, 32 | }, 33 | }, 34 | outputFields: { 35 | todoItem: { 36 | type: new GraphQLNonNull(todoItemType), 37 | resolve: payload => payload.todoItem, 38 | }, 39 | todoList: { 40 | type: new GraphQLNonNull(todoListType), 41 | resolve: async payload => getListFromTodoItem(payload.todoItem), 42 | }, 43 | }, 44 | mutateAndGetPayload: async ({ todoItemID: todoItemGlobalID, title, completed }) => { 45 | const { id: todoItemID } = getTypeAndIDFromGlobalID(todoItemGlobalID) 46 | 47 | // $FlowFixMe 48 | const todoItem = await updateTodoItem(todoItemID, { 49 | title, 50 | completed, 51 | }) 52 | 53 | const payload = { 54 | todoItem, 55 | } 56 | 57 | pubsub.publish(TODO_ITEM_UPDATED, { 58 | todoListID: toGlobalId('TodoList', todoItem.listID), 59 | ...payload, 60 | }) 61 | 62 | return payload 63 | }, 64 | }) 65 | 66 | export default updateTodoItemMutation 67 | -------------------------------------------------------------------------------- /src/server/subscriptions/itemsOnTodoListUpdatedSubscription.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { 4 | GraphQLNonNull, 5 | GraphQLList, 6 | GraphQLObjectType, 7 | GraphQLID, 8 | GraphQLString, 9 | GraphQLBoolean, 10 | } from 'graphql' 11 | 12 | import { withFilter } from 'graphql-subscriptions' 13 | 14 | import todoListType from '../types/todoListType' 15 | import todoItemType from '../types/todoItemType' 16 | 17 | import pubsub from './pubsub' 18 | import { TODO_ITEMS_UPDATED } from './pubsub/event-types' 19 | 20 | const itemsOnTodoListUpdatedSubscription = { 21 | args: { 22 | todoListID: { 23 | type: new GraphQLNonNull(GraphQLID), 24 | }, 25 | }, 26 | type: new GraphQLObjectType({ 27 | name: 'ItemsOnTodoListUpdated', 28 | fields: { 29 | todoList: { 30 | type: new GraphQLNonNull(todoListType), 31 | resolve: payload => payload.todoList, 32 | }, 33 | updatedTodoItemIDs: { 34 | type: new GraphQLNonNull(new GraphQLList(GraphQLID)), 35 | resolve: payload => payload.updatedTodoItemIDs, 36 | }, 37 | updatedTodoItems: { 38 | type: new GraphQLNonNull(new GraphQLList(todoItemType)), 39 | resolve: payload => payload.updatedTodoItems, 40 | }, 41 | changes: { 42 | type: new GraphQLNonNull(new GraphQLObjectType({ 43 | name: 'ItemsOnTodoListUpdatedChanges', 44 | fields: { 45 | title: { type: GraphQLString }, 46 | completed: { type: GraphQLBoolean }, 47 | }, 48 | })), 49 | resolve: payload => payload.changes, 50 | }, 51 | }, 52 | }), 53 | subscribe: withFilter( 54 | () => pubsub.asyncIterator(TODO_ITEMS_UPDATED), 55 | (payload, args) => payload && payload.todoListID === args.todoListID, 56 | ), 57 | resolve: (payload: mixed) => payload, 58 | } 59 | 60 | export default itemsOnTodoListUpdatedSubscription 61 | -------------------------------------------------------------------------------- /src/client/TodoApp/subscriptions/ItemsOnTodoListUpdatedSubscription.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { graphql } from 'react-relay' 4 | import type { RecordSourceSelectorProxy } from 'relay-runtime' 5 | 6 | import Subscription from './_Subscription' 7 | 8 | import todoItemsUpdatedUpdater from '../updaters/todoItemsUpdatedUpdater' 9 | 10 | import type { ItemsOnTodoListUpdatedSubscriptionVariables } 11 | from './__generated__/ItemsOnTodoListUpdatedSubscription.graphql' 12 | 13 | export default class ItemsOnTodoListUpdatedSubscription 14 | extends Subscription { 15 | static subscription = graphql` 16 | subscription ItemsOnTodoListUpdatedSubscription( 17 | $todoListID: ID! 18 | ) { 19 | itemsOnTodoListUpdated(todoListID: $todoListID) { 20 | updatedTodoItemIDs 21 | changes { 22 | title 23 | completed 24 | } 25 | todoList { 26 | id 27 | completedItemsCount 28 | activeItemsCount 29 | } 30 | } 31 | } 32 | ` 33 | 34 | getSubscriptionConfig() { 35 | return { 36 | updater: (store: RecordSourceSelectorProxy) => { 37 | const payload = store.getRootField('itemsOnTodoListUpdated') 38 | if (!payload) throw new Error('cannot get itemsOnTodoListUpdated') 39 | const updatedTodoItemIDs = payload.getValue('updatedTodoItemIDs') 40 | if (!updatedTodoItemIDs) throw new Error('cannot get updatedTodoItemIDs') 41 | const changes = payload.getLinkedRecord('changes') 42 | if (!changes) throw new Error('cannot get changes') 43 | const title = changes.getValue('title') 44 | const completed = changes.getValue('completed') 45 | 46 | todoItemsUpdatedUpdater(store, { 47 | updatedTodoItemIDs, 48 | changes: { 49 | title: (typeof title === 'string') ? title : undefined, 50 | completed: (typeof completed === 'boolean') ? completed : undefined, 51 | }, 52 | }) 53 | }, 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/client/TodoApp/containers/AddTodoItemInput.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React, { Component } from 'react' 4 | import { graphql, createFragmentContainer } from 'react-relay' 5 | import type { DataID } from 'react-relay' 6 | import environment from '../relay/environment' 7 | 8 | import AddTodoItemInputComponent from '../components/AddTodoItemInput' 9 | 10 | import CreateTodoItemMutation from '../mutations/CreateTodoItemMutation' 11 | 12 | type Props = {| 13 | todoList: { 14 | id: DataID, 15 | }, 16 | |}; 17 | 18 | type State = { 19 | mutation: CreateTodoItemMutation, 20 | }; 21 | 22 | class AddTodoItemInputContainer extends Component { 23 | constructor(props, context) { 24 | super(props, context) 25 | 26 | this.state = { 27 | mutation: this._getNewMutation(), 28 | } 29 | } 30 | 31 | _getNewMutation = () => { 32 | const { todoList } = this.props 33 | 34 | return new CreateTodoItemMutation(environment, { 35 | todoListID: todoList.id, 36 | }) 37 | } 38 | 39 | _handleTodoItemNameChange = (todoItemName) => { 40 | const { mutation } = this.state 41 | this.setState({ 42 | mutation: CreateTodoItemMutation.updateInput(mutation, { title: todoItemName }), 43 | }) 44 | } 45 | 46 | _handleSubmitEditing = async () => { 47 | const { mutation } = this.state 48 | 49 | try { 50 | this.setState({ mutation: this._getNewMutation() }) 51 | await mutation.commit() 52 | } catch (e) { 53 | this.setState({ mutation }) 54 | } 55 | } 56 | 57 | render() { 58 | const { mutation } = this.state 59 | 60 | return ( 61 | 66 | ) 67 | } 68 | } 69 | 70 | export default createFragmentContainer( 71 | AddTodoItemInputContainer, 72 | graphql` 73 | fragment AddTodoItemInput_todoList on TodoList { 74 | id 75 | } 76 | `, 77 | ) 78 | -------------------------------------------------------------------------------- /src/client/TodoApp/containers/__generated__/TodoListSelect_user.graphql.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | */ 4 | 5 | /* eslint-disable */ 6 | 7 | 'use strict'; 8 | 9 | /*:: 10 | import type {ConcreteFragment} from 'relay-runtime'; 11 | export type TodoListSelect_user = {| 12 | +todoLists: {| 13 | +edges: ?$ReadOnlyArray; 19 | |}; 20 | |}; 21 | */ 22 | 23 | 24 | const fragment /*: ConcreteFragment*/ = { 25 | "argumentDefinitions": [], 26 | "kind": "Fragment", 27 | "metadata": null, 28 | "name": "TodoListSelect_user", 29 | "selections": [ 30 | { 31 | "kind": "LinkedField", 32 | "alias": null, 33 | "args": null, 34 | "concreteType": "TodoListConnection", 35 | "name": "todoLists", 36 | "plural": false, 37 | "selections": [ 38 | { 39 | "kind": "LinkedField", 40 | "alias": null, 41 | "args": null, 42 | "concreteType": "TodoListEdge", 43 | "name": "edges", 44 | "plural": true, 45 | "selections": [ 46 | { 47 | "kind": "LinkedField", 48 | "alias": null, 49 | "args": null, 50 | "concreteType": "TodoList", 51 | "name": "node", 52 | "plural": false, 53 | "selections": [ 54 | { 55 | "kind": "ScalarField", 56 | "alias": null, 57 | "args": null, 58 | "name": "id", 59 | "storageKey": null 60 | }, 61 | { 62 | "kind": "ScalarField", 63 | "alias": null, 64 | "args": null, 65 | "name": "name", 66 | "storageKey": null 67 | } 68 | ], 69 | "storageKey": null 70 | } 71 | ], 72 | "storageKey": null 73 | } 74 | ], 75 | "storageKey": null 76 | } 77 | ], 78 | "type": "User" 79 | }; 80 | 81 | module.exports = fragment; 82 | -------------------------------------------------------------------------------- /src/server/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import express from 'express' 4 | import { createServer } from 'http' 5 | import path from 'path' 6 | import { SubscriptionServer } from 'subscriptions-transport-ws' 7 | import { execute, subscribe } from 'graphql' 8 | import graphqlHTTP from '../vendor/express-graphql' 9 | import schema from './schema' 10 | 11 | import { getAuthenticatedUser } from './data' 12 | 13 | const env = process.env.ENV || 'production' 14 | const port = process.env.PORT || 1337 15 | 16 | const app = express() 17 | 18 | app.use((req, res, next) => { 19 | req.viewer = getAuthenticatedUser() 20 | next() 21 | }) 22 | 23 | app.use('/graphql', graphqlHTTP({ 24 | schema, 25 | graphiql: true, 26 | })) 27 | 28 | const server = createServer(app) 29 | 30 | SubscriptionServer.create( 31 | { 32 | schema, 33 | execute, 34 | subscribe, 35 | }, 36 | { 37 | server, 38 | path: '/graphql/subscriptions', 39 | }, 40 | ) 41 | 42 | app.use('/', express.static(path.join(__dirname, '..', 'client'))) 43 | 44 | if (env === 'development') { 45 | /* eslint-disable global-require */ 46 | const webpack = require('webpack') 47 | const webpackDevMiddleware = require('webpack-dev-middleware') 48 | const webpackHotMiddleware = require('webpack-hot-middleware') 49 | const webpackConfig = require('../../webpack.config.js') 50 | const compiler = webpack({ 51 | ...webpackConfig, 52 | entry: [ 53 | ...webpackConfig.entry, 54 | 'webpack-hot-middleware/client?reload=true', 55 | ], 56 | output: { 57 | filename: './bundle.js', 58 | }, 59 | plugins: [ 60 | ...webpackConfig.plugins, 61 | new webpack.HotModuleReplacementPlugin(), 62 | ], 63 | devtool: 'inline-source-map', 64 | }) 65 | app.use(webpackDevMiddleware(compiler, { 66 | publicPath: '/', 67 | noInfo: true, 68 | stats: { 69 | colors: true, 70 | }, 71 | })) 72 | app.use(webpackHotMiddleware(compiler)) 73 | } 74 | 75 | app.get('/', (req, res) => res.sendFile(path.join(__dirname, '..', 'client', 'static', 'index.html'))) 76 | 77 | server.listen(port, () => console.log(`Server is now running on http://0.0.0.0:${port}`)) 78 | -------------------------------------------------------------------------------- /src/server/mutations/deleteCompletedItemsOnTodoListMutation.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { 4 | GraphQLNonNull, 5 | GraphQLList, 6 | GraphQLID, 7 | } from 'graphql' 8 | import { mutationWithClientMutationId, toGlobalId } from 'graphql-relay' 9 | 10 | import { getTypeAndIDFromGlobalID } from '../relay' 11 | 12 | import todoListType from '../types/todoListType' 13 | import todoItemType from '../types/todoItemType' 14 | 15 | import { deleteCompletedItemsOnTodoList } from '../data' 16 | 17 | import pubsub from '../subscriptions/pubsub' 18 | import { TODO_ITEMS_DELETED } from '../subscriptions/pubsub/event-types' 19 | 20 | const deleteCompletedItemsOnTodoListMutation = mutationWithClientMutationId({ 21 | name: 'DeleteCompletedItemsOnTodoList', 22 | inputFields: { 23 | todoListID: { 24 | type: new GraphQLNonNull(GraphQLID), 25 | }, 26 | }, 27 | outputFields: { 28 | todoList: { 29 | type: new GraphQLNonNull(todoListType), 30 | resolve: payload => payload.todoList, 31 | }, 32 | deletedTodoItemIDs: { 33 | type: new GraphQLNonNull(new GraphQLList(GraphQLID)), 34 | resolve: payload => payload.deletedTodoItemIDs, 35 | }, 36 | deletedTodoItems: { 37 | type: new GraphQLNonNull(new GraphQLList(todoItemType)), 38 | resolve: payload => payload.deletedTodoItems, 39 | }, 40 | }, 41 | mutateAndGetPayload: async ({ todoListID: todoListGlobalID }) => { 42 | const { id: todoListID } = getTypeAndIDFromGlobalID(todoListGlobalID) 43 | 44 | const { 45 | todoList, 46 | deletedTodoItemIDs, 47 | deletedTodoItems, 48 | } = await deleteCompletedItemsOnTodoList(todoListID) 49 | 50 | const deletedTodoItemGlobalIDs = deletedTodoItemIDs.map(id => toGlobalId('TodoItem', id)) 51 | 52 | pubsub.publish(TODO_ITEMS_DELETED, { 53 | todoListID: todoListGlobalID, 54 | todoList, 55 | deletedTodoItemIDs: deletedTodoItemGlobalIDs, 56 | deletedTodoItems, 57 | }) 58 | 59 | return { 60 | todoList, 61 | deletedTodoItemIDs: deletedTodoItemGlobalIDs, 62 | deletedTodoItems, 63 | } 64 | }, 65 | }) 66 | 67 | export default deleteCompletedItemsOnTodoListMutation 68 | -------------------------------------------------------------------------------- /src/client/TodoApp/mutations/DeleteTodoItemMutation.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { graphql } from 'react-relay' 4 | import type { RecordSourceSelectorProxy } from 'relay-runtime' 5 | 6 | import Mutation from './_Mutation' 7 | 8 | import todoItemDeletedUpdater from '../updaters/todoItemDeletedUpdater' 9 | 10 | import type { DeleteTodoItemMutationVariables } from './__generated__/DeleteTodoItemMutation.graphql' 11 | 12 | export type DeleteTodoItemInput = $Exact<$PropertyType>; 13 | 14 | export type DeleteTodoItemOptions = {| 15 | todoListID: string, 16 | |}; 17 | 18 | export default class DeleteTodoItemMutation extends Mutation { 19 | static mutation = graphql` 20 | mutation DeleteTodoItemMutation($input: DeleteTodoItemInput!) { 21 | deleteTodoItem(input: $input) { 22 | deletedTodoItemID 23 | todoList { 24 | id 25 | itemsCount 26 | completedItemsCount 27 | activeItemsCount 28 | } 29 | } 30 | } 31 | ` 32 | 33 | static constraints = { 34 | // TODO: add async validation to ensure todo item with the id exists 35 | todoItemID: { 36 | presence: true, 37 | }, 38 | } 39 | 40 | getMutationConfig() { 41 | const { input, options } = this 42 | 43 | return { 44 | updater: (store: RecordSourceSelectorProxy) => { 45 | const payload = store.getRootField('deleteTodoItem') 46 | if (!payload) throw new Error('Cannot get deleteTodoItem') 47 | const todoList = payload.getLinkedRecord('todoList') 48 | if (!todoList) throw new Error('Cannot get todoList') 49 | 50 | todoItemDeletedUpdater(store, { 51 | todoList, 52 | deletedTodoItemID: input.todoItemID, 53 | }) 54 | }, 55 | optimisticUpdater: (store: RecordSourceSelectorProxy) => { 56 | if (!options.todoListID) return 57 | const todoList = store.get(options.todoListID) 58 | if (!todoList) throw new Error('Cannot get todoList') 59 | 60 | todoItemDeletedUpdater(store, { 61 | todoList, 62 | deletedTodoItemID: input.todoItemID, 63 | }) 64 | }, 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-todomvc", 3 | "version": "1.0.0", 4 | "description": "TodoMVC demonstration of GraphQL", 5 | "scripts": { 6 | "start": "node dist/server/index.js", 7 | "build": "npm run babel-compile && npm run webpack-pack && npm run copy-static-to-dist", 8 | "dev": "concurrently \"npm run dev-server\" \"npm run relay-dev-compiler\"", 9 | "test": "echo \"Error: no test specified\" && exit 1", 10 | "flow": "flow", 11 | "dev-server": "ENV=development nodemon src/server/index.js --exec 'babel-node --inspect'", 12 | "relay-dev-compiler": "ENV=development nodemon ./scripts/relay-dev-compiler.js --exec 'babel-node'", 13 | "update-schema": "babel-node ./scripts/update-schema.js", 14 | "relay-compile": "relay-compiler --src ./src/client --schema ./schema.graphql", 15 | "babel-compile": "babel src -d dist", 16 | "webpack-pack": "webpack", 17 | "copy-static-to-dist": "rm -rf dist/client/static && cp -r src/client/static dist/client/static", 18 | "postinstall": "npm run build" 19 | }, 20 | "author": "zetavg ", 21 | "license": "MIT", 22 | "dependencies": { 23 | "atob": "^2.0.3", 24 | "babel-cli": "^6.26.0", 25 | "babel-loader": "^7.1.2", 26 | "babel-plugin-relay": "^1.4.1", 27 | "babel-polyfill": "^6.26.0", 28 | "babel-preset-env": "^1.6.1", 29 | "babel-preset-flow": "^6.23.0", 30 | "babel-preset-react": "^6.24.1", 31 | "babel-preset-stage-1": "^6.24.1", 32 | "btoa": "^1.1.2", 33 | "express": "^4.16.2", 34 | "express-graphql": "^0.6.11", 35 | "graphql": "^0.11.7", 36 | "graphql-relay": "^0.5.3", 37 | "graphql-subscriptions": "^0.5.5", 38 | "left-pad": "^1.2.0", 39 | "react": "^16.1.1", 40 | "react-dom": "^16.1.1", 41 | "react-relay": "^1.4.1", 42 | "subscriptions-transport-ws": "^0.9.1", 43 | "uuid": "^3.1.0", 44 | "validate.js": "^0.12.0", 45 | "webpack": "^3.8.1" 46 | }, 47 | "devDependencies": { 48 | "concurrently": "^3.5.1", 49 | "flow-bin": "^0.59.0", 50 | "nodemon": "^1.12.1", 51 | "relay-compiler": "^1.4.1", 52 | "webpack-dev-middleware": "^1.12.1", 53 | "webpack-hot-middleware": "^2.20.0" 54 | }, 55 | "nodemonConfig": { 56 | "ignore": [ 57 | "dist/*", 58 | "src/client/*" 59 | ] 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/server/mutations/createTodoItemMutation.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { 4 | GraphQLNonNull, 5 | GraphQLID, 6 | GraphQLString, 7 | GraphQLBoolean, 8 | } from 'graphql' 9 | import { mutationWithClientMutationId } from 'graphql-relay' 10 | 11 | import { getTypeAndIDFromGlobalID } from '../relay' 12 | 13 | import todoItemType from '../types/todoItemType' 14 | import todoListType, { todoListItemsConnectionEdgeType } from '../types/todoListType' 15 | 16 | import { 17 | createTodoItem, 18 | getListFromTodoItem, 19 | getItemsFromTodoList, 20 | getEdgeFromDatasetAndNode, 21 | } from '../data' 22 | 23 | import pubsub from '../subscriptions/pubsub' 24 | import { TODO_ITEM_CREATED } from '../subscriptions/pubsub/event-types' 25 | 26 | const createTodoItemMutation = mutationWithClientMutationId({ 27 | name: 'CreateTodoItem', 28 | inputFields: { 29 | todoListID: { 30 | type: new GraphQLNonNull(GraphQLID), 31 | }, 32 | title: { 33 | type: new GraphQLNonNull(GraphQLString), 34 | }, 35 | completed: { 36 | type: GraphQLBoolean, 37 | }, 38 | }, 39 | outputFields: { 40 | todoItem: { 41 | type: new GraphQLNonNull(todoItemType), 42 | resolve: payload => payload.todoItem, 43 | }, 44 | todoList: { 45 | type: new GraphQLNonNull(todoListType), 46 | resolve: async payload => await getListFromTodoItem(payload.todoItem), 47 | }, 48 | todoListItemsConnectionEdge: { 49 | type: new GraphQLNonNull(todoListItemsConnectionEdgeType), 50 | resolve: async payload => getEdgeFromDatasetAndNode( 51 | // $FlowFixMe 52 | await getItemsFromTodoList(await getListFromTodoItem(payload.todoItem)), 53 | payload.todoItem, 54 | ), 55 | }, 56 | }, 57 | mutateAndGetPayload: async ({ todoListID: todoListGlobalID, title, completed }) => { 58 | const { id: todoListID } = getTypeAndIDFromGlobalID(todoListGlobalID) 59 | 60 | const todoItem = await createTodoItem({ 61 | todoListID, 62 | title, 63 | completed, 64 | }) 65 | 66 | const payload = { 67 | todoItem, 68 | } 69 | 70 | pubsub.publish(TODO_ITEM_CREATED, { 71 | todoListID: todoListGlobalID, 72 | ...payload, 73 | }) 74 | 75 | return payload 76 | }, 77 | }) 78 | 79 | export default createTodoItemMutation 80 | -------------------------------------------------------------------------------- /src/client/TodoApp/components/TodoListItems.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import TodoItem from '../containers/TodoItem' 4 | 5 | export default class TodoListItems extends Component { 6 | static propTypes = { 7 | todoList: PropTypes.shape({ 8 | itemsCount: PropTypes.number.isRequired, 9 | completedItemsCount: PropTypes.number.isRequired, 10 | items: PropTypes.shape({ 11 | edges: PropTypes.arrayOf(PropTypes.shape({ 12 | node: PropTypes.shape({ 13 | id: PropTypes.string.isRequired, 14 | }).isRequired, 15 | }).isRequired).isRequired, 16 | }).isRequired, 17 | }).isRequired, 18 | onLoadMoreTriggered: PropTypes.func.isRequired, 19 | onMarkAllCompletedChangeValue: PropTypes.func.isRequired, 20 | } 21 | 22 | constructor(props, context) { 23 | super(props, context) 24 | 25 | this.state = { 26 | // refreshing: false, 27 | } 28 | } 29 | 30 | componentDidMount() { 31 | window.addEventListener('scroll', this._handleScroll) 32 | this._handleScroll() 33 | } 34 | 35 | componentWillUnmount() { 36 | window.removeEventListener('scroll', this._handleScroll) 37 | } 38 | 39 | refreshLayout = () => { 40 | this._handleScroll() 41 | } 42 | 43 | _handleScroll = () => { 44 | const scrollBottom = document.body.clientHeight - window.innerHeight - window.scrollY 45 | if (scrollBottom < 100) this.props.onLoadMoreTriggered(this._handleScroll) 46 | } 47 | 48 | _handleToggleAllChange = (e) => { 49 | this.props.onMarkAllCompletedChangeValue(e.target.checked) 50 | } 51 | 52 | render() { 53 | const { todoList } = this.props 54 | const { items } = todoList 55 | 56 | return ( 57 |
58 | 65 | 68 |
    69 | {items.edges.map(edge => )} 70 |
71 |
72 | ) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/server/mutations/updateAllItemsOnTodoListMutation.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { 4 | GraphQLNonNull, 5 | GraphQLList, 6 | GraphQLID, 7 | GraphQLString, 8 | GraphQLBoolean, 9 | } from 'graphql' 10 | import { mutationWithClientMutationId, toGlobalId } from 'graphql-relay' 11 | 12 | import { getTypeAndIDFromGlobalID } from '../relay' 13 | 14 | import todoListType from '../types/todoListType' 15 | import todoItemType from '../types/todoItemType' 16 | 17 | import { updateAllItemsOnTodoList } from '../data' 18 | 19 | import pubsub from '../subscriptions/pubsub' 20 | import { TODO_ITEMS_UPDATED } from '../subscriptions/pubsub/event-types' 21 | 22 | const updateAllItemsOnTodoListMutation = mutationWithClientMutationId({ 23 | name: 'UpdateAllItemsOnTodoList', 24 | inputFields: { 25 | todoListID: { 26 | type: new GraphQLNonNull(GraphQLID), 27 | }, 28 | title: { 29 | type: GraphQLString, 30 | }, 31 | completed: { 32 | type: GraphQLBoolean, 33 | }, 34 | }, 35 | outputFields: { 36 | todoList: { 37 | type: new GraphQLNonNull(todoListType), 38 | resolve: payload => payload.todoList, 39 | }, 40 | updatedTodoItemIDs: { 41 | type: new GraphQLNonNull(new GraphQLList(GraphQLID)), 42 | resolve: payload => payload.updatedTodoItemIDs, 43 | }, 44 | updatedTodoItems: { 45 | type: new GraphQLNonNull(new GraphQLList(todoItemType)), 46 | resolve: payload => payload.updatedTodoItems, 47 | }, 48 | }, 49 | mutateAndGetPayload: async ({ todoListID: todoListGlobalID, title, completed }) => { 50 | const { id: todoListID } = getTypeAndIDFromGlobalID(todoListGlobalID) 51 | 52 | const { todoList, updatedTodoItems, updatedTodoItemIDs } = await updateAllItemsOnTodoList(todoListID, { 53 | title, 54 | completed, 55 | }) 56 | 57 | const updatedTodoItemGlobalIDs = updatedTodoItemIDs.map(id => toGlobalId('TodoItem', id)) 58 | 59 | pubsub.publish(TODO_ITEMS_UPDATED, { 60 | todoListID: todoListGlobalID, 61 | todoList, 62 | updatedTodoItemIDs: updatedTodoItemGlobalIDs, 63 | updatedTodoItems, 64 | changes: { 65 | title, 66 | completed, 67 | }, 68 | }) 69 | 70 | return { 71 | todoList, 72 | updatedTodoItemIDs: updatedTodoItemGlobalIDs, 73 | updatedTodoItems, 74 | } 75 | }, 76 | }) 77 | 78 | export default updateAllItemsOnTodoListMutation 79 | -------------------------------------------------------------------------------- /src/client/TodoApp/containers/TodoListItemsWithFilter.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React, { Component } from 'react' 4 | import { graphql, createRefetchContainer } from 'react-relay' 5 | import type { RefetchContainerRelayProp } from 'react-relay' 6 | 7 | import TodoListItemsWithFilterComponent from '../components/TodoListItemsWithFilter' 8 | 9 | type Props = {| 10 | todoList: {}, 11 | relay: RefetchContainerRelayProp, 12 | |}; 13 | 14 | type State = {| 15 | filter: 'all' | 'active' | 'completed', 16 | |}; 17 | 18 | class TodoListItemsWithFilterContainer extends Component { 19 | /* eslint-disable react/sort-comp */ 20 | todoListItems: TodoListItemsWithFilterComponent; 21 | /* eslint-enable react/sort-comp */ 22 | 23 | constructor(props, context) { 24 | super(props, context) 25 | 26 | this.state = { 27 | filter: 'all', 28 | } 29 | } 30 | 31 | setFilter = (filter, callback) => { 32 | this.props.relay.refetch({ filter }, null, callback, { force: false }) 33 | this.setState({ filter }) 34 | } 35 | 36 | _handleFilterPress = (filter) => { 37 | this.setFilter(filter, () => { 38 | if ( 39 | this.todoListItems && 40 | this.todoListItems.refreshLayout 41 | ) { 42 | this.todoListItems.refreshLayout() 43 | } 44 | }) 45 | } 46 | 47 | render() { 48 | const { todoList } = this.props 49 | const { filter } = this.state 50 | 51 | return ( 52 | this.todoListItems = ref} 54 | todoList={todoList} 55 | filterValue={filter} 56 | onFilterPress={this._handleFilterPress} 57 | /> 58 | ) 59 | } 60 | } 61 | 62 | export default createRefetchContainer( 63 | TodoListItemsWithFilterContainer, 64 | graphql` 65 | fragment TodoListItemsWithFilter_todoList on TodoList 66 | @argumentDefinitions( 67 | filter: { type: "TodoListItemsFilterEnum", defaultValue: "all" } 68 | count: { type: "Int", defaultValue: 10 } 69 | ) { 70 | ...TodoListItems_todoList 71 | } 72 | `, 73 | graphql` 74 | query TodoListItemsWithFilterRefetchQuery( 75 | $todoListID: ID 76 | $filter: TodoListItemsFilterEnum! 77 | $count: Int! 78 | $cursor: String 79 | ) { 80 | viewer { 81 | todoList(id: $todoListID) { 82 | ...TodoListItemsWithFilter_todoList 83 | } 84 | } 85 | } 86 | `, 87 | ) 88 | -------------------------------------------------------------------------------- /src/server/types/todoListType.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { 4 | GraphQLNonNull, 5 | GraphQLInterfaceType, 6 | GraphQLObjectType, 7 | GraphQLEnumType, 8 | GraphQLID, 9 | GraphQLInt, 10 | GraphQLString, 11 | } from 'graphql' 12 | import { 13 | toGlobalId, 14 | globalIdField, 15 | connectionDefinitions, 16 | connectionArgs, 17 | } from 'graphql-relay' 18 | 19 | import { nodeInterface } from '../relay' 20 | 21 | import getType from './_getType' 22 | import todoItemType from './todoItemType' 23 | 24 | import { 25 | getUserFromTodoList, 26 | getItemsFromTodoList, 27 | getItemsCountFromTodoList, 28 | getActiveItemsCountFromTodoList, 29 | getCompletedItemsCountFromTodoList, 30 | connectionFor, 31 | } from '../data' 32 | 33 | export const { connectionType: todoListItemsConnectionType } = connectionDefinitions({ nodeType: todoItemType }) 34 | export const todoListItemsConnectionEdgeType: GraphQLInterfaceType = 35 | // $FlowFixMe 36 | todoListItemsConnectionType.getFields().edges.type.ofType 37 | 38 | const todoListType = new GraphQLObjectType({ 39 | name: 'TodoList', 40 | interfaces: [nodeInterface], 41 | fields: () => ({ 42 | id: globalIdField(), 43 | name: { 44 | type: new GraphQLNonNull(GraphQLString), 45 | }, 46 | userID: { 47 | type: new GraphQLNonNull(GraphQLID), 48 | resolve: todoList => toGlobalId('User', todoList.userID), 49 | }, 50 | user: { 51 | type: new GraphQLNonNull(getType('userType')), 52 | resolve: getUserFromTodoList, 53 | }, 54 | items: { 55 | type: new GraphQLNonNull(todoListItemsConnectionType), 56 | args: { 57 | ...connectionArgs, 58 | filter: { 59 | type: new GraphQLEnumType({ 60 | name: 'TodoListItemsFilterEnum', 61 | values: { 62 | all: { value: 'all' }, 63 | active: { value: 'active' }, 64 | completed: { value: 'completed' }, 65 | }, 66 | }), 67 | }, 68 | }, 69 | resolve: async (todoList, args) => { 70 | const { filter } = args 71 | const todoItems = await getItemsFromTodoList(todoList, { filter }) 72 | return connectionFor( 73 | // $FlowFixMe 74 | todoItems, 75 | args, 76 | ) 77 | }, 78 | }, 79 | itemsCount: { 80 | type: new GraphQLNonNull(GraphQLInt), 81 | resolve: getItemsCountFromTodoList, 82 | }, 83 | activeItemsCount: { 84 | type: new GraphQLNonNull(GraphQLInt), 85 | resolve: getActiveItemsCountFromTodoList, 86 | }, 87 | completedItemsCount: { 88 | type: new GraphQLNonNull(GraphQLInt), 89 | resolve: getCompletedItemsCountFromTodoList, 90 | }, 91 | }), 92 | }) 93 | 94 | export default todoListType 95 | -------------------------------------------------------------------------------- /src/client/TodoApp/mutations/UpdateTodoItemMutation.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { graphql } from 'react-relay' 4 | import type { RecordSourceSelectorProxy } from 'relay-runtime' 5 | import validate from 'validate.js' 6 | 7 | import Mutation from './_Mutation' 8 | 9 | import todoItemUpdatedUpdater from '../updaters/todoItemUpdatedUpdater' 10 | 11 | import type { UpdateTodoItemMutationVariables } from './__generated__/UpdateTodoItemMutation.graphql' 12 | 13 | export type UpdateTodoItemInput = $Exact<$PropertyType>; 14 | 15 | export type UpdateTodoItemOptions = {| 16 | todoListID: string, 17 | |}; 18 | 19 | export default class UpdateTodoItemMutation extends Mutation { 20 | static mutation = graphql` 21 | mutation UpdateTodoItemMutation($input: UpdateTodoItemInput!) { 22 | updateTodoItem(input: $input) { 23 | todoItem { 24 | id 25 | title 26 | completed 27 | } 28 | todoList { 29 | id 30 | completedItemsCount 31 | activeItemsCount 32 | } 33 | } 34 | } 35 | ` 36 | 37 | static constraints = { 38 | // TODO: add async validation to ensure todo item with the id exists 39 | todoItemID: { 40 | presence: true, 41 | }, 42 | title: (value: string) => { 43 | // Ensure the title is not a blank string 44 | if (!validate.isDefined(value)) return {} 45 | 46 | return { 47 | presence: { allowEmpty: false }, 48 | } 49 | }, 50 | } 51 | 52 | getMutationConfig() { 53 | const { input, options } = this 54 | 55 | const optimisticResponsePayload = { 56 | todoItem: { 57 | id: input.todoItemID, 58 | title: input.title, 59 | completed: input.completed, 60 | }, 61 | } 62 | 63 | return { 64 | optimisticResponse: { 65 | updateTodoItem: optimisticResponsePayload, 66 | }, 67 | updater: (store: RecordSourceSelectorProxy) => { 68 | const payload = store.getRootField('updateTodoItem') 69 | if (!payload) throw new Error('Cannot get updateTodoItem') 70 | const todoItem = payload.getLinkedRecord('todoItem') 71 | if (!todoItem) throw new Error('Cannot get todoItem') 72 | const todoList = payload.getLinkedRecord('todoList') 73 | if (!todoList) throw new Error('Cannot get todoList') 74 | 75 | todoItemUpdatedUpdater(store, { 76 | todoItem, 77 | todoList, 78 | }) 79 | }, 80 | optimisticUpdater: (store: RecordSourceSelectorProxy) => { 81 | if (!options.todoListID) return 82 | const todoList = store.get(options.todoListID) 83 | if (!todoList) throw new Error('Cannot get todoList') 84 | const todoItem = store.get(input.todoItemID) 85 | if (!todoItem) throw new Error('Cannot get todoItem') 86 | 87 | todoItemUpdatedUpdater(store, { 88 | todoList, 89 | todoItem, 90 | }) 91 | }, 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/client/TodoApp/components/TodoItem.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, { Component } from 'react' 4 | 5 | const ENTER_KEY_CODE = 13 6 | 7 | type Props = {| 8 | todoItem: { 9 | id: string, 10 | completed: boolean, 11 | title: string, 12 | }, 13 | onCompletedChangeValue: (value?: boolean) => any, 14 | editTitleValue: string, 15 | onEditTitleChangeText: (value?: string) => any, 16 | onSubmitTitleEditing: () => any, 17 | onRemovePress: () => any, 18 | |}; 19 | 20 | type State = {| 21 | editing: boolean, 22 | |}; 23 | 24 | export default class TodoItem extends Component { 25 | state = { 26 | editing: false, 27 | } 28 | 29 | editNameInput: ?HTMLInputElement; 30 | 31 | _handleToggleChange = (e: SyntheticInputEvent) => { 32 | this.props.onCompletedChangeValue(e.target.checked) 33 | } 34 | 35 | _handleLabelDoubleClick = () => { 36 | this.setState({ editing: true }, () => { 37 | if (!this.editNameInput) return 38 | this.editNameInput.focus() 39 | if (!this.editNameInput) return 40 | const l = this.editNameInput.value.length 41 | this.editNameInput.setSelectionRange(l, l) 42 | }) 43 | } 44 | 45 | _handleEditTitleChange = (e: SyntheticInputEvent) => { 46 | this.props.onEditTitleChangeText(e.target.value) 47 | } 48 | 49 | _handleEditTitleKeyDown = (e: SyntheticInputEvent) => { 50 | if (e.keyCode === ENTER_KEY_CODE) { 51 | this.props.onSubmitTitleEditing() 52 | this.setState({ editing: false }) 53 | } 54 | } 55 | 56 | _handleEditTitleBlur = () => { 57 | this.props.onSubmitTitleEditing() 58 | this.setState({ editing: false }) 59 | } 60 | 61 | render() { 62 | const { 63 | todoItem, 64 | editTitleValue, 65 | onRemovePress, 66 | } = this.props 67 | const { editing } = this.state 68 | 69 | return ( 70 |
  • el).join(' ')} 75 | > 76 |
    77 | 83 | 88 |
    93 | this.editNameInput = ref} 95 | className="edit" 96 | onChange={this._handleEditTitleChange} 97 | onKeyDown={this._handleEditTitleKeyDown} 98 | onBlur={this._handleEditTitleBlur} 99 | value={editTitleValue} 100 | /> 101 |
  • 102 | ) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/client/TodoApp/subscriptions/_Subscription.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { requestSubscription } from 'react-relay' 4 | import type { RequestSubscriptionConfig } from 'react-relay' 5 | import type { RelayEnvironment, Disposable } from 'relay-runtime' 6 | 7 | /** 8 | * Type defination of a typical variables object for Subscriptions. 9 | */ 10 | export type Variables = {| 11 | [string]: any, 12 | |}; 13 | 14 | /** 15 | * A base abstract class for Subscriptions. 16 | */ 17 | export default class Subscription { 18 | /** 19 | * Default variables 20 | */ 21 | static defaultVariables = ({}: $Shape) 22 | 23 | /** 24 | * The GraphQL subscription query 25 | */ 26 | static subscription = (undefined: any) 27 | 28 | /** 29 | * Subscription configurations 30 | * @see {@link https://facebook.github.io/relay/docs/subscriptions.html|Relay Subscriptions} 31 | */ 32 | getSubscriptionConfig(): $Shape { 33 | return (this.constructor.subscriptionConfig) 34 | } 35 | 36 | static subscriptionConfig = ({}: $Shape) 37 | 38 | _variables: $Shape 39 | _environment: RelayEnvironment 40 | 41 | _subscriptionDisposable: Disposable | void 42 | 43 | /** 44 | * Constructor of a new Subscription 45 | * 46 | * @param {RelayEnvironment} environment - The Relay Environment. 47 | * @param {Variables} variables - An object that contains the variables of subscription. 48 | */ 49 | constructor(environment: RelayEnvironment, variables?: $Shape) { 50 | const { defaultVariables } = this.constructor 51 | 52 | this._environment = environment 53 | this._variables = Object.freeze({ 54 | ...defaultVariables, 55 | ...variables, 56 | }) 57 | } 58 | 59 | /** 60 | * Getter of the Relay Environment. 61 | * 62 | * $FlowFixMe 63 | */ 64 | get environment(): RelayEnvironment { 65 | return this._environment 66 | } 67 | 68 | /** 69 | * Getter of the variables object. 70 | * 71 | * $FlowFixMe 72 | */ 73 | get variables(): $Shape { 74 | return this._variables 75 | } 76 | 77 | 78 | /** 79 | * Subscribe the subscription. 80 | */ 81 | subscribe = (): Promise => { 82 | const { subscription } = this.constructor 83 | const { environment, variables } = this 84 | 85 | const subscriptionConfig = this.getSubscriptionConfig() 86 | 87 | return new Promise((resolve, reject) => { 88 | this._subscriptionDisposable = requestSubscription( 89 | environment, 90 | { 91 | subscription, 92 | ...subscriptionConfig, 93 | variables, 94 | onCompleted: () => resolve(), 95 | onError: () => reject(), 96 | }, 97 | ) 98 | }) 99 | } 100 | 101 | /** 102 | * Unsubscribe the subscription. 103 | */ 104 | unsubscribe = (): boolean => { 105 | if (!this._subscriptionDisposable) return false 106 | this._subscriptionDisposable.dispose() 107 | this._subscriptionDisposable = undefined 108 | return true 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/client/TodoApp/mutations/CreateTodoItemMutation.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { graphql } from 'react-relay' 4 | import type { RecordSourceSelectorProxy } from 'relay-runtime' 5 | import uuidv4 from 'uuid/v4' 6 | 7 | import Mutation from './_Mutation' 8 | 9 | import todoItemCreatedUpdater from '../updaters/todoItemCreatedUpdater' 10 | 11 | import type { CreateTodoItemMutationVariables } from './__generated__/CreateTodoItemMutation.graphql' 12 | 13 | export type CreateTodoItemInput = $Exact<$PropertyType>; 14 | 15 | export default class CreateTodoItemMutation extends Mutation { 16 | static mutation = graphql` 17 | mutation CreateTodoItemMutation($input: CreateTodoItemInput!) { 18 | createTodoItem(input: $input) { 19 | todoListItemsConnectionEdge { 20 | cursor 21 | node { 22 | completed 23 | id 24 | title 25 | } 26 | } 27 | todoList { 28 | id 29 | itemsCount 30 | completedItemsCount 31 | activeItemsCount 32 | } 33 | } 34 | } 35 | ` 36 | 37 | static constraints = { 38 | title: { 39 | presence: { allowEmpty: false }, 40 | }, 41 | } 42 | 43 | getMutationConfig() { 44 | const { input } = this 45 | 46 | return { 47 | updater: (store: RecordSourceSelectorProxy) => { 48 | const payload = store.getRootField('createTodoItem') 49 | if (!payload) throw new Error('Cannot get createTodoItem') 50 | const todoListItemsConnectionEdge = payload.getLinkedRecord('todoListItemsConnectionEdge') 51 | if (!todoListItemsConnectionEdge) throw new Error('Cannot get todoListItemsConnectionEdge') 52 | const todoList = payload.getLinkedRecord('todoList') 53 | if (!todoList) throw new Error('Cannot get todoList') 54 | 55 | todoItemCreatedUpdater(store, { 56 | todoList, 57 | todoListItemsConnectionEdge, 58 | }) 59 | }, 60 | optimisticUpdater: (store: RecordSourceSelectorProxy) => { 61 | const todoList = store.get(input.todoListID) 62 | if (!todoList) throw new Error('Cannot get TodoList from store') 63 | 64 | const { temporaryTodoItemEdge } = this._generateTemporaryRecord(store, input) 65 | 66 | todoItemCreatedUpdater(store, { 67 | todoList, 68 | todoListItemsConnectionEdge: temporaryTodoItemEdge, 69 | }) 70 | }, 71 | } 72 | } 73 | 74 | _generateTemporaryRecord(store: RecordSourceSelectorProxy, input: CreateTodoItemInput) { 75 | const temporaryTodoItemID = `client:temporaryTodoItem:${uuidv4()}` 76 | const temporaryTodoItem = store.create(temporaryTodoItemID, 'TodoItem') 77 | temporaryTodoItem.setValue(temporaryTodoItemID, 'id') 78 | temporaryTodoItem.setValue(input.title, 'title') 79 | temporaryTodoItem.setValue(input.completed || false, 'completed') 80 | 81 | const temporaryTodoItemEdge = store.create( 82 | `client:temporaryTodoItemEdge:${uuidv4()}`, 83 | 'TodoItemEdge', 84 | ) 85 | temporaryTodoItemEdge.setLinkedRecord(temporaryTodoItem, 'node') 86 | 87 | return { 88 | temporaryTodoItem, 89 | temporaryTodoItemEdge, 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/client/TodoApp/subscriptions/ItemOnTodoListCreatedSubscription.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { graphql } from 'react-relay' 4 | import type { RecordSourceSelectorProxy, RelayRecordProxy } from 'relay-runtime' 5 | import uuidv4 from 'uuid/v4' 6 | 7 | import Subscription from './_Subscription' 8 | 9 | import todoItemCreatedUpdater from '../updaters/todoItemCreatedUpdater' 10 | 11 | import type { ItemOnTodoListCreatedSubscriptionVariables } 12 | from './__generated__/ItemOnTodoListCreatedSubscription.graphql' 13 | 14 | export default class ItemOnTodoListCreatedSubscription 15 | extends Subscription { 16 | static subscription = graphql` 17 | subscription ItemOnTodoListCreatedSubscription( 18 | $todoListID: ID! 19 | ) { 20 | itemOnTodoListCreated(todoListID: $todoListID) { 21 | todoItem { 22 | id 23 | completed 24 | title 25 | listID 26 | } 27 | todoListItemsConnectionEdge { 28 | cursor 29 | node { 30 | id 31 | } 32 | } 33 | todoList { 34 | id 35 | itemsCount 36 | completedItemsCount 37 | activeItemsCount 38 | } 39 | } 40 | } 41 | ` 42 | 43 | getSubscriptionConfig() { 44 | const { todoListID } = this.variables 45 | 46 | return { 47 | updater: (store: RecordSourceSelectorProxy) => { 48 | const payload = store.getRootField('itemOnTodoListCreated') 49 | if (!payload) throw new Error('cannot get itemOnTodoListCreated') 50 | const todoItem = payload.getLinkedRecord('todoItem') 51 | if (!todoItem) throw new Error('cannot get todoItem') 52 | const todoList = store.get(todoListID) 53 | if (!todoList) throw new Error('cannot get todoList') 54 | const todoListItemsConnectionEdge = payload.getLinkedRecord('todoListItemsConnectionEdge') 55 | if (!todoListItemsConnectionEdge) throw new Error('cannot get todoListItemsConnectionEdge') 56 | 57 | todoItemCreatedUpdater(store, { 58 | todoList, 59 | todoListItemsConnectionEdge, 60 | }) 61 | 62 | this._updateItemsCounts(store, todoList, todoItem) 63 | }, 64 | } 65 | } 66 | 67 | _updateItemsCounts( 68 | store: RecordSourceSelectorProxy, 69 | todoList: RelayRecordProxy, 70 | todoItem: RelayRecordProxy, 71 | ) { 72 | const currentItemsCount = todoList.getValue('todoItemsCount') 73 | if (typeof currentItemsCount === 'number') { 74 | todoList.setValue( 75 | currentItemsCount + 1, 76 | 'todoItemsCount', 77 | ) 78 | } 79 | 80 | const completed = todoItem.getValue('completed') 81 | const currentCompletedItemsCount = todoList.getValue('completedTodoItemsCount') 82 | const currentActiveItemsCount = todoList.getValue('activeTodoItemsCount') 83 | 84 | if (completed && typeof currentCompletedItemsCount === 'number') { 85 | todoList.setValue( 86 | currentCompletedItemsCount + 1, 87 | 'completedTodoItemsCount', 88 | ) 89 | } else if (typeof currentActiveItemsCount === 'number') { 90 | todoList.setValue( 91 | currentActiveItemsCount + 1, 92 | 'activeTodoItemsCount', 93 | ) 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/client/TodoApp/containers/TodoItem.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React, { Component } from 'react' 4 | import { graphql, createFragmentContainer } from 'react-relay' 5 | import environment from '../relay/environment' 6 | 7 | import TodoItemComponent from '../components/TodoItem' 8 | 9 | import UpdateTodoItemMutation from '../mutations/UpdateTodoItemMutation' 10 | import DeleteTodoItemMutation from '../mutations/DeleteTodoItemMutation' 11 | 12 | type Props = {| 13 | todoItem: { 14 | id: string, 15 | listID: string, 16 | completed: boolean, 17 | title: string, 18 | }, 19 | |}; 20 | 21 | type State = {| 22 | updateTitleMutation: UpdateTodoItemMutation, 23 | |}; 24 | 25 | class TodoItemContainer extends Component { 26 | constructor(props, context) { 27 | super(props, context) 28 | 29 | const { title } = props.todoItem 30 | this.state = { 31 | updateTitleMutation: this._getNewUpdateTodoItemMutation({ title }), 32 | } 33 | } 34 | 35 | _getNewUpdateTodoItemMutation = (input) => { 36 | const { todoItem } = this.props 37 | 38 | return new UpdateTodoItemMutation(environment, { 39 | todoItemID: todoItem.id, 40 | ...input, 41 | }, { 42 | todoListID: todoItem.listID, 43 | }) 44 | } 45 | 46 | _handleCompletedChangeValue = (completed) => { 47 | const mutation = this._getNewUpdateTodoItemMutation({ completed }) 48 | mutation.commit() 49 | } 50 | 51 | _handleEditTitleChangeText = (title) => { 52 | const { updateTitleMutation: prevMutation } = this.state 53 | const updateTitleMutation = UpdateTodoItemMutation.updateInput(prevMutation, { title }) 54 | this.setState({ updateTitleMutation }) 55 | } 56 | 57 | _handleSubmitTitleEditing = () => { 58 | const { updateTitleMutation } = this.state 59 | 60 | if (updateTitleMutation.isValid) { 61 | updateTitleMutation.commit() 62 | } else { 63 | const { title } = this.props.todoItem 64 | this.setState({ 65 | updateTitleMutation: this._getNewUpdateTodoItemMutation({ title }), 66 | }) 67 | } 68 | } 69 | 70 | _getNewDeleteTodoItemMutation = () => { 71 | const { todoItem } = this.props 72 | 73 | return new DeleteTodoItemMutation(environment, { 74 | todoItemID: todoItem.id, 75 | }, { 76 | todoListID: todoItem.listID, 77 | }) 78 | } 79 | 80 | _handleRemovePress = () => { 81 | const mutation = this._getNewDeleteTodoItemMutation() 82 | mutation.commit() 83 | } 84 | 85 | render() { 86 | const { updateTitleMutation } = this.state 87 | 88 | return ( 89 | 97 | ) 98 | } 99 | } 100 | 101 | export default createFragmentContainer( 102 | TodoItemContainer, 103 | graphql` 104 | fragment TodoItem_todoItem on TodoItem { 105 | id 106 | listID 107 | completed 108 | title 109 | } 110 | `, 111 | ) 112 | -------------------------------------------------------------------------------- /src/client/TodoApp/mutations/DeleteCompletedItemsOnTodoListMutation.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { graphql } from 'react-relay' 4 | import { ConnectionHandler } from 'relay-runtime' 5 | import type { RecordSourceSelectorProxy, RelayRecordProxy } from 'relay-runtime' 6 | 7 | import Mutation from './_Mutation' 8 | 9 | import todoListItemsConnectionNames from '../registrations/todoListItemsConnectionNames' 10 | import todoItemsDeletedUpdater from '../updaters/todoItemsDeletedUpdater' 11 | 12 | import type { DeleteCompletedItemsOnTodoListMutationVariables } 13 | from './__generated__/DeleteCompletedItemsOnTodoListMutation.graphql' 14 | 15 | export type DeleteCompletedItemsOnTodoListInput = 16 | $Exact<$PropertyType>; 17 | 18 | export default class DeleteCompletedItemsOnTodoListMutation extends Mutation { 19 | static mutation = graphql` 20 | mutation DeleteCompletedItemsOnTodoListMutation($input: DeleteCompletedItemsOnTodoListInput!) { 21 | deleteCompletedItemsOnTodoList(input: $input) { 22 | deletedTodoItemIDs 23 | todoList { 24 | id 25 | itemsCount 26 | completedItemsCount 27 | } 28 | } 29 | } 30 | ` 31 | 32 | static constraints = { 33 | // TODO: add async validation to ensure todo list with the id exists 34 | todoListID: { 35 | presence: true, 36 | }, 37 | } 38 | 39 | getMutationConfig() { 40 | const { input } = this 41 | 42 | return { 43 | updater: (store: RecordSourceSelectorProxy) => { 44 | const payload = store.getRootField('deleteCompletedItemsOnTodoList') 45 | if (!payload) throw new Error('Cannot get deleteCompletedItemsOnTodoList') 46 | const todoList = payload.getLinkedRecord('todoList') 47 | if (!todoList) throw new Error('Cannot get todoList') 48 | const deletedTodoItemIDs = payload.getValue('deletedTodoItemIDs') 49 | if (!deletedTodoItemIDs) throw new Error('Cannot get deletedTodoItemIDs') 50 | 51 | todoItemsDeletedUpdater(store, { 52 | todoList, 53 | deletedTodoItemIDs, 54 | }) 55 | }, 56 | optimisticUpdater: (store: RecordSourceSelectorProxy) => { 57 | const todoList = store.get(input.todoListID) 58 | if (!todoList) throw new Error('Cannot get todoList') 59 | 60 | todoList.setValue(0, 'completedItemsCount') 61 | 62 | const deletedTodoItemIDs = this._getDeletedTodoItemIDsFromTodoList(store, todoList) 63 | 64 | todoItemsDeletedUpdater(store, { 65 | todoList, 66 | deletedTodoItemIDs, 67 | }) 68 | }, 69 | } 70 | } 71 | 72 | _getDeletedTodoItemIDsFromTodoList( 73 | store: RecordSourceSelectorProxy, 74 | todoList: RelayRecordProxy, 75 | ) { 76 | const deletedTodoItemIDs: Set = new Set() 77 | 78 | todoListItemsConnectionNames.forEach((connName) => { 79 | ['all', 'completed'].forEach((filter) => { 80 | const conn = ConnectionHandler.getConnection( 81 | todoList, 82 | connName, 83 | { filter }, 84 | ) 85 | if (!conn) return 86 | 87 | let nodes = conn.getLinkedRecords('edges') 88 | .map(edge => edge.getLinkedRecord('node')) 89 | 90 | if (filter !== 'completed') { 91 | nodes = nodes.filter(node => node.getValue('completed')) 92 | } 93 | 94 | nodes 95 | .map(node => node.getValue('id')) 96 | .filter(id => id) 97 | .forEach(id => deletedTodoItemIDs.add(id)) 98 | }) 99 | }) 100 | 101 | return deletedTodoItemIDs 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/client/TodoApp/__generated__/TodoListSelectQuery.graphql.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | * @relayHash 45358bd7873c8c0d3f2104991564a001 4 | */ 5 | 6 | /* eslint-disable */ 7 | 8 | 'use strict'; 9 | 10 | /*:: 11 | import type {ConcreteBatch} from 'relay-runtime'; 12 | export type TodoListSelectQueryResponse = {| 13 | +viewer: ?{| |}; 14 | |}; 15 | */ 16 | 17 | 18 | /* 19 | query TodoListSelectQuery { 20 | viewer { 21 | ...TodoListSelect_user 22 | id 23 | } 24 | } 25 | 26 | fragment TodoListSelect_user on User { 27 | todoLists { 28 | edges { 29 | node { 30 | id 31 | name 32 | } 33 | } 34 | } 35 | } 36 | */ 37 | 38 | const batch /*: ConcreteBatch*/ = { 39 | "fragment": { 40 | "argumentDefinitions": [], 41 | "kind": "Fragment", 42 | "metadata": null, 43 | "name": "TodoListSelectQuery", 44 | "selections": [ 45 | { 46 | "kind": "LinkedField", 47 | "alias": null, 48 | "args": null, 49 | "concreteType": "User", 50 | "name": "viewer", 51 | "plural": false, 52 | "selections": [ 53 | { 54 | "kind": "FragmentSpread", 55 | "name": "TodoListSelect_user", 56 | "args": null 57 | } 58 | ], 59 | "storageKey": null 60 | } 61 | ], 62 | "type": "Query" 63 | }, 64 | "id": null, 65 | "kind": "Batch", 66 | "metadata": {}, 67 | "name": "TodoListSelectQuery", 68 | "query": { 69 | "argumentDefinitions": [], 70 | "kind": "Root", 71 | "name": "TodoListSelectQuery", 72 | "operation": "query", 73 | "selections": [ 74 | { 75 | "kind": "LinkedField", 76 | "alias": null, 77 | "args": null, 78 | "concreteType": "User", 79 | "name": "viewer", 80 | "plural": false, 81 | "selections": [ 82 | { 83 | "kind": "LinkedField", 84 | "alias": null, 85 | "args": null, 86 | "concreteType": "TodoListConnection", 87 | "name": "todoLists", 88 | "plural": false, 89 | "selections": [ 90 | { 91 | "kind": "LinkedField", 92 | "alias": null, 93 | "args": null, 94 | "concreteType": "TodoListEdge", 95 | "name": "edges", 96 | "plural": true, 97 | "selections": [ 98 | { 99 | "kind": "LinkedField", 100 | "alias": null, 101 | "args": null, 102 | "concreteType": "TodoList", 103 | "name": "node", 104 | "plural": false, 105 | "selections": [ 106 | { 107 | "kind": "ScalarField", 108 | "alias": null, 109 | "args": null, 110 | "name": "id", 111 | "storageKey": null 112 | }, 113 | { 114 | "kind": "ScalarField", 115 | "alias": null, 116 | "args": null, 117 | "name": "name", 118 | "storageKey": null 119 | } 120 | ], 121 | "storageKey": null 122 | } 123 | ], 124 | "storageKey": null 125 | } 126 | ], 127 | "storageKey": null 128 | }, 129 | { 130 | "kind": "ScalarField", 131 | "alias": null, 132 | "args": null, 133 | "name": "id", 134 | "storageKey": null 135 | } 136 | ], 137 | "storageKey": null 138 | } 139 | ] 140 | }, 141 | "text": "query TodoListSelectQuery {\n viewer {\n ...TodoListSelect_user\n id\n }\n}\n\nfragment TodoListSelect_user on User {\n todoLists {\n edges {\n node {\n id\n name\n }\n }\n }\n}\n" 142 | }; 143 | 144 | module.exports = batch; 145 | -------------------------------------------------------------------------------- /src/client/TodoApp/mutations/UpdateAllItemsOnTodoListMutation.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { graphql } from 'react-relay' 4 | import { ConnectionHandler } from 'relay-runtime' 5 | import type { RecordSourceSelectorProxy, RelayRecordProxy } from 'relay-runtime' 6 | 7 | import Mutation from './_Mutation' 8 | 9 | import todoItemsUpdatedUpdater from '../updaters/todoItemsUpdatedUpdater' 10 | import todoListItemsConnectionNames from '../registrations/todoListItemsConnectionNames' 11 | 12 | import type { UpdateAllItemsOnTodoListMutationVariables } 13 | from './__generated__/UpdateAllItemsOnTodoListMutation.graphql' 14 | 15 | export type UpdateAllItemsOnTodoListInput = $Exact<$PropertyType>; 16 | 17 | export default class UpdateAllItemsOnTodoListMutation extends Mutation { 18 | static mutation = graphql` 19 | mutation UpdateAllItemsOnTodoListMutation($input: UpdateAllItemsOnTodoListInput!) { 20 | updateAllItemsOnTodoList(input: $input) { 21 | todoList { 22 | id 23 | completedItemsCount 24 | activeItemsCount 25 | } 26 | updatedTodoItemIDs 27 | } 28 | } 29 | ` 30 | 31 | static constraints = { 32 | // TODO: add async validation to ensure todo list with the id exists 33 | todoListID: { 34 | presence: true, 35 | }, 36 | } 37 | 38 | getMutationConfig() { 39 | const { input } = this 40 | 41 | return { 42 | updater: (store: RecordSourceSelectorProxy) => { 43 | const payload = store.getRootField('updateAllItemsOnTodoList') 44 | if (!payload) throw new Error('Cannot get updateAllItemsOnTodoList') 45 | const updatedTodoItemIDs = payload.getValue('updatedTodoItemIDs') 46 | 47 | todoItemsUpdatedUpdater(store, { 48 | updatedTodoItemIDs, 49 | changes: { 50 | title: (typeof input.title === 'string') ? input.title : undefined, 51 | completed: (typeof input.completed === 'boolean') ? input.completed : undefined, 52 | }, 53 | }) 54 | }, 55 | optimisticUpdater: (store: RecordSourceSelectorProxy) => { 56 | const todoList = store.get(input.todoListID) 57 | if (!todoList) return 58 | 59 | this._optimisticallyUpdateTodoListItemsCounts(store, todoList, input) 60 | 61 | const updatedTodoItemIDs = this._getUpdatedTodoItemIDsFromTodoList(store, todoList) 62 | 63 | todoItemsUpdatedUpdater(store, { 64 | updatedTodoItemIDs, 65 | changes: { 66 | title: (typeof input.title === 'string') ? input.title : undefined, 67 | completed: (typeof input.completed === 'boolean') ? input.completed : undefined, 68 | }, 69 | }) 70 | }, 71 | } 72 | } 73 | 74 | _optimisticallyUpdateTodoListItemsCounts( 75 | store: RecordSourceSelectorProxy, 76 | todoList: RelayRecordProxy, 77 | input: UpdateAllItemsOnTodoListInput, 78 | ) { 79 | if (typeof input.completed !== 'boolean') return 80 | 81 | const itemsCount = todoList.getValue('itemsCount') 82 | 83 | if (input.completed) { 84 | todoList.setValue(0, 'activeItemsCount') 85 | if (typeof itemsCount === 'number') todoList.setValue(itemsCount, 'completedItemsCount') 86 | } else { 87 | todoList.setValue(0, 'completedItemsCount') 88 | if (typeof itemsCount === 'number') todoList.setValue(itemsCount, 'activeItemsCount') 89 | } 90 | } 91 | 92 | _getUpdatedTodoItemIDsFromTodoList( 93 | store: RecordSourceSelectorProxy, 94 | todoList: RelayRecordProxy, 95 | ) { 96 | const updatedTodoItemIDs: Set = new Set() 97 | 98 | todoListItemsConnectionNames.forEach((connName) => { 99 | ['all', 'active', 'completed'].forEach((filter) => { 100 | const conn = ConnectionHandler.getConnection( 101 | todoList, 102 | connName, 103 | { filter }, 104 | ) 105 | if (!conn) return 106 | conn.getLinkedRecords('edges') 107 | .map(edge => edge.getLinkedRecord('node').getValue('id')) 108 | .filter(id => id) 109 | .forEach(id => updatedTodoItemIDs.add(id)) 110 | }) 111 | }) 112 | 113 | return updatedTodoItemIDs 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/vendor/express-graphql/parseBody.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /** 3 | * Copyright (c) 2015, Facebook, Inc. 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the BSD-style license found in the 7 | * LICENSE file in the root directory of this source tree. An additional grant 8 | * of patent rights can be found in the PATENTS file in the same directory. 9 | */ 10 | 11 | import contentType from 'content-type'; 12 | import getBody from 'raw-body'; 13 | import httpError from 'http-errors'; 14 | import querystring from 'querystring'; 15 | import zlib from 'zlib'; 16 | 17 | import type { $Request } from 'express'; 18 | 19 | /** 20 | * Provided a "Request" provided by express or connect (typically a node style 21 | * HTTPClientRequest), Promise the body data contained. 22 | */ 23 | export function parseBody(req: $Request): Promise<{ [param: string]: mixed }> { 24 | return new Promise((resolve, reject) => { 25 | const body = req.body; 26 | 27 | // If express has already parsed a body as a keyed object, use it. 28 | if (typeof body === 'object' && !(body instanceof Buffer)) { 29 | return resolve((body: any)); 30 | } 31 | 32 | // Skip requests without content types. 33 | if (req.headers['content-type'] === undefined) { 34 | return resolve({}); 35 | } 36 | 37 | const typeInfo = contentType.parse(req); 38 | 39 | // If express has already parsed a body as a string, and the content-type 40 | // was application/graphql, parse the string body. 41 | if (typeof body === 'string' && typeInfo.type === 'application/graphql') { 42 | return resolve(graphqlParser(body)); 43 | } 44 | 45 | // Already parsed body we didn't recognise? Parse nothing. 46 | if (body) { 47 | return resolve({}); 48 | } 49 | 50 | // Use the correct body parser based on Content-Type header. 51 | switch (typeInfo.type) { 52 | case 'application/graphql': 53 | return read(req, typeInfo, graphqlParser, resolve, reject); 54 | case 'application/json': 55 | return read(req, typeInfo, jsonEncodedParser, resolve, reject); 56 | case 'application/x-www-form-urlencoded': 57 | return read(req, typeInfo, urlEncodedParser, resolve, reject); 58 | } 59 | 60 | // If no Content-Type header matches, parse nothing. 61 | return resolve({}); 62 | }); 63 | } 64 | 65 | function jsonEncodedParser(body) { 66 | if (jsonObjRegex.test(body)) { 67 | /* eslint-disable no-empty */ 68 | try { 69 | return JSON.parse(body); 70 | } catch (error) { 71 | // Do nothing 72 | } 73 | /* eslint-enable no-empty */ 74 | } 75 | throw httpError(400, 'POST body sent invalid JSON.'); 76 | } 77 | 78 | function urlEncodedParser(body) { 79 | return querystring.parse(body); 80 | } 81 | 82 | function graphqlParser(body) { 83 | return { query: body }; 84 | } 85 | 86 | /** 87 | * RegExp to match an Object-opening brace "{" as the first non-space 88 | * in a string. Allowed whitespace is defined in RFC 7159: 89 | * 90 | * x20 Space 91 | * x09 Horizontal tab 92 | * x0A Line feed or New line 93 | * x0D Carriage return 94 | */ 95 | const jsonObjRegex = /^[\x20\x09\x0a\x0d]*\{/; 96 | 97 | // Read and parse a request body. 98 | function read(req, typeInfo, parseFn, resolve, reject) { 99 | const charset = (typeInfo.parameters.charset || 'utf-8').toLowerCase(); 100 | 101 | // Assert charset encoding per JSON RFC 7159 sec 8.1 102 | if (charset.slice(0, 4) !== 'utf-') { 103 | throw httpError(415, `Unsupported charset "${charset.toUpperCase()}".`); 104 | } 105 | 106 | // Get content-encoding (e.g. gzip) 107 | const contentEncoding = req.headers['content-encoding']; 108 | const encoding = 109 | typeof contentEncoding === 'string' 110 | ? contentEncoding.toLowerCase() 111 | : 'identity'; 112 | const length = encoding === 'identity' ? req.headers['content-length'] : null; 113 | const limit = 100 * 1024; // 100kb 114 | const stream = decompressed(req, encoding); 115 | 116 | // Read body from stream. 117 | getBody(stream, { encoding: charset, length, limit }, (err, body) => { 118 | if (err) { 119 | return reject( 120 | err.type === 'encoding.unsupported' 121 | ? httpError(415, `Unsupported charset "${charset.toUpperCase()}".`) 122 | : httpError(400, `Invalid body: ${err.message}.`), 123 | ); 124 | } 125 | 126 | try { 127 | // Decode and parse body. 128 | return resolve(parseFn(body)); 129 | } catch (error) { 130 | return reject(error); 131 | } 132 | }); 133 | } 134 | 135 | // Return a decompressed stream, given an encoding. 136 | function decompressed(req, encoding) { 137 | switch (encoding) { 138 | case 'identity': 139 | return req; 140 | case 'deflate': 141 | return req.pipe(zlib.createInflate()); 142 | case 'gzip': 143 | return req.pipe(zlib.createGunzip()); 144 | } 145 | throw httpError(415, `Unsupported content-encoding "${encoding}".`); 146 | } 147 | -------------------------------------------------------------------------------- /schema.graphql: -------------------------------------------------------------------------------- 1 | input CreateTodoItemInput { 2 | todoListID: ID! 3 | title: String! 4 | completed: Boolean 5 | clientMutationId: String 6 | } 7 | 8 | type CreateTodoItemPayload { 9 | todoItem: TodoItem! 10 | todoList: TodoList! 11 | todoListItemsConnectionEdge: TodoItemEdge! 12 | clientMutationId: String 13 | } 14 | 15 | input DeleteCompletedItemsOnTodoListInput { 16 | todoListID: ID! 17 | clientMutationId: String 18 | } 19 | 20 | type DeleteCompletedItemsOnTodoListPayload { 21 | todoList: TodoList! 22 | deletedTodoItemIDs: [ID]! 23 | deletedTodoItems: [TodoItem]! 24 | clientMutationId: String 25 | } 26 | 27 | input DeleteTodoItemInput { 28 | todoItemID: ID! 29 | clientMutationId: String 30 | } 31 | 32 | type DeleteTodoItemPayload { 33 | deletedTodoItemID: ID! 34 | deletedtodoItem: TodoItem! 35 | todoList: TodoList! 36 | clientMutationId: String 37 | } 38 | 39 | type ItemOnTodoListCreated { 40 | todoItem: TodoItem! 41 | todoList: TodoList! 42 | todoListItemsConnectionEdge: TodoItemEdge! 43 | } 44 | 45 | type ItemOnTodoListDeleted { 46 | deletedTodoItemID: ID! 47 | deletedtodoItem: TodoItem! 48 | todoList: TodoList! 49 | } 50 | 51 | type ItemOnTodoListUpdated { 52 | todoItem: TodoItem! 53 | todoList: TodoList! 54 | } 55 | 56 | type ItemsOnTodoListDeleted { 57 | todoList: TodoList! 58 | deletedTodoItemIDs: [ID]! 59 | deletedTodoItems: [TodoItem]! 60 | } 61 | 62 | type ItemsOnTodoListUpdated { 63 | todoList: TodoList! 64 | updatedTodoItemIDs: [ID]! 65 | updatedTodoItems: [TodoItem]! 66 | changes: ItemsOnTodoListUpdatedChanges! 67 | } 68 | 69 | type ItemsOnTodoListUpdatedChanges { 70 | title: String 71 | completed: Boolean 72 | } 73 | 74 | type Mutation { 75 | createTodoItem(input: CreateTodoItemInput!): CreateTodoItemPayload 76 | updateTodoItem(input: UpdateTodoItemInput!): UpdateTodoItemPayload 77 | deleteTodoItem(input: DeleteTodoItemInput!): DeleteTodoItemPayload 78 | updateAllItemsOnTodoList(input: UpdateAllItemsOnTodoListInput!): UpdateAllItemsOnTodoListPayload 79 | deleteCompletedItemsOnTodoList(input: DeleteCompletedItemsOnTodoListInput!): DeleteCompletedItemsOnTodoListPayload 80 | } 81 | 82 | # An object with an ID 83 | interface Node { 84 | # The id of the object. 85 | id: ID! 86 | } 87 | 88 | # Information about pagination in a connection. 89 | type PageInfo { 90 | # When paginating forwards, are there more items? 91 | hasNextPage: Boolean! 92 | 93 | # When paginating backwards, are there more items? 94 | hasPreviousPage: Boolean! 95 | 96 | # When paginating backwards, the cursor to continue. 97 | startCursor: String 98 | 99 | # When paginating forwards, the cursor to continue. 100 | endCursor: String 101 | } 102 | 103 | type Query { 104 | # Fetches an object given its ID 105 | node( 106 | # The ID of an object 107 | id: ID! 108 | ): Node 109 | viewer: User 110 | } 111 | 112 | type Subscription { 113 | itemOnTodoListCreated(todoListID: ID!): ItemOnTodoListCreated 114 | itemOnTodoListUpdated(todoListID: ID!): ItemOnTodoListUpdated 115 | itemOnTodoListDeleted(todoListID: ID!): ItemOnTodoListDeleted 116 | itemsOnTodoListUpdated(todoListID: ID!): ItemsOnTodoListUpdated 117 | itemsOnTodoListDeleted(todoListID: ID!): ItemsOnTodoListDeleted 118 | } 119 | 120 | type TodoItem implements Node { 121 | # The ID of an object 122 | id: ID! 123 | completed: Boolean! 124 | title: String! 125 | listID: ID! 126 | list: TodoList! 127 | } 128 | 129 | # A connection to a list of items. 130 | type TodoItemConnection { 131 | # Information to aid in pagination. 132 | pageInfo: PageInfo! 133 | 134 | # A list of edges. 135 | edges: [TodoItemEdge] 136 | } 137 | 138 | # An edge in a connection. 139 | type TodoItemEdge { 140 | # The item at the end of the edge 141 | node: TodoItem 142 | 143 | # A cursor for use in pagination 144 | cursor: String! 145 | } 146 | 147 | type TodoList implements Node { 148 | # The ID of an object 149 | id: ID! 150 | name: String! 151 | userID: ID! 152 | user: User! 153 | items(after: String, first: Int, before: String, last: Int, filter: TodoListItemsFilterEnum): TodoItemConnection! 154 | itemsCount: Int! 155 | activeItemsCount: Int! 156 | completedItemsCount: Int! 157 | } 158 | 159 | # A connection to a list of items. 160 | type TodoListConnection { 161 | # Information to aid in pagination. 162 | pageInfo: PageInfo! 163 | 164 | # A list of edges. 165 | edges: [TodoListEdge] 166 | } 167 | 168 | # An edge in a connection. 169 | type TodoListEdge { 170 | # The item at the end of the edge 171 | node: TodoList 172 | 173 | # A cursor for use in pagination 174 | cursor: String! 175 | } 176 | 177 | enum TodoListItemsFilterEnum { 178 | all 179 | active 180 | completed 181 | } 182 | 183 | input UpdateAllItemsOnTodoListInput { 184 | todoListID: ID! 185 | title: String 186 | completed: Boolean 187 | clientMutationId: String 188 | } 189 | 190 | type UpdateAllItemsOnTodoListPayload { 191 | todoList: TodoList! 192 | updatedTodoItemIDs: [ID]! 193 | updatedTodoItems: [TodoItem]! 194 | clientMutationId: String 195 | } 196 | 197 | input UpdateTodoItemInput { 198 | todoItemID: ID! 199 | title: String 200 | completed: Boolean 201 | clientMutationId: String 202 | } 203 | 204 | type UpdateTodoItemPayload { 205 | todoItem: TodoItem! 206 | todoList: TodoList! 207 | clientMutationId: String 208 | } 209 | 210 | type User implements Node { 211 | # The ID of an object 212 | id: ID! 213 | todoLists(after: String, first: Int, before: String, last: Int): TodoListConnection! 214 | todoList(id: ID): TodoList 215 | } 216 | -------------------------------------------------------------------------------- /src/client/TodoApp/containers/__generated__/TodoListItems_todoList.graphql.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | */ 4 | 5 | /* eslint-disable */ 6 | 7 | 'use strict'; 8 | 9 | /*:: 10 | import type {ConcreteFragment} from 'relay-runtime'; 11 | export type TodoListItems_todoList = {| 12 | +id: string; 13 | +itemsCount: number; 14 | +completedItemsCount: number; 15 | +items: {| 16 | +pageInfo: {| 17 | +endCursor: ?string; 18 | +hasNextPage: boolean; 19 | |}; 20 | +edges: ?$ReadOnlyArray; 26 | |}; 27 | |}; 28 | */ 29 | 30 | 31 | const fragment /*: ConcreteFragment*/ = { 32 | "argumentDefinitions": [ 33 | { 34 | "kind": "RootArgument", 35 | "name": "count", 36 | "type": "Int" 37 | }, 38 | { 39 | "kind": "RootArgument", 40 | "name": "cursor", 41 | "type": "String" 42 | }, 43 | { 44 | "kind": "RootArgument", 45 | "name": "filter", 46 | "type": "TodoListItemsFilterEnum" 47 | } 48 | ], 49 | "kind": "Fragment", 50 | "metadata": { 51 | "connection": [ 52 | { 53 | "count": "count", 54 | "cursor": "cursor", 55 | "direction": "forward", 56 | "path": [ 57 | "items" 58 | ] 59 | } 60 | ] 61 | }, 62 | "name": "TodoListItems_todoList", 63 | "selections": [ 64 | { 65 | "kind": "ScalarField", 66 | "alias": null, 67 | "args": null, 68 | "name": "id", 69 | "storageKey": null 70 | }, 71 | { 72 | "kind": "ScalarField", 73 | "alias": null, 74 | "args": null, 75 | "name": "itemsCount", 76 | "storageKey": null 77 | }, 78 | { 79 | "kind": "ScalarField", 80 | "alias": null, 81 | "args": null, 82 | "name": "completedItemsCount", 83 | "storageKey": null 84 | }, 85 | { 86 | "kind": "LinkedField", 87 | "alias": "items", 88 | "args": [ 89 | { 90 | "kind": "Variable", 91 | "name": "filter", 92 | "variableName": "filter", 93 | "type": "TodoListItemsFilterEnum" 94 | } 95 | ], 96 | "concreteType": "TodoItemConnection", 97 | "name": "__TodoListItems_items_connection", 98 | "plural": false, 99 | "selections": [ 100 | { 101 | "kind": "LinkedField", 102 | "alias": null, 103 | "args": null, 104 | "concreteType": "PageInfo", 105 | "name": "pageInfo", 106 | "plural": false, 107 | "selections": [ 108 | { 109 | "kind": "ScalarField", 110 | "alias": null, 111 | "args": null, 112 | "name": "endCursor", 113 | "storageKey": null 114 | }, 115 | { 116 | "kind": "ScalarField", 117 | "alias": null, 118 | "args": null, 119 | "name": "hasNextPage", 120 | "storageKey": null 121 | } 122 | ], 123 | "storageKey": null 124 | }, 125 | { 126 | "kind": "LinkedField", 127 | "alias": null, 128 | "args": null, 129 | "concreteType": "TodoItemEdge", 130 | "name": "edges", 131 | "plural": true, 132 | "selections": [ 133 | { 134 | "kind": "ScalarField", 135 | "alias": null, 136 | "args": null, 137 | "name": "cursor", 138 | "storageKey": null 139 | }, 140 | { 141 | "kind": "LinkedField", 142 | "alias": null, 143 | "args": null, 144 | "concreteType": "TodoItem", 145 | "name": "node", 146 | "plural": false, 147 | "selections": [ 148 | { 149 | "kind": "ScalarField", 150 | "alias": null, 151 | "args": null, 152 | "name": "id", 153 | "storageKey": null 154 | }, 155 | { 156 | "kind": "FragmentSpread", 157 | "name": "TodoItem_todoItem", 158 | "args": null 159 | } 160 | ], 161 | "storageKey": null 162 | } 163 | ], 164 | "storageKey": null 165 | }, 166 | { 167 | "kind": "InlineFragment", 168 | "type": "TodoItemConnection", 169 | "selections": [ 170 | { 171 | "kind": "LinkedField", 172 | "alias": null, 173 | "args": null, 174 | "concreteType": "TodoItemEdge", 175 | "name": "edges", 176 | "plural": true, 177 | "selections": [ 178 | { 179 | "kind": "LinkedField", 180 | "alias": null, 181 | "args": null, 182 | "concreteType": "TodoItem", 183 | "name": "node", 184 | "plural": false, 185 | "selections": [ 186 | { 187 | "kind": "ScalarField", 188 | "alias": null, 189 | "args": null, 190 | "name": "__typename", 191 | "storageKey": null 192 | } 193 | ], 194 | "storageKey": null 195 | } 196 | ], 197 | "storageKey": null 198 | } 199 | ] 200 | } 201 | ], 202 | "storageKey": null 203 | } 204 | ], 205 | "type": "TodoList" 206 | }; 207 | 208 | module.exports = fragment; 209 | -------------------------------------------------------------------------------- /src/client/TodoApp/subscriptions/__generated__/ItemsOnTodoListDeletedSubscription.graphql.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | * @relayHash fb8db917a89d05a5ec5a9f5eaab8698d 4 | */ 5 | 6 | /* eslint-disable */ 7 | 8 | 'use strict'; 9 | 10 | /*:: 11 | import type {ConcreteBatch} from 'relay-runtime'; 12 | export type ItemsOnTodoListDeletedSubscriptionVariables = {| 13 | todoListID: string; 14 | |}; 15 | export type ItemsOnTodoListDeletedSubscriptionResponse = {| 16 | +itemsOnTodoListDeleted: ?{| 17 | +deletedTodoItemIDs: $ReadOnlyArray; 18 | +todoList: {| 19 | +id: string; 20 | +completedItemsCount: number; 21 | +activeItemsCount: number; 22 | |}; 23 | |}; 24 | |}; 25 | */ 26 | 27 | 28 | /* 29 | subscription ItemsOnTodoListDeletedSubscription( 30 | $todoListID: ID! 31 | ) { 32 | itemsOnTodoListDeleted(todoListID: $todoListID) { 33 | deletedTodoItemIDs 34 | todoList { 35 | id 36 | completedItemsCount 37 | activeItemsCount 38 | } 39 | } 40 | } 41 | */ 42 | 43 | const batch /*: ConcreteBatch*/ = { 44 | "fragment": { 45 | "argumentDefinitions": [ 46 | { 47 | "kind": "LocalArgument", 48 | "name": "todoListID", 49 | "type": "ID!", 50 | "defaultValue": null 51 | } 52 | ], 53 | "kind": "Fragment", 54 | "metadata": null, 55 | "name": "ItemsOnTodoListDeletedSubscription", 56 | "selections": [ 57 | { 58 | "kind": "LinkedField", 59 | "alias": null, 60 | "args": [ 61 | { 62 | "kind": "Variable", 63 | "name": "todoListID", 64 | "variableName": "todoListID", 65 | "type": "ID!" 66 | } 67 | ], 68 | "concreteType": "ItemsOnTodoListDeleted", 69 | "name": "itemsOnTodoListDeleted", 70 | "plural": false, 71 | "selections": [ 72 | { 73 | "kind": "ScalarField", 74 | "alias": null, 75 | "args": null, 76 | "name": "deletedTodoItemIDs", 77 | "storageKey": null 78 | }, 79 | { 80 | "kind": "LinkedField", 81 | "alias": null, 82 | "args": null, 83 | "concreteType": "TodoList", 84 | "name": "todoList", 85 | "plural": false, 86 | "selections": [ 87 | { 88 | "kind": "ScalarField", 89 | "alias": null, 90 | "args": null, 91 | "name": "id", 92 | "storageKey": null 93 | }, 94 | { 95 | "kind": "ScalarField", 96 | "alias": null, 97 | "args": null, 98 | "name": "completedItemsCount", 99 | "storageKey": null 100 | }, 101 | { 102 | "kind": "ScalarField", 103 | "alias": null, 104 | "args": null, 105 | "name": "activeItemsCount", 106 | "storageKey": null 107 | } 108 | ], 109 | "storageKey": null 110 | } 111 | ], 112 | "storageKey": null 113 | } 114 | ], 115 | "type": "Subscription" 116 | }, 117 | "id": null, 118 | "kind": "Batch", 119 | "metadata": {}, 120 | "name": "ItemsOnTodoListDeletedSubscription", 121 | "query": { 122 | "argumentDefinitions": [ 123 | { 124 | "kind": "LocalArgument", 125 | "name": "todoListID", 126 | "type": "ID!", 127 | "defaultValue": null 128 | } 129 | ], 130 | "kind": "Root", 131 | "name": "ItemsOnTodoListDeletedSubscription", 132 | "operation": "subscription", 133 | "selections": [ 134 | { 135 | "kind": "LinkedField", 136 | "alias": null, 137 | "args": [ 138 | { 139 | "kind": "Variable", 140 | "name": "todoListID", 141 | "variableName": "todoListID", 142 | "type": "ID!" 143 | } 144 | ], 145 | "concreteType": "ItemsOnTodoListDeleted", 146 | "name": "itemsOnTodoListDeleted", 147 | "plural": false, 148 | "selections": [ 149 | { 150 | "kind": "ScalarField", 151 | "alias": null, 152 | "args": null, 153 | "name": "deletedTodoItemIDs", 154 | "storageKey": null 155 | }, 156 | { 157 | "kind": "LinkedField", 158 | "alias": null, 159 | "args": null, 160 | "concreteType": "TodoList", 161 | "name": "todoList", 162 | "plural": false, 163 | "selections": [ 164 | { 165 | "kind": "ScalarField", 166 | "alias": null, 167 | "args": null, 168 | "name": "id", 169 | "storageKey": null 170 | }, 171 | { 172 | "kind": "ScalarField", 173 | "alias": null, 174 | "args": null, 175 | "name": "completedItemsCount", 176 | "storageKey": null 177 | }, 178 | { 179 | "kind": "ScalarField", 180 | "alias": null, 181 | "args": null, 182 | "name": "activeItemsCount", 183 | "storageKey": null 184 | } 185 | ], 186 | "storageKey": null 187 | } 188 | ], 189 | "storageKey": null 190 | } 191 | ] 192 | }, 193 | "text": "subscription ItemsOnTodoListDeletedSubscription(\n $todoListID: ID!\n) {\n itemsOnTodoListDeleted(todoListID: $todoListID) {\n deletedTodoItemIDs\n todoList {\n id\n completedItemsCount\n activeItemsCount\n }\n }\n}\n" 194 | }; 195 | 196 | module.exports = batch; 197 | -------------------------------------------------------------------------------- /src/client/TodoApp/mutations/__generated__/UpdateAllItemsOnTodoListMutation.graphql.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | * @relayHash db1b87011835f5fb55c9102a0bd46891 4 | */ 5 | 6 | /* eslint-disable */ 7 | 8 | 'use strict'; 9 | 10 | /*:: 11 | import type {ConcreteBatch} from 'relay-runtime'; 12 | export type UpdateAllItemsOnTodoListMutationVariables = {| 13 | input: { 14 | todoListID: string; 15 | title?: ?string; 16 | completed?: ?boolean; 17 | clientMutationId?: ?string; 18 | }; 19 | |}; 20 | export type UpdateAllItemsOnTodoListMutationResponse = {| 21 | +updateAllItemsOnTodoList: ?{| 22 | +todoList: {| 23 | +id: string; 24 | +completedItemsCount: number; 25 | +activeItemsCount: number; 26 | |}; 27 | +updatedTodoItemIDs: $ReadOnlyArray; 28 | |}; 29 | |}; 30 | */ 31 | 32 | 33 | /* 34 | mutation UpdateAllItemsOnTodoListMutation( 35 | $input: UpdateAllItemsOnTodoListInput! 36 | ) { 37 | updateAllItemsOnTodoList(input: $input) { 38 | todoList { 39 | id 40 | completedItemsCount 41 | activeItemsCount 42 | } 43 | updatedTodoItemIDs 44 | } 45 | } 46 | */ 47 | 48 | const batch /*: ConcreteBatch*/ = { 49 | "fragment": { 50 | "argumentDefinitions": [ 51 | { 52 | "kind": "LocalArgument", 53 | "name": "input", 54 | "type": "UpdateAllItemsOnTodoListInput!", 55 | "defaultValue": null 56 | } 57 | ], 58 | "kind": "Fragment", 59 | "metadata": null, 60 | "name": "UpdateAllItemsOnTodoListMutation", 61 | "selections": [ 62 | { 63 | "kind": "LinkedField", 64 | "alias": null, 65 | "args": [ 66 | { 67 | "kind": "Variable", 68 | "name": "input", 69 | "variableName": "input", 70 | "type": "UpdateAllItemsOnTodoListInput!" 71 | } 72 | ], 73 | "concreteType": "UpdateAllItemsOnTodoListPayload", 74 | "name": "updateAllItemsOnTodoList", 75 | "plural": false, 76 | "selections": [ 77 | { 78 | "kind": "LinkedField", 79 | "alias": null, 80 | "args": null, 81 | "concreteType": "TodoList", 82 | "name": "todoList", 83 | "plural": false, 84 | "selections": [ 85 | { 86 | "kind": "ScalarField", 87 | "alias": null, 88 | "args": null, 89 | "name": "id", 90 | "storageKey": null 91 | }, 92 | { 93 | "kind": "ScalarField", 94 | "alias": null, 95 | "args": null, 96 | "name": "completedItemsCount", 97 | "storageKey": null 98 | }, 99 | { 100 | "kind": "ScalarField", 101 | "alias": null, 102 | "args": null, 103 | "name": "activeItemsCount", 104 | "storageKey": null 105 | } 106 | ], 107 | "storageKey": null 108 | }, 109 | { 110 | "kind": "ScalarField", 111 | "alias": null, 112 | "args": null, 113 | "name": "updatedTodoItemIDs", 114 | "storageKey": null 115 | } 116 | ], 117 | "storageKey": null 118 | } 119 | ], 120 | "type": "Mutation" 121 | }, 122 | "id": null, 123 | "kind": "Batch", 124 | "metadata": {}, 125 | "name": "UpdateAllItemsOnTodoListMutation", 126 | "query": { 127 | "argumentDefinitions": [ 128 | { 129 | "kind": "LocalArgument", 130 | "name": "input", 131 | "type": "UpdateAllItemsOnTodoListInput!", 132 | "defaultValue": null 133 | } 134 | ], 135 | "kind": "Root", 136 | "name": "UpdateAllItemsOnTodoListMutation", 137 | "operation": "mutation", 138 | "selections": [ 139 | { 140 | "kind": "LinkedField", 141 | "alias": null, 142 | "args": [ 143 | { 144 | "kind": "Variable", 145 | "name": "input", 146 | "variableName": "input", 147 | "type": "UpdateAllItemsOnTodoListInput!" 148 | } 149 | ], 150 | "concreteType": "UpdateAllItemsOnTodoListPayload", 151 | "name": "updateAllItemsOnTodoList", 152 | "plural": false, 153 | "selections": [ 154 | { 155 | "kind": "LinkedField", 156 | "alias": null, 157 | "args": null, 158 | "concreteType": "TodoList", 159 | "name": "todoList", 160 | "plural": false, 161 | "selections": [ 162 | { 163 | "kind": "ScalarField", 164 | "alias": null, 165 | "args": null, 166 | "name": "id", 167 | "storageKey": null 168 | }, 169 | { 170 | "kind": "ScalarField", 171 | "alias": null, 172 | "args": null, 173 | "name": "completedItemsCount", 174 | "storageKey": null 175 | }, 176 | { 177 | "kind": "ScalarField", 178 | "alias": null, 179 | "args": null, 180 | "name": "activeItemsCount", 181 | "storageKey": null 182 | } 183 | ], 184 | "storageKey": null 185 | }, 186 | { 187 | "kind": "ScalarField", 188 | "alias": null, 189 | "args": null, 190 | "name": "updatedTodoItemIDs", 191 | "storageKey": null 192 | } 193 | ], 194 | "storageKey": null 195 | } 196 | ] 197 | }, 198 | "text": "mutation UpdateAllItemsOnTodoListMutation(\n $input: UpdateAllItemsOnTodoListInput!\n) {\n updateAllItemsOnTodoList(input: $input) {\n todoList {\n id\n completedItemsCount\n activeItemsCount\n }\n updatedTodoItemIDs\n }\n}\n" 199 | }; 200 | 201 | module.exports = batch; 202 | -------------------------------------------------------------------------------- /src/client/TodoApp/mutations/__generated__/DeleteCompletedItemsOnTodoListMutation.graphql.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | * @relayHash 3e678190c1da80ea714cd713a0e17cf1 4 | */ 5 | 6 | /* eslint-disable */ 7 | 8 | 'use strict'; 9 | 10 | /*:: 11 | import type {ConcreteBatch} from 'relay-runtime'; 12 | export type DeleteCompletedItemsOnTodoListMutationVariables = {| 13 | input: { 14 | todoListID: string; 15 | clientMutationId?: ?string; 16 | }; 17 | |}; 18 | export type DeleteCompletedItemsOnTodoListMutationResponse = {| 19 | +deleteCompletedItemsOnTodoList: ?{| 20 | +deletedTodoItemIDs: $ReadOnlyArray; 21 | +todoList: {| 22 | +id: string; 23 | +itemsCount: number; 24 | +completedItemsCount: number; 25 | |}; 26 | |}; 27 | |}; 28 | */ 29 | 30 | 31 | /* 32 | mutation DeleteCompletedItemsOnTodoListMutation( 33 | $input: DeleteCompletedItemsOnTodoListInput! 34 | ) { 35 | deleteCompletedItemsOnTodoList(input: $input) { 36 | deletedTodoItemIDs 37 | todoList { 38 | id 39 | itemsCount 40 | completedItemsCount 41 | } 42 | } 43 | } 44 | */ 45 | 46 | const batch /*: ConcreteBatch*/ = { 47 | "fragment": { 48 | "argumentDefinitions": [ 49 | { 50 | "kind": "LocalArgument", 51 | "name": "input", 52 | "type": "DeleteCompletedItemsOnTodoListInput!", 53 | "defaultValue": null 54 | } 55 | ], 56 | "kind": "Fragment", 57 | "metadata": null, 58 | "name": "DeleteCompletedItemsOnTodoListMutation", 59 | "selections": [ 60 | { 61 | "kind": "LinkedField", 62 | "alias": null, 63 | "args": [ 64 | { 65 | "kind": "Variable", 66 | "name": "input", 67 | "variableName": "input", 68 | "type": "DeleteCompletedItemsOnTodoListInput!" 69 | } 70 | ], 71 | "concreteType": "DeleteCompletedItemsOnTodoListPayload", 72 | "name": "deleteCompletedItemsOnTodoList", 73 | "plural": false, 74 | "selections": [ 75 | { 76 | "kind": "ScalarField", 77 | "alias": null, 78 | "args": null, 79 | "name": "deletedTodoItemIDs", 80 | "storageKey": null 81 | }, 82 | { 83 | "kind": "LinkedField", 84 | "alias": null, 85 | "args": null, 86 | "concreteType": "TodoList", 87 | "name": "todoList", 88 | "plural": false, 89 | "selections": [ 90 | { 91 | "kind": "ScalarField", 92 | "alias": null, 93 | "args": null, 94 | "name": "id", 95 | "storageKey": null 96 | }, 97 | { 98 | "kind": "ScalarField", 99 | "alias": null, 100 | "args": null, 101 | "name": "itemsCount", 102 | "storageKey": null 103 | }, 104 | { 105 | "kind": "ScalarField", 106 | "alias": null, 107 | "args": null, 108 | "name": "completedItemsCount", 109 | "storageKey": null 110 | } 111 | ], 112 | "storageKey": null 113 | } 114 | ], 115 | "storageKey": null 116 | } 117 | ], 118 | "type": "Mutation" 119 | }, 120 | "id": null, 121 | "kind": "Batch", 122 | "metadata": {}, 123 | "name": "DeleteCompletedItemsOnTodoListMutation", 124 | "query": { 125 | "argumentDefinitions": [ 126 | { 127 | "kind": "LocalArgument", 128 | "name": "input", 129 | "type": "DeleteCompletedItemsOnTodoListInput!", 130 | "defaultValue": null 131 | } 132 | ], 133 | "kind": "Root", 134 | "name": "DeleteCompletedItemsOnTodoListMutation", 135 | "operation": "mutation", 136 | "selections": [ 137 | { 138 | "kind": "LinkedField", 139 | "alias": null, 140 | "args": [ 141 | { 142 | "kind": "Variable", 143 | "name": "input", 144 | "variableName": "input", 145 | "type": "DeleteCompletedItemsOnTodoListInput!" 146 | } 147 | ], 148 | "concreteType": "DeleteCompletedItemsOnTodoListPayload", 149 | "name": "deleteCompletedItemsOnTodoList", 150 | "plural": false, 151 | "selections": [ 152 | { 153 | "kind": "ScalarField", 154 | "alias": null, 155 | "args": null, 156 | "name": "deletedTodoItemIDs", 157 | "storageKey": null 158 | }, 159 | { 160 | "kind": "LinkedField", 161 | "alias": null, 162 | "args": null, 163 | "concreteType": "TodoList", 164 | "name": "todoList", 165 | "plural": false, 166 | "selections": [ 167 | { 168 | "kind": "ScalarField", 169 | "alias": null, 170 | "args": null, 171 | "name": "id", 172 | "storageKey": null 173 | }, 174 | { 175 | "kind": "ScalarField", 176 | "alias": null, 177 | "args": null, 178 | "name": "itemsCount", 179 | "storageKey": null 180 | }, 181 | { 182 | "kind": "ScalarField", 183 | "alias": null, 184 | "args": null, 185 | "name": "completedItemsCount", 186 | "storageKey": null 187 | } 188 | ], 189 | "storageKey": null 190 | } 191 | ], 192 | "storageKey": null 193 | } 194 | ] 195 | }, 196 | "text": "mutation DeleteCompletedItemsOnTodoListMutation(\n $input: DeleteCompletedItemsOnTodoListInput!\n) {\n deleteCompletedItemsOnTodoList(input: $input) {\n deletedTodoItemIDs\n todoList {\n id\n itemsCount\n completedItemsCount\n }\n }\n}\n" 197 | }; 198 | 199 | module.exports = batch; 200 | -------------------------------------------------------------------------------- /src/client/TodoApp/mutations/__generated__/DeleteTodoItemMutation.graphql.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | * @relayHash 4f7d951ba7fe7ba3de2f15c3d0bd7141 4 | */ 5 | 6 | /* eslint-disable */ 7 | 8 | 'use strict'; 9 | 10 | /*:: 11 | import type {ConcreteBatch} from 'relay-runtime'; 12 | export type DeleteTodoItemMutationVariables = {| 13 | input: { 14 | todoItemID: string; 15 | clientMutationId?: ?string; 16 | }; 17 | |}; 18 | export type DeleteTodoItemMutationResponse = {| 19 | +deleteTodoItem: ?{| 20 | +deletedTodoItemID: string; 21 | +todoList: {| 22 | +id: string; 23 | +itemsCount: number; 24 | +completedItemsCount: number; 25 | +activeItemsCount: number; 26 | |}; 27 | |}; 28 | |}; 29 | */ 30 | 31 | 32 | /* 33 | mutation DeleteTodoItemMutation( 34 | $input: DeleteTodoItemInput! 35 | ) { 36 | deleteTodoItem(input: $input) { 37 | deletedTodoItemID 38 | todoList { 39 | id 40 | itemsCount 41 | completedItemsCount 42 | activeItemsCount 43 | } 44 | } 45 | } 46 | */ 47 | 48 | const batch /*: ConcreteBatch*/ = { 49 | "fragment": { 50 | "argumentDefinitions": [ 51 | { 52 | "kind": "LocalArgument", 53 | "name": "input", 54 | "type": "DeleteTodoItemInput!", 55 | "defaultValue": null 56 | } 57 | ], 58 | "kind": "Fragment", 59 | "metadata": null, 60 | "name": "DeleteTodoItemMutation", 61 | "selections": [ 62 | { 63 | "kind": "LinkedField", 64 | "alias": null, 65 | "args": [ 66 | { 67 | "kind": "Variable", 68 | "name": "input", 69 | "variableName": "input", 70 | "type": "DeleteTodoItemInput!" 71 | } 72 | ], 73 | "concreteType": "DeleteTodoItemPayload", 74 | "name": "deleteTodoItem", 75 | "plural": false, 76 | "selections": [ 77 | { 78 | "kind": "ScalarField", 79 | "alias": null, 80 | "args": null, 81 | "name": "deletedTodoItemID", 82 | "storageKey": null 83 | }, 84 | { 85 | "kind": "LinkedField", 86 | "alias": null, 87 | "args": null, 88 | "concreteType": "TodoList", 89 | "name": "todoList", 90 | "plural": false, 91 | "selections": [ 92 | { 93 | "kind": "ScalarField", 94 | "alias": null, 95 | "args": null, 96 | "name": "id", 97 | "storageKey": null 98 | }, 99 | { 100 | "kind": "ScalarField", 101 | "alias": null, 102 | "args": null, 103 | "name": "itemsCount", 104 | "storageKey": null 105 | }, 106 | { 107 | "kind": "ScalarField", 108 | "alias": null, 109 | "args": null, 110 | "name": "completedItemsCount", 111 | "storageKey": null 112 | }, 113 | { 114 | "kind": "ScalarField", 115 | "alias": null, 116 | "args": null, 117 | "name": "activeItemsCount", 118 | "storageKey": null 119 | } 120 | ], 121 | "storageKey": null 122 | } 123 | ], 124 | "storageKey": null 125 | } 126 | ], 127 | "type": "Mutation" 128 | }, 129 | "id": null, 130 | "kind": "Batch", 131 | "metadata": {}, 132 | "name": "DeleteTodoItemMutation", 133 | "query": { 134 | "argumentDefinitions": [ 135 | { 136 | "kind": "LocalArgument", 137 | "name": "input", 138 | "type": "DeleteTodoItemInput!", 139 | "defaultValue": null 140 | } 141 | ], 142 | "kind": "Root", 143 | "name": "DeleteTodoItemMutation", 144 | "operation": "mutation", 145 | "selections": [ 146 | { 147 | "kind": "LinkedField", 148 | "alias": null, 149 | "args": [ 150 | { 151 | "kind": "Variable", 152 | "name": "input", 153 | "variableName": "input", 154 | "type": "DeleteTodoItemInput!" 155 | } 156 | ], 157 | "concreteType": "DeleteTodoItemPayload", 158 | "name": "deleteTodoItem", 159 | "plural": false, 160 | "selections": [ 161 | { 162 | "kind": "ScalarField", 163 | "alias": null, 164 | "args": null, 165 | "name": "deletedTodoItemID", 166 | "storageKey": null 167 | }, 168 | { 169 | "kind": "LinkedField", 170 | "alias": null, 171 | "args": null, 172 | "concreteType": "TodoList", 173 | "name": "todoList", 174 | "plural": false, 175 | "selections": [ 176 | { 177 | "kind": "ScalarField", 178 | "alias": null, 179 | "args": null, 180 | "name": "id", 181 | "storageKey": null 182 | }, 183 | { 184 | "kind": "ScalarField", 185 | "alias": null, 186 | "args": null, 187 | "name": "itemsCount", 188 | "storageKey": null 189 | }, 190 | { 191 | "kind": "ScalarField", 192 | "alias": null, 193 | "args": null, 194 | "name": "completedItemsCount", 195 | "storageKey": null 196 | }, 197 | { 198 | "kind": "ScalarField", 199 | "alias": null, 200 | "args": null, 201 | "name": "activeItemsCount", 202 | "storageKey": null 203 | } 204 | ], 205 | "storageKey": null 206 | } 207 | ], 208 | "storageKey": null 209 | } 210 | ] 211 | }, 212 | "text": "mutation DeleteTodoItemMutation(\n $input: DeleteTodoItemInput!\n) {\n deleteTodoItem(input: $input) {\n deletedTodoItemID\n todoList {\n id\n itemsCount\n completedItemsCount\n activeItemsCount\n }\n }\n}\n" 213 | }; 214 | 215 | module.exports = batch; 216 | -------------------------------------------------------------------------------- /src/client/TodoApp/subscriptions/__generated__/ItemOnTodoListDeletedSubscription.graphql.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | * @relayHash a67d15bd5481f0cf81d87844f8e470bc 4 | */ 5 | 6 | /* eslint-disable */ 7 | 8 | 'use strict'; 9 | 10 | /*:: 11 | import type {ConcreteBatch} from 'relay-runtime'; 12 | export type ItemOnTodoListDeletedSubscriptionVariables = {| 13 | todoListID: string; 14 | |}; 15 | export type ItemOnTodoListDeletedSubscriptionResponse = {| 16 | +itemOnTodoListDeleted: ?{| 17 | +deletedTodoItemID: string; 18 | +todoList: {| 19 | +id: string; 20 | +itemsCount: number; 21 | +completedItemsCount: number; 22 | +activeItemsCount: number; 23 | |}; 24 | |}; 25 | |}; 26 | */ 27 | 28 | 29 | /* 30 | subscription ItemOnTodoListDeletedSubscription( 31 | $todoListID: ID! 32 | ) { 33 | itemOnTodoListDeleted(todoListID: $todoListID) { 34 | deletedTodoItemID 35 | todoList { 36 | id 37 | itemsCount 38 | completedItemsCount 39 | activeItemsCount 40 | } 41 | } 42 | } 43 | */ 44 | 45 | const batch /*: ConcreteBatch*/ = { 46 | "fragment": { 47 | "argumentDefinitions": [ 48 | { 49 | "kind": "LocalArgument", 50 | "name": "todoListID", 51 | "type": "ID!", 52 | "defaultValue": null 53 | } 54 | ], 55 | "kind": "Fragment", 56 | "metadata": null, 57 | "name": "ItemOnTodoListDeletedSubscription", 58 | "selections": [ 59 | { 60 | "kind": "LinkedField", 61 | "alias": null, 62 | "args": [ 63 | { 64 | "kind": "Variable", 65 | "name": "todoListID", 66 | "variableName": "todoListID", 67 | "type": "ID!" 68 | } 69 | ], 70 | "concreteType": "ItemOnTodoListDeleted", 71 | "name": "itemOnTodoListDeleted", 72 | "plural": false, 73 | "selections": [ 74 | { 75 | "kind": "ScalarField", 76 | "alias": null, 77 | "args": null, 78 | "name": "deletedTodoItemID", 79 | "storageKey": null 80 | }, 81 | { 82 | "kind": "LinkedField", 83 | "alias": null, 84 | "args": null, 85 | "concreteType": "TodoList", 86 | "name": "todoList", 87 | "plural": false, 88 | "selections": [ 89 | { 90 | "kind": "ScalarField", 91 | "alias": null, 92 | "args": null, 93 | "name": "id", 94 | "storageKey": null 95 | }, 96 | { 97 | "kind": "ScalarField", 98 | "alias": null, 99 | "args": null, 100 | "name": "itemsCount", 101 | "storageKey": null 102 | }, 103 | { 104 | "kind": "ScalarField", 105 | "alias": null, 106 | "args": null, 107 | "name": "completedItemsCount", 108 | "storageKey": null 109 | }, 110 | { 111 | "kind": "ScalarField", 112 | "alias": null, 113 | "args": null, 114 | "name": "activeItemsCount", 115 | "storageKey": null 116 | } 117 | ], 118 | "storageKey": null 119 | } 120 | ], 121 | "storageKey": null 122 | } 123 | ], 124 | "type": "Subscription" 125 | }, 126 | "id": null, 127 | "kind": "Batch", 128 | "metadata": {}, 129 | "name": "ItemOnTodoListDeletedSubscription", 130 | "query": { 131 | "argumentDefinitions": [ 132 | { 133 | "kind": "LocalArgument", 134 | "name": "todoListID", 135 | "type": "ID!", 136 | "defaultValue": null 137 | } 138 | ], 139 | "kind": "Root", 140 | "name": "ItemOnTodoListDeletedSubscription", 141 | "operation": "subscription", 142 | "selections": [ 143 | { 144 | "kind": "LinkedField", 145 | "alias": null, 146 | "args": [ 147 | { 148 | "kind": "Variable", 149 | "name": "todoListID", 150 | "variableName": "todoListID", 151 | "type": "ID!" 152 | } 153 | ], 154 | "concreteType": "ItemOnTodoListDeleted", 155 | "name": "itemOnTodoListDeleted", 156 | "plural": false, 157 | "selections": [ 158 | { 159 | "kind": "ScalarField", 160 | "alias": null, 161 | "args": null, 162 | "name": "deletedTodoItemID", 163 | "storageKey": null 164 | }, 165 | { 166 | "kind": "LinkedField", 167 | "alias": null, 168 | "args": null, 169 | "concreteType": "TodoList", 170 | "name": "todoList", 171 | "plural": false, 172 | "selections": [ 173 | { 174 | "kind": "ScalarField", 175 | "alias": null, 176 | "args": null, 177 | "name": "id", 178 | "storageKey": null 179 | }, 180 | { 181 | "kind": "ScalarField", 182 | "alias": null, 183 | "args": null, 184 | "name": "itemsCount", 185 | "storageKey": null 186 | }, 187 | { 188 | "kind": "ScalarField", 189 | "alias": null, 190 | "args": null, 191 | "name": "completedItemsCount", 192 | "storageKey": null 193 | }, 194 | { 195 | "kind": "ScalarField", 196 | "alias": null, 197 | "args": null, 198 | "name": "activeItemsCount", 199 | "storageKey": null 200 | } 201 | ], 202 | "storageKey": null 203 | } 204 | ], 205 | "storageKey": null 206 | } 207 | ] 208 | }, 209 | "text": "subscription ItemOnTodoListDeletedSubscription(\n $todoListID: ID!\n) {\n itemOnTodoListDeleted(todoListID: $todoListID) {\n deletedTodoItemID\n todoList {\n id\n itemsCount\n completedItemsCount\n activeItemsCount\n }\n }\n}\n" 210 | }; 211 | 212 | module.exports = batch; 213 | -------------------------------------------------------------------------------- /src/vendor/express-graphql/renderGraphiQL.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /** 3 | * Copyright (c) 2015, Facebook, Inc. 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the BSD-style license found in the 7 | * LICENSE file in the root directory of this source tree. An additional grant 8 | * of patent rights can be found in the PATENTS file in the same directory. 9 | */ 10 | 11 | type GraphiQLData = { 12 | query: ?string, 13 | variables: ?{ [name: string]: mixed }, 14 | operationName: ?string, 15 | result?: mixed, 16 | }; 17 | 18 | // Current latest version of GraphiQL. 19 | const GRAPHIQL_VERSION = '0.11.2'; 20 | 21 | // Ensures string values are safe to be used within a 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 171 | 172 | `; 173 | } 174 | -------------------------------------------------------------------------------- /src/client/TodoApp/containers/TodoListItems.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React, { Component } from 'react' 4 | import { graphql, createPaginationContainer } from 'react-relay' 5 | import type { DataID, PaginationContainerRelayProp } from 'react-relay' 6 | import environment from '../relay/environment' 7 | 8 | import TodoListItemsComponent from '../components/TodoListItems' 9 | 10 | import { registerTodoListItemsConnectionName } from '../registrations/todoListItemsConnectionNames' 11 | 12 | import UpdateAllItemsOnTodoListMutation from '../mutations/UpdateAllItemsOnTodoListMutation' 13 | 14 | import ItemOnTodoListCreatedSubscription from '../subscriptions/ItemOnTodoListCreatedSubscription' 15 | import ItemOnTodoListUpdatedSubscription from '../subscriptions/ItemOnTodoListUpdatedSubscription' 16 | import ItemOnTodoListDeletedSubscription from '../subscriptions/ItemOnTodoListDeletedSubscription' 17 | import ItemsOnTodoListUpdatedSubscription from '../subscriptions/ItemsOnTodoListUpdatedSubscription' 18 | import ItemsOnTodoListDeletedSubscription from '../subscriptions/ItemsOnTodoListDeletedSubscription' 19 | 20 | type Props = {| 21 | todoList: { 22 | id: DataID, 23 | }, 24 | relay: PaginationContainerRelayProp, 25 | |}; 26 | 27 | class TodoListItemsContainer extends Component { 28 | /* eslint-disable react/sort-comp */ 29 | itemOnTodoListCreatedSubscription: ItemOnTodoListCreatedSubscription; 30 | itemOnTodoListUpdatedSubscription: ItemOnTodoListUpdatedSubscription; 31 | itemOnTodoListDeletedSubscription: ItemOnTodoListDeletedSubscription; 32 | itemsOnTodoListUpdatedSubscription: ItemsOnTodoListUpdatedSubscription; 33 | itemsOnTodoListDeletedSubscription: ItemsOnTodoListDeletedSubscription; 34 | component: TodoListItemsComponent; 35 | /* eslint-enable react/sort-comp */ 36 | 37 | componentWillMount() { 38 | const subscriptionParams = [environment, { 39 | todoListID: this.props.todoList.id, 40 | }] 41 | 42 | this.itemOnTodoListCreatedSubscription = new ItemOnTodoListCreatedSubscription(...subscriptionParams) 43 | this.itemOnTodoListUpdatedSubscription = new ItemOnTodoListUpdatedSubscription(...subscriptionParams) 44 | this.itemOnTodoListDeletedSubscription = new ItemOnTodoListDeletedSubscription(...subscriptionParams) 45 | this.itemsOnTodoListUpdatedSubscription = new ItemsOnTodoListUpdatedSubscription(...subscriptionParams) 46 | this.itemsOnTodoListDeletedSubscription = new ItemsOnTodoListDeletedSubscription(...subscriptionParams) 47 | 48 | this.itemOnTodoListCreatedSubscription.subscribe() 49 | this.itemOnTodoListUpdatedSubscription.subscribe() 50 | this.itemOnTodoListDeletedSubscription.subscribe() 51 | this.itemsOnTodoListUpdatedSubscription.subscribe() 52 | this.itemsOnTodoListDeletedSubscription.subscribe() 53 | } 54 | 55 | componentWillUnmount() { 56 | if (this.itemOnTodoListCreatedSubscription) this.itemOnTodoListCreatedSubscription.unsubscribe() 57 | if (this.itemOnTodoListUpdatedSubscription) this.itemOnTodoListUpdatedSubscription.unsubscribe() 58 | if (this.itemOnTodoListDeletedSubscription) this.itemOnTodoListDeletedSubscription.unsubscribe() 59 | if (this.itemsOnTodoListUpdatedSubscription) this.itemsOnTodoListUpdatedSubscription.unsubscribe() 60 | if (this.itemsOnTodoListDeletedSubscription) this.itemsOnTodoListDeletedSubscription.unsubscribe() 61 | } 62 | 63 | setFilter = (filter: 'all' | 'active' | 'completed') => { 64 | if (!this.props.relay.refetch) throw new Error('props.relay.refetch is undefined') 65 | this.props.relay.refetch({ filter }) 66 | } 67 | 68 | loadMore = (callback) => { 69 | if (!this.props.relay.hasMore() || this.props.relay.isLoading()) return 70 | this.props.relay.loadMore(10, callback) 71 | } 72 | 73 | refreshLayout = () => { 74 | if ( 75 | this.component && 76 | this.component.refreshLayout 77 | ) { 78 | setTimeout(this.component.refreshLayout, 10) 79 | } 80 | } 81 | 82 | refresh = () => { 83 | if (this.props.relay.isLoading()) return 84 | // this.setState({ refreshing: true }) 85 | this.props.relay.refetchConnection(10, () => { 86 | // this.setState({ refreshing: false }) 87 | }) 88 | } 89 | 90 | _getNewMarkAllItemsOnTodoListMutation = ({ completed }) => { 91 | const { todoList } = this.props 92 | 93 | return new UpdateAllItemsOnTodoListMutation(environment, { 94 | todoListID: todoList.id, 95 | completed, 96 | }) 97 | } 98 | 99 | _handleLoadMoreTriggered = (callbackIfHasMore) => { 100 | this.loadMore(() => { 101 | if (this.props.relay.hasMore()) callbackIfHasMore() 102 | }) 103 | } 104 | 105 | _handleMarkAllCompletedChangeValue = (completed) => { 106 | const mutation = this._getNewMarkAllItemsOnTodoListMutation({ completed }) 107 | mutation.commit() 108 | } 109 | 110 | render() { 111 | return ( 112 | this.component = ref} 115 | onMarkAllCompletedChangeValue={this._handleMarkAllCompletedChangeValue} 116 | onLoadMoreTriggered={this._handleLoadMoreTriggered} 117 | /> 118 | ) 119 | } 120 | } 121 | 122 | export default createPaginationContainer( 123 | TodoListItemsContainer, 124 | graphql` 125 | fragment TodoListItems_todoList on TodoList { 126 | id 127 | itemsCount 128 | completedItemsCount 129 | items( 130 | first: $count 131 | after: $cursor 132 | filter: $filter 133 | ) @connection(key: "TodoListItems_items") { 134 | pageInfo { 135 | endCursor 136 | hasNextPage 137 | } 138 | edges { 139 | cursor 140 | node { 141 | id 142 | ...TodoItem_todoItem 143 | } 144 | } 145 | } 146 | } 147 | `, 148 | { 149 | direction: 'forward', 150 | getConnectionFromProps(props) { 151 | return props.todoList && props.todoList.items 152 | }, 153 | getFragmentVariables(prevVars, totalCount) { 154 | return { 155 | ...prevVars, 156 | count: totalCount, 157 | } 158 | }, 159 | getVariables(props, { count, cursor }, fragmentVariables) { 160 | return { 161 | todoListID: props.todoList.id, 162 | ...fragmentVariables, 163 | count, 164 | cursor, 165 | } 166 | }, 167 | query: graphql` 168 | query TodoListItemsPaginationQuery( 169 | $todoListID: ID 170 | $count: Int! 171 | $cursor: String 172 | $filter: TodoListItemsFilterEnum 173 | ) { 174 | viewer { 175 | todoList(id: $todoListID) { 176 | ...TodoListItems_todoList 177 | } 178 | } 179 | } 180 | `, 181 | }, 182 | ) 183 | 184 | registerTodoListItemsConnectionName('TodoListItems_items') 185 | -------------------------------------------------------------------------------- /src/client/TodoApp/mutations/_Mutation.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { commitMutation } from 'react-relay' 4 | import type { CommitMutationConfig } from 'react-relay' 5 | import type { RelayEnvironment } from 'relay-runtime' 6 | import uuidv4 from 'uuid/v4' 7 | import validate from 'validate.js' 8 | import type { Constraints } from 'validate.js' 9 | 10 | /** 11 | * Type defination of a typical input object. 12 | */ 13 | export type Input = {| 14 | [string]: any, 15 | |}; 16 | 17 | /** 18 | * Type defination of a typical option object. 19 | */ 20 | export type Options = ?{||}; 21 | 22 | /** 23 | * Defination for validation errors. 24 | */ 25 | export type ValidationErrors = { [key: string]: Array }; 26 | 27 | class MutationValidationError extends Error { 28 | object: ValidationErrors 29 | 30 | constructor(message: string, object: ValidationErrors) { 31 | super(message) 32 | this.object = object 33 | } 34 | } 35 | 36 | /** 37 | * A base abstract Mutation class to support input validation. 38 | */ 39 | export default class Mutation { 40 | /** 41 | * Default input 42 | */ 43 | static defaultInput = ({}: $Shape) 44 | 45 | /** 46 | * Constraints that is used for validate.js 47 | */ 48 | static constraints = ({}: Constraints) 49 | 50 | /** 51 | * The GraphQL mutation query 52 | */ 53 | static mutation = (undefined: any) 54 | 55 | /** 56 | * The input variable name in the GraphQL mutation query 57 | */ 58 | static inputName = 'input' 59 | 60 | /** 61 | * Mutation configurations 62 | * @see {@link https://facebook.github.io/relay/docs/mutations.html|Relay Mutations} 63 | */ 64 | getMutationConfig(): $Shape { 65 | return (this.constructor.mutationConfig) 66 | } 67 | 68 | static mutationConfig = ({}: $Shape) 69 | 70 | /** 71 | * A static function to update the input of a given mutation 72 | * 73 | * @param {Mutation} mutation - A mutation. 74 | * @param {Input} inputChanges - An object that contains the input of mutation. 75 | * @returns {Mutation} 76 | */ 77 | static updateInput>>(mutation: MT, inputChanges: $Shape): MT { 78 | if ( 79 | !mutation || 80 | !mutation.environment || 81 | typeof mutation.input !== 'object' || 82 | typeof mutation.constructor !== 'function' 83 | ) { 84 | const message = `${this.name}.updateInput accepts a mutation object as the first argument, ` + 85 | 'and the changes as the second argument.' 86 | throw new Error(message) 87 | } 88 | 89 | const newInput = { 90 | ...mutation.input, 91 | ...inputChanges, 92 | } 93 | 94 | const { options } = mutation 95 | 96 | return new mutation.constructor(mutation.environment, newInput, options) 97 | } 98 | 99 | _id: string; 100 | _environment: RelayEnvironment; 101 | _input: $Shape; 102 | _options: ?O; 103 | 104 | _validated: boolean; 105 | _errors: ValidationErrors; 106 | _asyncValidationPromise: Promise; 107 | 108 | /** 109 | * Constructor of a new Mutation. 110 | * 111 | * @param {RelayEnvironment} environment - The Relay Environment. 112 | * @param {Input} input - An object that contains the input of mutation. 113 | */ 114 | constructor(environment: RelayEnvironment, input?: $Shape, options?: O) { 115 | const { defaultInput } = this.constructor 116 | 117 | this._id = uuidv4() 118 | this._environment = environment 119 | this._input = Object.freeze({ 120 | ...defaultInput, 121 | ...input, 122 | }) 123 | this._options = options 124 | this._validated = false 125 | } 126 | 127 | /** 128 | * Getter of the Mutation ID. 129 | * 130 | * $FlowFixMe 131 | */ 132 | get id(): string { 133 | return this._id 134 | } 135 | 136 | /** 137 | * Getter of the Relay Environment. 138 | * 139 | * $FlowFixMe 140 | */ 141 | get environment(): RelayEnvironment { 142 | return this._environment 143 | } 144 | 145 | /** 146 | * Getter of the input object. 147 | * 148 | * $FlowFixMe 149 | */ 150 | get input(): $Shape { 151 | return this._input 152 | } 153 | 154 | /** 155 | * Getter of the options object. 156 | * 157 | * $FlowFixMe 158 | */ 159 | get options(): O | {} { 160 | return this._options || {} 161 | } 162 | 163 | /** 164 | * Getter of validation errors. 165 | * 166 | * $FlowFixMe 167 | */ 168 | get errors(): ValidationErrors { 169 | this._validate() 170 | return this._errors 171 | } 172 | 173 | /** 174 | * Getter of valid status. 175 | * 176 | * $FlowFixMe 177 | */ 178 | get isValid(): Promise { 179 | this._validate() 180 | return !this._errors 181 | } 182 | 183 | /** 184 | * Getter of async validation errors. 185 | */ 186 | getIsValidAsync = async (): Promise => { 187 | const errors = await this._validateAsync() 188 | return !errors 189 | } 190 | 191 | /** 192 | * Getter of async valid status. 193 | */ 194 | getErrorsAsync = async (): Promise => { 195 | const errors = await this._validateAsync() 196 | return errors 197 | } 198 | 199 | _validate = () => { 200 | if (this._validated) return 201 | this._errors = validate(this.input, this.constructor.constraints) 202 | this._validated = true 203 | } 204 | 205 | _validateAsync = () => { 206 | // Reuse the existing promise 207 | if (this._asyncValidationPromise) return this._asyncValidationPromise 208 | 209 | // Or create the promise of async validation 210 | // $FlowFixMe 211 | this._asyncValidationPromise = new Promise((resolve) => { 212 | validate.async(this.input, this.constructor.constraints).then(() => { 213 | resolve() 214 | }, (errors: ValidationErrors) => { 215 | resolve(errors) 216 | }) 217 | }) 218 | 219 | return this._asyncValidationPromise 220 | } 221 | 222 | /** 223 | * Commit the mutation. 224 | */ 225 | commit = (options?: {| throw?: boolean |}): Promise<{ response: ?Object, errors: ?[Error]} > => { 226 | if (!this.isValid) { 227 | const { errors } = this 228 | const fullMessage = 229 | Object.keys(errors).map(k => errors[k].join(', ')).join(', ') 230 | const error = new MutationValidationError(fullMessage, errors) 231 | 232 | if (!options || options.throw !== false) { 233 | throw error 234 | } 235 | 236 | return Promise.resolve({ response: null, errors: [error] }) 237 | } 238 | 239 | const { mutation, inputName } = this.constructor 240 | if (!mutation) throw new Error(`The mutation of ${this.constructor.name} is undefined`) 241 | const { id: clientMutationId, environment, input } = this 242 | const mutationConfig = this.getMutationConfig() 243 | 244 | return new Promise((resolve, reject) => { 245 | commitMutation( 246 | environment, 247 | { 248 | mutation, 249 | ...mutationConfig, 250 | variables: { 251 | [inputName]: { 252 | ...input, 253 | clientMutationId, 254 | }, 255 | ...mutationConfig.variables, 256 | }, 257 | onCompleted: (response, errors) => resolve({ response, errors }), 258 | onError: error => reject(error), 259 | }, 260 | ) 261 | }) 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /src/client/TodoApp/mutations/__generated__/UpdateTodoItemMutation.graphql.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | * @relayHash 1a7868323402a432b0f4d222d1c2465c 4 | */ 5 | 6 | /* eslint-disable */ 7 | 8 | 'use strict'; 9 | 10 | /*:: 11 | import type {ConcreteBatch} from 'relay-runtime'; 12 | export type UpdateTodoItemMutationVariables = {| 13 | input: { 14 | todoItemID: string; 15 | title?: ?string; 16 | completed?: ?boolean; 17 | clientMutationId?: ?string; 18 | }; 19 | |}; 20 | export type UpdateTodoItemMutationResponse = {| 21 | +updateTodoItem: ?{| 22 | +todoItem: {| 23 | +id: string; 24 | +title: string; 25 | +completed: boolean; 26 | |}; 27 | +todoList: {| 28 | +id: string; 29 | +completedItemsCount: number; 30 | +activeItemsCount: number; 31 | |}; 32 | |}; 33 | |}; 34 | */ 35 | 36 | 37 | /* 38 | mutation UpdateTodoItemMutation( 39 | $input: UpdateTodoItemInput! 40 | ) { 41 | updateTodoItem(input: $input) { 42 | todoItem { 43 | id 44 | title 45 | completed 46 | } 47 | todoList { 48 | id 49 | completedItemsCount 50 | activeItemsCount 51 | } 52 | } 53 | } 54 | */ 55 | 56 | const batch /*: ConcreteBatch*/ = { 57 | "fragment": { 58 | "argumentDefinitions": [ 59 | { 60 | "kind": "LocalArgument", 61 | "name": "input", 62 | "type": "UpdateTodoItemInput!", 63 | "defaultValue": null 64 | } 65 | ], 66 | "kind": "Fragment", 67 | "metadata": null, 68 | "name": "UpdateTodoItemMutation", 69 | "selections": [ 70 | { 71 | "kind": "LinkedField", 72 | "alias": null, 73 | "args": [ 74 | { 75 | "kind": "Variable", 76 | "name": "input", 77 | "variableName": "input", 78 | "type": "UpdateTodoItemInput!" 79 | } 80 | ], 81 | "concreteType": "UpdateTodoItemPayload", 82 | "name": "updateTodoItem", 83 | "plural": false, 84 | "selections": [ 85 | { 86 | "kind": "LinkedField", 87 | "alias": null, 88 | "args": null, 89 | "concreteType": "TodoItem", 90 | "name": "todoItem", 91 | "plural": false, 92 | "selections": [ 93 | { 94 | "kind": "ScalarField", 95 | "alias": null, 96 | "args": null, 97 | "name": "id", 98 | "storageKey": null 99 | }, 100 | { 101 | "kind": "ScalarField", 102 | "alias": null, 103 | "args": null, 104 | "name": "title", 105 | "storageKey": null 106 | }, 107 | { 108 | "kind": "ScalarField", 109 | "alias": null, 110 | "args": null, 111 | "name": "completed", 112 | "storageKey": null 113 | } 114 | ], 115 | "storageKey": null 116 | }, 117 | { 118 | "kind": "LinkedField", 119 | "alias": null, 120 | "args": null, 121 | "concreteType": "TodoList", 122 | "name": "todoList", 123 | "plural": false, 124 | "selections": [ 125 | { 126 | "kind": "ScalarField", 127 | "alias": null, 128 | "args": null, 129 | "name": "id", 130 | "storageKey": null 131 | }, 132 | { 133 | "kind": "ScalarField", 134 | "alias": null, 135 | "args": null, 136 | "name": "completedItemsCount", 137 | "storageKey": null 138 | }, 139 | { 140 | "kind": "ScalarField", 141 | "alias": null, 142 | "args": null, 143 | "name": "activeItemsCount", 144 | "storageKey": null 145 | } 146 | ], 147 | "storageKey": null 148 | } 149 | ], 150 | "storageKey": null 151 | } 152 | ], 153 | "type": "Mutation" 154 | }, 155 | "id": null, 156 | "kind": "Batch", 157 | "metadata": {}, 158 | "name": "UpdateTodoItemMutation", 159 | "query": { 160 | "argumentDefinitions": [ 161 | { 162 | "kind": "LocalArgument", 163 | "name": "input", 164 | "type": "UpdateTodoItemInput!", 165 | "defaultValue": null 166 | } 167 | ], 168 | "kind": "Root", 169 | "name": "UpdateTodoItemMutation", 170 | "operation": "mutation", 171 | "selections": [ 172 | { 173 | "kind": "LinkedField", 174 | "alias": null, 175 | "args": [ 176 | { 177 | "kind": "Variable", 178 | "name": "input", 179 | "variableName": "input", 180 | "type": "UpdateTodoItemInput!" 181 | } 182 | ], 183 | "concreteType": "UpdateTodoItemPayload", 184 | "name": "updateTodoItem", 185 | "plural": false, 186 | "selections": [ 187 | { 188 | "kind": "LinkedField", 189 | "alias": null, 190 | "args": null, 191 | "concreteType": "TodoItem", 192 | "name": "todoItem", 193 | "plural": false, 194 | "selections": [ 195 | { 196 | "kind": "ScalarField", 197 | "alias": null, 198 | "args": null, 199 | "name": "id", 200 | "storageKey": null 201 | }, 202 | { 203 | "kind": "ScalarField", 204 | "alias": null, 205 | "args": null, 206 | "name": "title", 207 | "storageKey": null 208 | }, 209 | { 210 | "kind": "ScalarField", 211 | "alias": null, 212 | "args": null, 213 | "name": "completed", 214 | "storageKey": null 215 | } 216 | ], 217 | "storageKey": null 218 | }, 219 | { 220 | "kind": "LinkedField", 221 | "alias": null, 222 | "args": null, 223 | "concreteType": "TodoList", 224 | "name": "todoList", 225 | "plural": false, 226 | "selections": [ 227 | { 228 | "kind": "ScalarField", 229 | "alias": null, 230 | "args": null, 231 | "name": "id", 232 | "storageKey": null 233 | }, 234 | { 235 | "kind": "ScalarField", 236 | "alias": null, 237 | "args": null, 238 | "name": "completedItemsCount", 239 | "storageKey": null 240 | }, 241 | { 242 | "kind": "ScalarField", 243 | "alias": null, 244 | "args": null, 245 | "name": "activeItemsCount", 246 | "storageKey": null 247 | } 248 | ], 249 | "storageKey": null 250 | } 251 | ], 252 | "storageKey": null 253 | } 254 | ] 255 | }, 256 | "text": "mutation UpdateTodoItemMutation(\n $input: UpdateTodoItemInput!\n) {\n updateTodoItem(input: $input) {\n todoItem {\n id\n title\n completed\n }\n todoList {\n id\n completedItemsCount\n activeItemsCount\n }\n }\n}\n" 257 | }; 258 | 259 | module.exports = batch; 260 | -------------------------------------------------------------------------------- /src/client/TodoApp/subscriptions/__generated__/ItemsOnTodoListUpdatedSubscription.graphql.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | * @relayHash ce410c4b75739dd70bfd392f38a83a0b 4 | */ 5 | 6 | /* eslint-disable */ 7 | 8 | 'use strict'; 9 | 10 | /*:: 11 | import type {ConcreteBatch} from 'relay-runtime'; 12 | export type ItemsOnTodoListUpdatedSubscriptionVariables = {| 13 | todoListID: string; 14 | |}; 15 | export type ItemsOnTodoListUpdatedSubscriptionResponse = {| 16 | +itemsOnTodoListUpdated: ?{| 17 | +updatedTodoItemIDs: $ReadOnlyArray; 18 | +changes: {| 19 | +title: ?string; 20 | +completed: ?boolean; 21 | |}; 22 | +todoList: {| 23 | +id: string; 24 | +completedItemsCount: number; 25 | +activeItemsCount: number; 26 | |}; 27 | |}; 28 | |}; 29 | */ 30 | 31 | 32 | /* 33 | subscription ItemsOnTodoListUpdatedSubscription( 34 | $todoListID: ID! 35 | ) { 36 | itemsOnTodoListUpdated(todoListID: $todoListID) { 37 | updatedTodoItemIDs 38 | changes { 39 | title 40 | completed 41 | } 42 | todoList { 43 | id 44 | completedItemsCount 45 | activeItemsCount 46 | } 47 | } 48 | } 49 | */ 50 | 51 | const batch /*: ConcreteBatch*/ = { 52 | "fragment": { 53 | "argumentDefinitions": [ 54 | { 55 | "kind": "LocalArgument", 56 | "name": "todoListID", 57 | "type": "ID!", 58 | "defaultValue": null 59 | } 60 | ], 61 | "kind": "Fragment", 62 | "metadata": null, 63 | "name": "ItemsOnTodoListUpdatedSubscription", 64 | "selections": [ 65 | { 66 | "kind": "LinkedField", 67 | "alias": null, 68 | "args": [ 69 | { 70 | "kind": "Variable", 71 | "name": "todoListID", 72 | "variableName": "todoListID", 73 | "type": "ID!" 74 | } 75 | ], 76 | "concreteType": "ItemsOnTodoListUpdated", 77 | "name": "itemsOnTodoListUpdated", 78 | "plural": false, 79 | "selections": [ 80 | { 81 | "kind": "ScalarField", 82 | "alias": null, 83 | "args": null, 84 | "name": "updatedTodoItemIDs", 85 | "storageKey": null 86 | }, 87 | { 88 | "kind": "LinkedField", 89 | "alias": null, 90 | "args": null, 91 | "concreteType": "ItemsOnTodoListUpdatedChanges", 92 | "name": "changes", 93 | "plural": false, 94 | "selections": [ 95 | { 96 | "kind": "ScalarField", 97 | "alias": null, 98 | "args": null, 99 | "name": "title", 100 | "storageKey": null 101 | }, 102 | { 103 | "kind": "ScalarField", 104 | "alias": null, 105 | "args": null, 106 | "name": "completed", 107 | "storageKey": null 108 | } 109 | ], 110 | "storageKey": null 111 | }, 112 | { 113 | "kind": "LinkedField", 114 | "alias": null, 115 | "args": null, 116 | "concreteType": "TodoList", 117 | "name": "todoList", 118 | "plural": false, 119 | "selections": [ 120 | { 121 | "kind": "ScalarField", 122 | "alias": null, 123 | "args": null, 124 | "name": "id", 125 | "storageKey": null 126 | }, 127 | { 128 | "kind": "ScalarField", 129 | "alias": null, 130 | "args": null, 131 | "name": "completedItemsCount", 132 | "storageKey": null 133 | }, 134 | { 135 | "kind": "ScalarField", 136 | "alias": null, 137 | "args": null, 138 | "name": "activeItemsCount", 139 | "storageKey": null 140 | } 141 | ], 142 | "storageKey": null 143 | } 144 | ], 145 | "storageKey": null 146 | } 147 | ], 148 | "type": "Subscription" 149 | }, 150 | "id": null, 151 | "kind": "Batch", 152 | "metadata": {}, 153 | "name": "ItemsOnTodoListUpdatedSubscription", 154 | "query": { 155 | "argumentDefinitions": [ 156 | { 157 | "kind": "LocalArgument", 158 | "name": "todoListID", 159 | "type": "ID!", 160 | "defaultValue": null 161 | } 162 | ], 163 | "kind": "Root", 164 | "name": "ItemsOnTodoListUpdatedSubscription", 165 | "operation": "subscription", 166 | "selections": [ 167 | { 168 | "kind": "LinkedField", 169 | "alias": null, 170 | "args": [ 171 | { 172 | "kind": "Variable", 173 | "name": "todoListID", 174 | "variableName": "todoListID", 175 | "type": "ID!" 176 | } 177 | ], 178 | "concreteType": "ItemsOnTodoListUpdated", 179 | "name": "itemsOnTodoListUpdated", 180 | "plural": false, 181 | "selections": [ 182 | { 183 | "kind": "ScalarField", 184 | "alias": null, 185 | "args": null, 186 | "name": "updatedTodoItemIDs", 187 | "storageKey": null 188 | }, 189 | { 190 | "kind": "LinkedField", 191 | "alias": null, 192 | "args": null, 193 | "concreteType": "ItemsOnTodoListUpdatedChanges", 194 | "name": "changes", 195 | "plural": false, 196 | "selections": [ 197 | { 198 | "kind": "ScalarField", 199 | "alias": null, 200 | "args": null, 201 | "name": "title", 202 | "storageKey": null 203 | }, 204 | { 205 | "kind": "ScalarField", 206 | "alias": null, 207 | "args": null, 208 | "name": "completed", 209 | "storageKey": null 210 | } 211 | ], 212 | "storageKey": null 213 | }, 214 | { 215 | "kind": "LinkedField", 216 | "alias": null, 217 | "args": null, 218 | "concreteType": "TodoList", 219 | "name": "todoList", 220 | "plural": false, 221 | "selections": [ 222 | { 223 | "kind": "ScalarField", 224 | "alias": null, 225 | "args": null, 226 | "name": "id", 227 | "storageKey": null 228 | }, 229 | { 230 | "kind": "ScalarField", 231 | "alias": null, 232 | "args": null, 233 | "name": "completedItemsCount", 234 | "storageKey": null 235 | }, 236 | { 237 | "kind": "ScalarField", 238 | "alias": null, 239 | "args": null, 240 | "name": "activeItemsCount", 241 | "storageKey": null 242 | } 243 | ], 244 | "storageKey": null 245 | } 246 | ], 247 | "storageKey": null 248 | } 249 | ] 250 | }, 251 | "text": "subscription ItemsOnTodoListUpdatedSubscription(\n $todoListID: ID!\n) {\n itemsOnTodoListUpdated(todoListID: $todoListID) {\n updatedTodoItemIDs\n changes {\n title\n completed\n }\n todoList {\n id\n completedItemsCount\n activeItemsCount\n }\n }\n}\n" 252 | }; 253 | 254 | module.exports = batch; 255 | -------------------------------------------------------------------------------- /src/client/TodoApp/subscriptions/__generated__/ItemOnTodoListUpdatedSubscription.graphql.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | * @relayHash 5896a9d36b60a019eb530dbd2526fb6d 4 | */ 5 | 6 | /* eslint-disable */ 7 | 8 | 'use strict'; 9 | 10 | /*:: 11 | import type {ConcreteBatch} from 'relay-runtime'; 12 | export type ItemOnTodoListUpdatedSubscriptionVariables = {| 13 | todoListID: string; 14 | |}; 15 | export type ItemOnTodoListUpdatedSubscriptionResponse = {| 16 | +itemOnTodoListUpdated: ?{| 17 | +todoItem: {| 18 | +id: string; 19 | +completed: boolean; 20 | +title: string; 21 | +listID: string; 22 | |}; 23 | +todoList: {| 24 | +id: string; 25 | +completedItemsCount: number; 26 | +activeItemsCount: number; 27 | |}; 28 | |}; 29 | |}; 30 | */ 31 | 32 | 33 | /* 34 | subscription ItemOnTodoListUpdatedSubscription( 35 | $todoListID: ID! 36 | ) { 37 | itemOnTodoListUpdated(todoListID: $todoListID) { 38 | todoItem { 39 | id 40 | completed 41 | title 42 | listID 43 | } 44 | todoList { 45 | id 46 | completedItemsCount 47 | activeItemsCount 48 | } 49 | } 50 | } 51 | */ 52 | 53 | const batch /*: ConcreteBatch*/ = { 54 | "fragment": { 55 | "argumentDefinitions": [ 56 | { 57 | "kind": "LocalArgument", 58 | "name": "todoListID", 59 | "type": "ID!", 60 | "defaultValue": null 61 | } 62 | ], 63 | "kind": "Fragment", 64 | "metadata": null, 65 | "name": "ItemOnTodoListUpdatedSubscription", 66 | "selections": [ 67 | { 68 | "kind": "LinkedField", 69 | "alias": null, 70 | "args": [ 71 | { 72 | "kind": "Variable", 73 | "name": "todoListID", 74 | "variableName": "todoListID", 75 | "type": "ID!" 76 | } 77 | ], 78 | "concreteType": "ItemOnTodoListUpdated", 79 | "name": "itemOnTodoListUpdated", 80 | "plural": false, 81 | "selections": [ 82 | { 83 | "kind": "LinkedField", 84 | "alias": null, 85 | "args": null, 86 | "concreteType": "TodoItem", 87 | "name": "todoItem", 88 | "plural": false, 89 | "selections": [ 90 | { 91 | "kind": "ScalarField", 92 | "alias": null, 93 | "args": null, 94 | "name": "id", 95 | "storageKey": null 96 | }, 97 | { 98 | "kind": "ScalarField", 99 | "alias": null, 100 | "args": null, 101 | "name": "completed", 102 | "storageKey": null 103 | }, 104 | { 105 | "kind": "ScalarField", 106 | "alias": null, 107 | "args": null, 108 | "name": "title", 109 | "storageKey": null 110 | }, 111 | { 112 | "kind": "ScalarField", 113 | "alias": null, 114 | "args": null, 115 | "name": "listID", 116 | "storageKey": null 117 | } 118 | ], 119 | "storageKey": null 120 | }, 121 | { 122 | "kind": "LinkedField", 123 | "alias": null, 124 | "args": null, 125 | "concreteType": "TodoList", 126 | "name": "todoList", 127 | "plural": false, 128 | "selections": [ 129 | { 130 | "kind": "ScalarField", 131 | "alias": null, 132 | "args": null, 133 | "name": "id", 134 | "storageKey": null 135 | }, 136 | { 137 | "kind": "ScalarField", 138 | "alias": null, 139 | "args": null, 140 | "name": "completedItemsCount", 141 | "storageKey": null 142 | }, 143 | { 144 | "kind": "ScalarField", 145 | "alias": null, 146 | "args": null, 147 | "name": "activeItemsCount", 148 | "storageKey": null 149 | } 150 | ], 151 | "storageKey": null 152 | } 153 | ], 154 | "storageKey": null 155 | } 156 | ], 157 | "type": "Subscription" 158 | }, 159 | "id": null, 160 | "kind": "Batch", 161 | "metadata": {}, 162 | "name": "ItemOnTodoListUpdatedSubscription", 163 | "query": { 164 | "argumentDefinitions": [ 165 | { 166 | "kind": "LocalArgument", 167 | "name": "todoListID", 168 | "type": "ID!", 169 | "defaultValue": null 170 | } 171 | ], 172 | "kind": "Root", 173 | "name": "ItemOnTodoListUpdatedSubscription", 174 | "operation": "subscription", 175 | "selections": [ 176 | { 177 | "kind": "LinkedField", 178 | "alias": null, 179 | "args": [ 180 | { 181 | "kind": "Variable", 182 | "name": "todoListID", 183 | "variableName": "todoListID", 184 | "type": "ID!" 185 | } 186 | ], 187 | "concreteType": "ItemOnTodoListUpdated", 188 | "name": "itemOnTodoListUpdated", 189 | "plural": false, 190 | "selections": [ 191 | { 192 | "kind": "LinkedField", 193 | "alias": null, 194 | "args": null, 195 | "concreteType": "TodoItem", 196 | "name": "todoItem", 197 | "plural": false, 198 | "selections": [ 199 | { 200 | "kind": "ScalarField", 201 | "alias": null, 202 | "args": null, 203 | "name": "id", 204 | "storageKey": null 205 | }, 206 | { 207 | "kind": "ScalarField", 208 | "alias": null, 209 | "args": null, 210 | "name": "completed", 211 | "storageKey": null 212 | }, 213 | { 214 | "kind": "ScalarField", 215 | "alias": null, 216 | "args": null, 217 | "name": "title", 218 | "storageKey": null 219 | }, 220 | { 221 | "kind": "ScalarField", 222 | "alias": null, 223 | "args": null, 224 | "name": "listID", 225 | "storageKey": null 226 | } 227 | ], 228 | "storageKey": null 229 | }, 230 | { 231 | "kind": "LinkedField", 232 | "alias": null, 233 | "args": null, 234 | "concreteType": "TodoList", 235 | "name": "todoList", 236 | "plural": false, 237 | "selections": [ 238 | { 239 | "kind": "ScalarField", 240 | "alias": null, 241 | "args": null, 242 | "name": "id", 243 | "storageKey": null 244 | }, 245 | { 246 | "kind": "ScalarField", 247 | "alias": null, 248 | "args": null, 249 | "name": "completedItemsCount", 250 | "storageKey": null 251 | }, 252 | { 253 | "kind": "ScalarField", 254 | "alias": null, 255 | "args": null, 256 | "name": "activeItemsCount", 257 | "storageKey": null 258 | } 259 | ], 260 | "storageKey": null 261 | } 262 | ], 263 | "storageKey": null 264 | } 265 | ] 266 | }, 267 | "text": "subscription ItemOnTodoListUpdatedSubscription(\n $todoListID: ID!\n) {\n itemOnTodoListUpdated(todoListID: $todoListID) {\n todoItem {\n id\n completed\n title\n listID\n }\n todoList {\n id\n completedItemsCount\n activeItemsCount\n }\n }\n}\n" 268 | }; 269 | 270 | module.exports = batch; 271 | -------------------------------------------------------------------------------- /src/client/TodoApp/mutations/__generated__/CreateTodoItemMutation.graphql.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | * @relayHash 3adc755d1f8da8b386f1681e228a2854 4 | */ 5 | 6 | /* eslint-disable */ 7 | 8 | 'use strict'; 9 | 10 | /*:: 11 | import type {ConcreteBatch} from 'relay-runtime'; 12 | export type CreateTodoItemMutationVariables = {| 13 | input: { 14 | todoListID: string; 15 | title: string; 16 | completed?: ?boolean; 17 | clientMutationId?: ?string; 18 | }; 19 | |}; 20 | export type CreateTodoItemMutationResponse = {| 21 | +createTodoItem: ?{| 22 | +todoListItemsConnectionEdge: {| 23 | +cursor: string; 24 | +node: ?{| 25 | +completed: boolean; 26 | +id: string; 27 | +title: string; 28 | |}; 29 | |}; 30 | +todoList: {| 31 | +id: string; 32 | +itemsCount: number; 33 | +completedItemsCount: number; 34 | +activeItemsCount: number; 35 | |}; 36 | |}; 37 | |}; 38 | */ 39 | 40 | 41 | /* 42 | mutation CreateTodoItemMutation( 43 | $input: CreateTodoItemInput! 44 | ) { 45 | createTodoItem(input: $input) { 46 | todoListItemsConnectionEdge { 47 | cursor 48 | node { 49 | completed 50 | id 51 | title 52 | } 53 | } 54 | todoList { 55 | id 56 | itemsCount 57 | completedItemsCount 58 | activeItemsCount 59 | } 60 | } 61 | } 62 | */ 63 | 64 | const batch /*: ConcreteBatch*/ = { 65 | "fragment": { 66 | "argumentDefinitions": [ 67 | { 68 | "kind": "LocalArgument", 69 | "name": "input", 70 | "type": "CreateTodoItemInput!", 71 | "defaultValue": null 72 | } 73 | ], 74 | "kind": "Fragment", 75 | "metadata": null, 76 | "name": "CreateTodoItemMutation", 77 | "selections": [ 78 | { 79 | "kind": "LinkedField", 80 | "alias": null, 81 | "args": [ 82 | { 83 | "kind": "Variable", 84 | "name": "input", 85 | "variableName": "input", 86 | "type": "CreateTodoItemInput!" 87 | } 88 | ], 89 | "concreteType": "CreateTodoItemPayload", 90 | "name": "createTodoItem", 91 | "plural": false, 92 | "selections": [ 93 | { 94 | "kind": "LinkedField", 95 | "alias": null, 96 | "args": null, 97 | "concreteType": "TodoItemEdge", 98 | "name": "todoListItemsConnectionEdge", 99 | "plural": false, 100 | "selections": [ 101 | { 102 | "kind": "ScalarField", 103 | "alias": null, 104 | "args": null, 105 | "name": "cursor", 106 | "storageKey": null 107 | }, 108 | { 109 | "kind": "LinkedField", 110 | "alias": null, 111 | "args": null, 112 | "concreteType": "TodoItem", 113 | "name": "node", 114 | "plural": false, 115 | "selections": [ 116 | { 117 | "kind": "ScalarField", 118 | "alias": null, 119 | "args": null, 120 | "name": "completed", 121 | "storageKey": null 122 | }, 123 | { 124 | "kind": "ScalarField", 125 | "alias": null, 126 | "args": null, 127 | "name": "id", 128 | "storageKey": null 129 | }, 130 | { 131 | "kind": "ScalarField", 132 | "alias": null, 133 | "args": null, 134 | "name": "title", 135 | "storageKey": null 136 | } 137 | ], 138 | "storageKey": null 139 | } 140 | ], 141 | "storageKey": null 142 | }, 143 | { 144 | "kind": "LinkedField", 145 | "alias": null, 146 | "args": null, 147 | "concreteType": "TodoList", 148 | "name": "todoList", 149 | "plural": false, 150 | "selections": [ 151 | { 152 | "kind": "ScalarField", 153 | "alias": null, 154 | "args": null, 155 | "name": "id", 156 | "storageKey": null 157 | }, 158 | { 159 | "kind": "ScalarField", 160 | "alias": null, 161 | "args": null, 162 | "name": "itemsCount", 163 | "storageKey": null 164 | }, 165 | { 166 | "kind": "ScalarField", 167 | "alias": null, 168 | "args": null, 169 | "name": "completedItemsCount", 170 | "storageKey": null 171 | }, 172 | { 173 | "kind": "ScalarField", 174 | "alias": null, 175 | "args": null, 176 | "name": "activeItemsCount", 177 | "storageKey": null 178 | } 179 | ], 180 | "storageKey": null 181 | } 182 | ], 183 | "storageKey": null 184 | } 185 | ], 186 | "type": "Mutation" 187 | }, 188 | "id": null, 189 | "kind": "Batch", 190 | "metadata": {}, 191 | "name": "CreateTodoItemMutation", 192 | "query": { 193 | "argumentDefinitions": [ 194 | { 195 | "kind": "LocalArgument", 196 | "name": "input", 197 | "type": "CreateTodoItemInput!", 198 | "defaultValue": null 199 | } 200 | ], 201 | "kind": "Root", 202 | "name": "CreateTodoItemMutation", 203 | "operation": "mutation", 204 | "selections": [ 205 | { 206 | "kind": "LinkedField", 207 | "alias": null, 208 | "args": [ 209 | { 210 | "kind": "Variable", 211 | "name": "input", 212 | "variableName": "input", 213 | "type": "CreateTodoItemInput!" 214 | } 215 | ], 216 | "concreteType": "CreateTodoItemPayload", 217 | "name": "createTodoItem", 218 | "plural": false, 219 | "selections": [ 220 | { 221 | "kind": "LinkedField", 222 | "alias": null, 223 | "args": null, 224 | "concreteType": "TodoItemEdge", 225 | "name": "todoListItemsConnectionEdge", 226 | "plural": false, 227 | "selections": [ 228 | { 229 | "kind": "ScalarField", 230 | "alias": null, 231 | "args": null, 232 | "name": "cursor", 233 | "storageKey": null 234 | }, 235 | { 236 | "kind": "LinkedField", 237 | "alias": null, 238 | "args": null, 239 | "concreteType": "TodoItem", 240 | "name": "node", 241 | "plural": false, 242 | "selections": [ 243 | { 244 | "kind": "ScalarField", 245 | "alias": null, 246 | "args": null, 247 | "name": "completed", 248 | "storageKey": null 249 | }, 250 | { 251 | "kind": "ScalarField", 252 | "alias": null, 253 | "args": null, 254 | "name": "id", 255 | "storageKey": null 256 | }, 257 | { 258 | "kind": "ScalarField", 259 | "alias": null, 260 | "args": null, 261 | "name": "title", 262 | "storageKey": null 263 | } 264 | ], 265 | "storageKey": null 266 | } 267 | ], 268 | "storageKey": null 269 | }, 270 | { 271 | "kind": "LinkedField", 272 | "alias": null, 273 | "args": null, 274 | "concreteType": "TodoList", 275 | "name": "todoList", 276 | "plural": false, 277 | "selections": [ 278 | { 279 | "kind": "ScalarField", 280 | "alias": null, 281 | "args": null, 282 | "name": "id", 283 | "storageKey": null 284 | }, 285 | { 286 | "kind": "ScalarField", 287 | "alias": null, 288 | "args": null, 289 | "name": "itemsCount", 290 | "storageKey": null 291 | }, 292 | { 293 | "kind": "ScalarField", 294 | "alias": null, 295 | "args": null, 296 | "name": "completedItemsCount", 297 | "storageKey": null 298 | }, 299 | { 300 | "kind": "ScalarField", 301 | "alias": null, 302 | "args": null, 303 | "name": "activeItemsCount", 304 | "storageKey": null 305 | } 306 | ], 307 | "storageKey": null 308 | } 309 | ], 310 | "storageKey": null 311 | } 312 | ] 313 | }, 314 | "text": "mutation CreateTodoItemMutation(\n $input: CreateTodoItemInput!\n) {\n createTodoItem(input: $input) {\n todoListItemsConnectionEdge {\n cursor\n node {\n completed\n id\n title\n }\n }\n todoList {\n id\n itemsCount\n completedItemsCount\n activeItemsCount\n }\n }\n}\n" 315 | }; 316 | 317 | module.exports = batch; 318 | --------------------------------------------------------------------------------