├── 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 |
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 | [](https://deploy.now.sh/?repo=https://github.com/zetavg/graphql-todomvc)
38 | [](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 |
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 |
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{|
14 | +node: ?{|
15 | +id: string;
16 | +name: string;
17 | |};
18 | |}>;
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 |
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 |
92 |
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{|
21 | +cursor: string;
22 | +node: ?{|
23 | +id: string;
24 | |};
25 | |}>;
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 |