├── .DS_Store ├── .gitignore ├── README.md ├── apollo-local-state ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public │ └── index.html ├── src │ ├── cache.tsx │ ├── components │ │ ├── App.tsx │ │ ├── Footer.tsx │ │ ├── Header.tsx │ │ ├── Link.tsx │ │ ├── MainSection.tsx │ │ ├── TodoItem.tsx │ │ ├── TodoList.tsx │ │ └── TodoTextInput.tsx │ ├── containers │ │ ├── Header.tsx │ │ ├── MainSection.tsx │ │ └── VisibleTodoList.tsx │ ├── index.tsx │ ├── models │ │ ├── Todos.tsx │ │ └── VisibilityFilter.tsx │ ├── operations │ │ ├── mutations │ │ │ ├── addTodo │ │ │ │ ├── addTodo.spec.tsx │ │ │ │ └── addTodo.tsx │ │ │ ├── clearCompletedTodos │ │ │ │ ├── clearCompletedTodos.spec.tsx │ │ │ │ └── clearCompletedTodos.tsx │ │ │ ├── completeAllTodos │ │ │ │ └── completeAllTodos.tsx │ │ │ ├── completeTodo │ │ │ │ ├── completeTodo.spec.tsx │ │ │ │ └── completeTodo.tsx │ │ │ ├── deleteTodo │ │ │ │ ├── deleteTodo.spec.tsx │ │ │ │ └── deleteTodo.tsx │ │ │ ├── editTodo │ │ │ │ ├── editTodo.spec.tsx │ │ │ │ └── editTodo.tsx │ │ │ ├── index.tsx │ │ │ └── setVisibilityFilter │ │ │ │ ├── setVisibilityFilter.spec.tsx │ │ │ │ └── setVisibilityFilter.tsx │ │ └── queries │ │ │ ├── getAllTodos.tsx │ │ │ └── getVisibilityFilter.tsx │ ├── react-app-env.d.ts │ └── tests │ │ ├── createMockReactiveVar.spec.ts │ │ ├── createMockReactiveVar.ts │ │ └── mocks │ │ ├── mockTodosVar.ts │ │ └── mockVisibilityFilter.ts └── tsconfig.json ├── apollo-remote-state-advanced-cache-apis ├── README.md ├── client │ ├── .gitignore │ ├── __generated__ │ │ └── globalTypes.ts │ ├── apollo.config.js │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── index.html │ ├── src │ │ ├── cache.tsx │ │ ├── components │ │ │ ├── App.tsx │ │ │ ├── Footer.tsx │ │ │ ├── Header.tsx │ │ │ ├── Link.tsx │ │ │ ├── MainSection.tsx │ │ │ ├── TodoItem.tsx │ │ │ ├── TodoList.tsx │ │ │ └── TodoTextInput.tsx │ │ ├── containers │ │ │ ├── Header.tsx │ │ │ ├── MainSection.tsx │ │ │ └── VisibleTodoList.tsx │ │ ├── index.tsx │ │ ├── models │ │ │ ├── Todos.tsx │ │ │ └── VisibilityFilter.tsx │ │ ├── operations │ │ │ ├── __generated__ │ │ │ │ └── GetAllTodos.ts │ │ │ ├── mutations │ │ │ │ ├── __generated__ │ │ │ │ │ ├── AddTodo.ts │ │ │ │ │ ├── ClearCompletedTodos.ts │ │ │ │ │ ├── CompleteAllTodos.ts │ │ │ │ │ ├── CompleteTodo.ts │ │ │ │ │ ├── DeleteTodo.ts │ │ │ │ │ └── EditTodo.ts │ │ │ │ ├── addTodo.tsx │ │ │ │ ├── clearCompletedTodos.tsx │ │ │ │ ├── completeAllTodos.tsx │ │ │ │ ├── completeTodo.tsx │ │ │ │ ├── deleteTodo.tsx │ │ │ │ ├── editTodo.tsx │ │ │ │ └── setVisibilityFilter │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── setVisibilityFilter.spec.tsx │ │ │ │ │ └── setVisibilityFilter.tsx │ │ │ └── queries │ │ │ │ ├── __generated__ │ │ │ │ └── GetAllTodos.ts │ │ │ │ ├── getAllTodos.tsx │ │ │ │ └── getVisibilityFilter.tsx │ │ ├── react-app-env.d.ts │ │ └── tests │ │ │ ├── createMockReactiveVar.spec.ts │ │ │ ├── createMockReactiveVar.ts │ │ │ └── mocks │ │ │ ├── mockTodosVar.ts │ │ │ └── mockVisibilityFilter.ts │ └── tsconfig.json └── server │ ├── .gitignore │ ├── README.md │ ├── apollo.config.js │ ├── codegen.yml │ ├── graphql.schema.json │ ├── nodemon.json │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── generated │ │ └── graphql.ts │ ├── index.ts │ ├── modules │ │ └── todos │ │ │ └── repos │ │ │ ├── implementations │ │ │ └── InMemoryTodoRepo.ts │ │ │ ├── index.ts │ │ │ └── todoRepo.ts │ ├── resolvers.ts │ ├── schema.ts │ └── shared │ │ ├── core │ │ └── result.ts │ │ ├── mappers │ │ └── todoMapper.ts │ │ └── utils │ │ ├── numberUtils.ts │ │ └── paginationUtils.ts │ └── tsconfig.json ├── apollo-remote-state-no-relay ├── .DS_Store ├── README.md ├── client │ ├── .gitignore │ ├── NOTES.md │ ├── __generated__ │ │ └── globalTypes.ts │ ├── apollo.config.js │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── index.html │ ├── src │ │ ├── cache.spec.ts │ │ ├── cache.tsx │ │ ├── components │ │ │ ├── App.tsx │ │ │ ├── Footer.tsx │ │ │ ├── Header.tsx │ │ │ ├── Link.tsx │ │ │ ├── MainSection.tsx │ │ │ ├── TodoItem.tsx │ │ │ ├── TodoList.tsx │ │ │ └── TodoTextInput.tsx │ │ ├── containers │ │ │ ├── Header.tsx │ │ │ ├── MainSection.tsx │ │ │ └── VisibleTodoList.tsx │ │ ├── index.tsx │ │ ├── models │ │ │ ├── Todos.tsx │ │ │ └── VisibilityFilter.tsx │ │ ├── operations │ │ │ ├── mutations │ │ │ │ ├── __generated__ │ │ │ │ │ ├── AddTodo.ts │ │ │ │ │ ├── ClearCompletedTodos.ts │ │ │ │ │ ├── CompleteAllTodos.ts │ │ │ │ │ ├── CompleteTodo.ts │ │ │ │ │ ├── DeleteTodo.ts │ │ │ │ │ └── EditTodo.ts │ │ │ │ ├── addTodo.tsx │ │ │ │ ├── clearCompletedTodos.tsx │ │ │ │ ├── completeAllTodos.tsx │ │ │ │ ├── completeTodo.tsx │ │ │ │ ├── deleteTodo.tsx │ │ │ │ ├── editTodo.tsx │ │ │ │ └── setVisibilityFilter │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── setVisibilityFilter.spec.tsx │ │ │ │ │ └── setVisibilityFilter.tsx │ │ │ └── queries │ │ │ │ ├── __generated__ │ │ │ │ └── GetAllTodos.ts │ │ │ │ ├── getAllTodos.tsx │ │ │ │ └── getVisibilityFilter.tsx │ │ └── react-app-env.d.ts │ └── tsconfig.json ├── package-lock.json └── server │ ├── .gitignore │ ├── apollo.config.js │ ├── codegen.yml │ ├── graphql.schema.json │ ├── nodemon.json │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── generated │ │ └── graphql.ts │ ├── index.ts │ ├── modules │ │ └── todos │ │ │ └── repos │ │ │ ├── implementations │ │ │ └── InMemoryTodoRepo.ts │ │ │ ├── index.ts │ │ │ └── todoRepo.ts │ ├── resolvers.ts │ ├── schema.ts │ └── shared │ │ ├── core │ │ └── result.ts │ │ ├── mappers │ │ └── todoMapper.ts │ │ └── utils │ │ ├── numberUtils.ts │ │ └── paginationUtils.ts │ └── tsconfig.json ├── apollo-remote-state ├── .DS_Store ├── README.md ├── client │ ├── .gitignore │ ├── __generated__ │ │ └── globalTypes.ts │ ├── apollo.config.js │ ├── graphql-schema.json │ ├── local-schema.graphql │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── index.html │ ├── src │ │ ├── cache.tsx │ │ ├── components │ │ │ ├── App.tsx │ │ │ ├── Footer.tsx │ │ │ ├── Header.tsx │ │ │ ├── Link.tsx │ │ │ ├── MainSection.tsx │ │ │ ├── TodoItem.tsx │ │ │ ├── TodoList.tsx │ │ │ └── TodoTextInput.tsx │ │ ├── containers │ │ │ ├── Header.tsx │ │ │ ├── MainSection.tsx │ │ │ ├── VisibleTodoList.tsx │ │ │ └── __generated__ │ │ │ │ └── GetLastTodos.ts │ │ ├── index.tsx │ │ ├── models │ │ │ ├── Todos.tsx │ │ │ └── VisibilityFilter.tsx │ │ ├── operations │ │ │ ├── __generated__ │ │ │ │ └── GetAllTodos.ts │ │ │ ├── mutations │ │ │ │ ├── __generated__ │ │ │ │ │ ├── AddTodo.ts │ │ │ │ │ ├── ClearCompletedTodos.ts │ │ │ │ │ ├── CompleteAllTodos.ts │ │ │ │ │ ├── CompleteTodo.ts │ │ │ │ │ ├── DeleteTodo.ts │ │ │ │ │ └── EditTodo.ts │ │ │ │ ├── addTodo.tsx │ │ │ │ ├── clearCompletedTodos.tsx │ │ │ │ ├── completeAllTodos.tsx │ │ │ │ ├── completeTodo.tsx │ │ │ │ ├── deleteTodo.tsx │ │ │ │ ├── editTodo.tsx │ │ │ │ └── setVisibilityFilter │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── setVisibilityFilter.spec.tsx │ │ │ │ │ └── setVisibilityFilter.tsx │ │ │ └── queries │ │ │ │ ├── __generated__ │ │ │ │ ├── GetAllTodos.ts │ │ │ │ └── GetVisibilityFilter.ts │ │ │ │ ├── getAllTodos.tsx │ │ │ │ └── getVisibilityFilter.tsx │ │ ├── react-app-env.d.ts │ │ └── tests │ │ │ ├── createMockReactiveVar.spec.ts │ │ │ ├── createMockReactiveVar.ts │ │ │ └── mocks │ │ │ ├── mockTodosVar.ts │ │ │ └── mockVisibilityFilter.ts │ └── tsconfig.json ├── package-lock.json └── server │ ├── .gitignore │ ├── apollo.config.js │ ├── codegen.yml │ ├── graphql.schema.json │ ├── nodemon.json │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── generated │ │ └── graphql.ts │ ├── index.ts │ ├── modules │ │ └── todos │ │ │ └── repos │ │ │ ├── implementations │ │ │ └── InMemoryTodoRepo.ts │ │ │ ├── index.ts │ │ │ └── todoRepo.ts │ ├── resolvers.ts │ ├── schema.ts │ └── shared │ │ ├── core │ │ └── result.ts │ │ ├── mappers │ │ └── todoMapper.ts │ │ └── utils │ │ ├── numberUtils.ts │ │ └── paginationUtils.ts │ └── tsconfig.json ├── package-lock.json └── redux-local-state ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public └── index.html └── src ├── actions ├── index.js └── index.spec.js ├── components ├── App.js ├── App.spec.js ├── Footer.js ├── Footer.spec.js ├── Header.js ├── Header.spec.js ├── Link.js ├── Link.spec.js ├── MainSection.js ├── MainSection.spec.js ├── TodoItem.js ├── TodoItem.spec.js ├── TodoList.js ├── TodoList.spec.js ├── TodoTextInput.js └── TodoTextInput.spec.js ├── constants ├── ActionTypes.js └── TodoFilters.js ├── containers ├── FilterLink.js ├── Header.js ├── MainSection.js └── VisibleTodoList.js ├── index.js ├── reducers ├── index.js ├── todos.js ├── todos.spec.js └── visibilityFilter.js └── selectors └── index.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/ac3-state-management-examples/57ddd7aff0b86e12ea7f63db625c004add712ba4/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/ac3-state-management-examples/57ddd7aff0b86e12ea7f63db625c004add712ba4/.gitignore -------------------------------------------------------------------------------- /apollo-local-state/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # production 7 | build 8 | 9 | # misc 10 | .DS_Store 11 | npm-debug.log 12 | -------------------------------------------------------------------------------- /apollo-local-state/README.md: -------------------------------------------------------------------------------- 1 | # apollo-local-state 2 | 3 | > Summary: Using Apollo Client 3's **Reactive Variables API**, we can store the entire application state locally (and optionally persist it using local storage). 4 | 5 | The key components that make this work are: 6 | 7 | - Reactive Variables: Holds onto local state. Perform _local mutations_ against them. Used in conjunction with cache policies, can act as a replacement for client-side resolvers from AC2.x. 8 | - Cache Policies: Can perform pre-processing on any `query` or `mutation` before the field is returned. Combining this with Reactive Variables broadcasts changes to all client-side queries. 9 | 10 | ## Getting started 11 | 12 | To start the project, run the following commands: 13 | 14 | ``` 15 | npm install && npm run start 16 | ``` 17 | 18 | The app should start at `http://localhost:3000/`. 19 | -------------------------------------------------------------------------------- /apollo-local-state/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todomvc-apollo", 3 | "version": "0.0.1", 4 | "private": true, 5 | "devDependencies": { 6 | "@types/classnames": "^2.2.9", 7 | "@types/react-test-renderer": "^16.9.2", 8 | "react-scripts": "^3.0.0", 9 | "react-test-renderer": "^16.8.6" 10 | }, 11 | "dependencies": { 12 | "@apollo/client": "^3.2.7", 13 | "@types/jest": "^25.1.3", 14 | "@types/node": "^13.7.6", 15 | "@types/react": "^16.9.23", 16 | "@types/react-dom": "^16.9.5", 17 | "classnames": "^2.2.6", 18 | "graphql": "^14.6.0", 19 | "pkg.json": "^2.0.8", 20 | "prop-types": "^15.7.2", 21 | "react": "^16.8.6", 22 | "react-dom": "^16.8.6", 23 | "reselect": "^4.0.0", 24 | "todomvc-app-css": "^2.2.0", 25 | "typescript": "^3.8.2" 26 | }, 27 | "scripts": { 28 | "start": "react-scripts start", 29 | "build": "react-scripts build", 30 | "eject": "react-scripts eject", 31 | "test": "react-scripts test --env=node" 32 | }, 33 | "browserslist": [ 34 | ">0.2%", 35 | "not dead", 36 | "not ie <= 11", 37 | "not op_mini all" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /apollo-local-state/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Redux TodoMVC Example 7 | 8 | 9 |
10 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /apollo-local-state/src/cache.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { InMemoryCache, ReactiveVar, makeVar } from "@apollo/client"; 3 | import { Todos } from "./models/Todos"; 4 | import { VisibilityFilter, VisibilityFilters } from "./models/VisibilityFilter"; 5 | 6 | export const cache: InMemoryCache = new InMemoryCache({ 7 | typePolicies: { 8 | Query: { 9 | fields: { 10 | todos: { 11 | read () { 12 | return todosVar(); 13 | } 14 | }, 15 | visibilityFilter: { 16 | read () { 17 | return visibilityFilterVar(); 18 | }, 19 | } 20 | } 21 | } 22 | } 23 | }); 24 | 25 | /** 26 | * Set initial values when we create cache variables. 27 | */ 28 | 29 | const todosInitialValue: Todos = [ 30 | { 31 | id: 0, 32 | completed: false, 33 | text: "Use Apollo Client 3" 34 | } 35 | ] 36 | 37 | export const todosVar: ReactiveVar = makeVar( 38 | todosInitialValue 39 | ); 40 | 41 | export const visibilityFilterVar = makeVar( 42 | VisibilityFilters.SHOW_ALL 43 | ) 44 | -------------------------------------------------------------------------------- /apollo-local-state/src/components/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Header from '../containers/Header' 3 | import MainSection from '../containers/MainSection' 4 | 5 | const App = () => ( 6 |
7 |
8 | 9 |
10 | ) 11 | 12 | export default App 13 | -------------------------------------------------------------------------------- /apollo-local-state/src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import Link from './Link' 4 | import { VisibilityFilters, VisibilityFilter } from '../models/VisibilityFilter'; 5 | 6 | interface FooterProps { 7 | activeVisibilityFilter: VisibilityFilter; 8 | activeCount: number; 9 | completedCount: number; 10 | onClearCompleted: () => void; 11 | setVisibilityFilter: (filter: VisibilityFilter) => void; 12 | } 13 | 14 | const Footer = (props: FooterProps) => { 15 | const { activeCount, completedCount, onClearCompleted, activeVisibilityFilter, setVisibilityFilter } = props; 16 | const itemWord = activeCount === 1 ? "item" : "items"; 17 | return ( 18 |
19 | 20 | {activeCount || "No"} {itemWord} left 21 | 22 |
    23 | {Object.keys(VisibilityFilters).map((key) => VisibilityFilters[key]).map((filter) => ( 24 |
  • 25 | setVisibilityFilter(filter)}> 28 | {filter.displayName} 29 |
  • 30 | ))} 31 |
32 | {!!completedCount && ( 33 | 36 | )} 37 |
38 | ); 39 | }; 40 | 41 | Footer.propTypes = { 42 | completedCount: PropTypes.number.isRequired, 43 | activeCount: PropTypes.number.isRequired, 44 | onClearCompleted: PropTypes.func.isRequired, 45 | setVisibilityFilter: PropTypes.func.isRequired 46 | } 47 | 48 | export default Footer 49 | -------------------------------------------------------------------------------- /apollo-local-state/src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import TodoTextInput from './TodoTextInput' 4 | 5 | interface HeaderProps { 6 | addTodo: (text: string) => void; 7 | } 8 | 9 | const Header = ({ addTodo }: HeaderProps) => ( 10 |
11 |

todos

12 | { 15 | if (text.length !== 0) { 16 | addTodo(text); 17 | } 18 | }} 19 | placeholder="What needs to be done?" 20 | /> 21 |
22 | ); 23 | 24 | Header.propTypes = { 25 | addTodo: PropTypes.func.isRequired 26 | } 27 | 28 | export default Header 29 | -------------------------------------------------------------------------------- /apollo-local-state/src/components/Link.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import classnames from 'classnames' 4 | 5 | interface LinkProps { 6 | setFilter: () => any; 7 | active: boolean; 8 | children: any; 9 | } 10 | 11 | const Link = ({ active, children, setFilter }: LinkProps) => 12 | ( 13 | // eslint-disable-next-line jsx-a11y/anchor-is-valid 14 | setFilter()} 18 | > 19 | {children} 20 | 21 | ) 22 | 23 | 24 | Link.propTypes = { 25 | active: PropTypes.bool.isRequired, 26 | children: PropTypes.node.isRequired, 27 | setFilter: PropTypes.func.isRequired 28 | } 29 | 30 | export default Link 31 | -------------------------------------------------------------------------------- /apollo-local-state/src/components/MainSection.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import Footer from './Footer' 4 | import VisibleTodoList from '../containers/VisibleTodoList' 5 | import { VisibilityFilter } from '../models/VisibilityFilter'; 6 | 7 | interface MainSectionProps { 8 | activeVisibilityFilter: VisibilityFilter; 9 | todosCount: number; 10 | completedCount: number; 11 | actions: any; 12 | } 13 | 14 | /** 15 | * This is a view component. It doesn't define anything that 16 | * is responsible for querying or mutating, it just relies 17 | * on it from the upper layer component (namely, actions) 18 | */ 19 | 20 | const MainSection = ({ 21 | activeVisibilityFilter, 22 | todosCount, 23 | completedCount, 24 | actions 25 | }: MainSectionProps) => ( 26 |
27 | {!!todosCount && ( 28 | 29 | 35 | 37 | )} 38 | 39 | {!!todosCount && ( 40 |
47 | )} 48 |
49 | ); 50 | 51 | MainSection.propTypes = { 52 | todosCount: PropTypes.number.isRequired, 53 | completedCount: PropTypes.number.isRequired, 54 | actions: PropTypes.object.isRequired 55 | } 56 | 57 | export default MainSection; -------------------------------------------------------------------------------- /apollo-local-state/src/components/TodoItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import classnames from 'classnames' 4 | import TodoTextInput from './TodoTextInput' 5 | 6 | type Props = any; 7 | 8 | export default class TodoItem extends Component { 9 | static propTypes = { 10 | todo: PropTypes.object.isRequired, 11 | editTodo: PropTypes.func.isRequired, 12 | deleteTodo: PropTypes.func.isRequired, 13 | completeTodo: PropTypes.func.isRequired 14 | } 15 | 16 | state = { 17 | editing: false 18 | } 19 | 20 | handleDoubleClick = () => { 21 | this.setState({ editing: true }) 22 | } 23 | 24 | handleSave = (id: number, text: string) => { 25 | if (text.length === 0) { 26 | this.props.deleteTodo(id) 27 | } else { 28 | this.props.editTodo(id, text) 29 | } 30 | this.setState({ editing: false }) 31 | } 32 | 33 | render() { 34 | const { todo, completeTodo, deleteTodo } = this.props 35 | 36 | let element 37 | if (this.state.editing) { 38 | element = ( 39 | this.handleSave(todo.id, text)} /> 42 | ) 43 | } else { 44 | element = ( 45 |
46 | { 50 | completeTodo(todo.id) 51 | }} /> 52 | 55 |
58 | ) 59 | } 60 | 61 | return ( 62 |
  • 66 | {element} 67 |
  • 68 | ) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /apollo-local-state/src/components/TodoList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import TodoItem from './TodoItem' 4 | import { Todo } from '../models/Todos' 5 | 6 | const TodoList = ({ filteredTodos, actions }: any) => ( 7 |
      8 | {filteredTodos.map((todo: Todo) => 9 | 10 | )} 11 |
    12 | ) 13 | 14 | TodoList.propTypes = { 15 | filteredTodos: PropTypes.arrayOf(PropTypes.shape({ 16 | id: PropTypes.number.isRequired, 17 | completed: PropTypes.bool.isRequired, 18 | text: PropTypes.string.isRequired 19 | }).isRequired).isRequired, 20 | actions: PropTypes.object.isRequired 21 | } 22 | 23 | export default TodoList 24 | -------------------------------------------------------------------------------- /apollo-local-state/src/components/TodoTextInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import classnames from 'classnames' 4 | 5 | export default class TodoTextInput extends Component { 6 | static propTypes = { 7 | onSave: PropTypes.func.isRequired, 8 | text: PropTypes.string, 9 | placeholder: PropTypes.string, 10 | editing: PropTypes.bool, 11 | newTodo: PropTypes.bool 12 | } 13 | 14 | state = { 15 | text: this.props.text || '' 16 | } 17 | 18 | handleSubmit = (e: any) => { 19 | const text = e.target.value.trim() 20 | if (e.which === 13) { 21 | this.props.onSave(text) 22 | if (this.props.newTodo) { 23 | this.setState({ text: '' }) 24 | } 25 | } 26 | } 27 | 28 | handleChange = (e: any) => { 29 | this.setState({ text: e.target.value }) 30 | } 31 | 32 | handleBlur = (e: any) => { 33 | if (!this.props.newTodo) { 34 | this.props.onSave(e.target.value) 35 | } 36 | } 37 | 38 | render() { 39 | return ( 40 | 52 | ) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /apollo-local-state/src/containers/Header.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import Header from '../components/Header' 4 | import { todoMutations } from '../operations/mutations'; 5 | 6 | export default function () { 7 | return
    ; 8 | } -------------------------------------------------------------------------------- /apollo-local-state/src/containers/MainSection.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import MainSection from '../components/MainSection' 4 | import { useQuery } from '@apollo/client' 5 | import { VisibilityFilter } from '../models/VisibilityFilter' 6 | import { Todos } from '../models/Todos' 7 | import { GET_ALL_TODOS } from '../operations/queries/getAllTodos' 8 | import { GET_VISIBILITY_FILTER } from '../operations/queries/getVisibilityFilter' 9 | import { todoMutations } from '../operations/mutations' 10 | 11 | export default function Main () { 12 | const todosQueryResult = useQuery(GET_ALL_TODOS); 13 | const visibilityFilterQueryResult = useQuery(GET_VISIBILITY_FILTER); 14 | const todos: Todos = todosQueryResult.data.todos; 15 | const visibilityFilter: VisibilityFilter = visibilityFilterQueryResult.data.visibilityFilter; 16 | const { completeAllTodos, setVisibilityFilter, clearCompletedTodos } = todoMutations; 17 | 18 | return ( 19 | t.completed).length} 23 | actions={{ 24 | completeAllTodos, 25 | setVisibilityFilter, 26 | clearCompletedTodos 27 | }} 28 | /> 29 | ); 30 | }; 31 | 32 | -------------------------------------------------------------------------------- /apollo-local-state/src/containers/VisibleTodoList.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import { visibilityFilterVar, todosVar } from '../cache' 4 | import TodoList from '../components/TodoList'; 5 | import { VisibilityFilter, VisibilityFilters } from '../models/VisibilityFilter'; 6 | import { Todos } from '../models/Todos'; 7 | import { todoMutations } from '../operations/mutations'; 8 | 9 | function filterTodosByVisibility(visibilityFilter: VisibilityFilter, todos: Todos) { 10 | switch (visibilityFilter.id) { 11 | case VisibilityFilters.SHOW_ALL.id: 12 | return todos; 13 | case VisibilityFilters.SHOW_COMPLETED.id: 14 | return todos.filter(t => t.completed); 15 | case VisibilityFilters.SHOW_ACTIVE.id: 16 | return todos.filter(t => !t.completed); 17 | default: 18 | throw new Error("Unknown filter: " + visibilityFilter); 19 | } 20 | } 21 | 22 | export default function VisibleTodoList () { 23 | const { completeTodo, deleteTodo, editTodo } = todoMutations; 24 | const todos = todosVar(); 25 | const filteredTodos = filterTodosByVisibility(visibilityFilterVar(), todos); 26 | 27 | return ; 34 | } 35 | -------------------------------------------------------------------------------- /apollo-local-state/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import App from './components/App' 4 | import { ApolloClient, ApolloProvider } from '@apollo/client'; 5 | import 'todomvc-app-css/index.css' 6 | import { cache } from './cache'; 7 | 8 | export const client = new ApolloClient({ 9 | cache, 10 | connectToDevTools: true, 11 | }); 12 | 13 | render( 14 | 15 | 16 | , 17 | document.getElementById('root') 18 | ) 19 | -------------------------------------------------------------------------------- /apollo-local-state/src/models/Todos.tsx: -------------------------------------------------------------------------------- 1 | 2 | export interface Todo { 3 | text: string; 4 | completed: boolean; 5 | id: number 6 | } 7 | 8 | export type Todos = Todo[]; -------------------------------------------------------------------------------- /apollo-local-state/src/models/VisibilityFilter.tsx: -------------------------------------------------------------------------------- 1 | 2 | export type VisibilityFilter = { 3 | id: string; 4 | displayName: string; 5 | } 6 | 7 | export const VisibilityFilters: { [filter: string]: VisibilityFilter } = { 8 | SHOW_ALL: { 9 | id: "show_all", 10 | displayName: "All" 11 | }, 12 | SHOW_COMPLETED: { 13 | id: "show_completed", 14 | displayName: "Completed" 15 | }, 16 | SHOW_ACTIVE: { 17 | id: "show_active", 18 | displayName: "Active" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /apollo-local-state/src/operations/mutations/addTodo/addTodo.spec.tsx: -------------------------------------------------------------------------------- 1 | 2 | import createAddTodo from './addTodo' 3 | import { mockTodosVar } from '../../../tests/mocks/mockTodosVar'; 4 | 5 | const addTodo = createAddTodo(mockTodosVar); 6 | 7 | describe('addTodos hook', () => { 8 | beforeEach(() => mockTodosVar([])); 9 | 10 | it('should add a todo', () => { 11 | addTodo('First todo') 12 | expect( 13 | mockTodosVar() 14 | ).toHaveLength(1) 15 | 16 | expect( 17 | mockTodosVar()[0].id 18 | ).toEqual(0) 19 | 20 | expect( 21 | mockTodosVar()[0].completed 22 | ).toEqual(false) 23 | 24 | expect( 25 | mockTodosVar()[0].text 26 | ).toEqual('First todo') 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /apollo-local-state/src/operations/mutations/addTodo/addTodo.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { Todos, Todo } from "../../../models/Todos"; 3 | import { ReactiveVar } from "@apollo/client"; 4 | 5 | export default function createAddTodo (todosVar: ReactiveVar) { 6 | const createNewTodoId = (allTodos: Todos) => { 7 | return allTodos.reduce( 8 | (maxId: number, todo: Todo) => Math.max(todo.id, maxId), 9 | -1 10 | ) + 1; 11 | } 12 | 13 | const createNewTodo = (text: string, allTodos: Todos) => { 14 | return { text, completed: false, id: createNewTodoId(allTodos) } 15 | } 16 | 17 | return (text: string) => { 18 | const allTodos = todosVar(); 19 | todosVar(allTodos.concat([createNewTodo(text, allTodos)])); 20 | }; 21 | } -------------------------------------------------------------------------------- /apollo-local-state/src/operations/mutations/clearCompletedTodos/clearCompletedTodos.spec.tsx: -------------------------------------------------------------------------------- 1 | 2 | import createClearCompletedTodos from './clearCompletedTodos' 3 | import { mockTodosVar } from '../../../tests/mocks/mockTodosVar'; 4 | 5 | const clearCompletedTodos = createClearCompletedTodos(mockTodosVar); 6 | 7 | describe('clearCompletedTodos hook', () => { 8 | beforeEach(() => mockTodosVar([])); 9 | 10 | it('should clear a list of completed todos', () => { 11 | mockTodosVar([{ id: 0, text: 'Completed todo', completed: true }]) 12 | clearCompletedTodos(); 13 | 14 | expect( 15 | mockTodosVar() 16 | ).toHaveLength(0) 17 | }) 18 | 19 | it('should not clear todos that are not completed', () => { 20 | mockTodosVar([ 21 | { id: 0, text: 'Completed todo', completed: true }, 22 | { id: 1, text: 'Not completed todo', completed: false } 23 | ]) 24 | clearCompletedTodos(); 25 | 26 | expect( 27 | mockTodosVar() 28 | ).toHaveLength(1) 29 | 30 | expect( 31 | mockTodosVar()[0].text 32 | ).toEqual('Not completed todo') 33 | 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /apollo-local-state/src/operations/mutations/clearCompletedTodos/clearCompletedTodos.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { ReactiveVar } from "@apollo/client"; 3 | import { Todos } from "../../../models/Todos"; 4 | 5 | export default function createClearCompletedTodos ( 6 | todosVar: ReactiveVar 7 | ) { 8 | return () => { 9 | const nonCompletedTodos = todosVar() 10 | .filter((t) => t.completed !== true); 11 | 12 | todosVar(nonCompletedTodos); 13 | 14 | } 15 | } -------------------------------------------------------------------------------- /apollo-local-state/src/operations/mutations/completeAllTodos/completeAllTodos.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { Todo, Todos } from "../../../models/Todos"; 3 | import { ReactiveVar } from "@apollo/client"; 4 | 5 | export default function completeAllTodos ( 6 | todosVar: ReactiveVar 7 | ) { 8 | return () => { 9 | const allTodosCompleted = todosVar() 10 | .map((t: Todo) => ({ ...t, completed: true })); 11 | 12 | todosVar(allTodosCompleted); 13 | } 14 | } -------------------------------------------------------------------------------- /apollo-local-state/src/operations/mutations/completeTodo/completeTodo.spec.tsx: -------------------------------------------------------------------------------- 1 | 2 | import createCompleteTodo from './completeTodo' 3 | import { mockTodosVar } from '../../../tests/mocks/mockTodosVar'; 4 | 5 | const completeTodo = createCompleteTodo(mockTodosVar); 6 | 7 | describe('completeTodo hook', () => { 8 | beforeEach(() => mockTodosVar([])); 9 | 10 | it('should mark a todo as completed', () => { 11 | mockTodosVar([{ id: 0, text: 'First todo', completed: false }]) 12 | completeTodo(0); 13 | 14 | expect( 15 | mockTodosVar()[0].completed 16 | ).toBeTruthy() 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /apollo-local-state/src/operations/mutations/completeTodo/completeTodo.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { ReactiveVar } from "@apollo/client"; 3 | import { Todos } from "../../../models/Todos"; 4 | 5 | export default function createCompleteTodo ( 6 | todosVar: ReactiveVar 7 | ) { 8 | return (id: number) => { 9 | const allTodos = todosVar(); 10 | 11 | todosVar(allTodos.map((t) => t.id === id ? { ...t, completed: true } : t)); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /apollo-local-state/src/operations/mutations/deleteTodo/deleteTodo.spec.tsx: -------------------------------------------------------------------------------- 1 | 2 | import createDeleteTodo from './deleteTodo' 3 | import { mockTodosVar } from '../../../tests/mocks/mockTodosVar'; 4 | 5 | const deleteTodo = createDeleteTodo(mockTodosVar); 6 | 7 | describe('deleteTodo hook', () => { 8 | beforeEach(() => mockTodosVar([])); 9 | 10 | it('should delete a todo from the list of todos', () => { 11 | mockTodosVar([{ id: 0, text: 'First todo', completed: false }]) 12 | deleteTodo(0); 13 | 14 | expect( 15 | mockTodosVar().length 16 | ).toEqual(0) 17 | }) 18 | }) 19 | 20 | -------------------------------------------------------------------------------- /apollo-local-state/src/operations/mutations/deleteTodo/deleteTodo.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { Todo, Todos } from "../../../models/Todos"; 3 | import { ReactiveVar } from "@apollo/client"; 4 | 5 | export default function deleteTodo (todosVar: ReactiveVar) { 6 | return (id: number) => { 7 | const allTodos = todosVar(); 8 | const filteredTodos = allTodos.filter((todo: Todo) => todo.id !== id); 9 | todosVar(filteredTodos); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /apollo-local-state/src/operations/mutations/editTodo/editTodo.spec.tsx: -------------------------------------------------------------------------------- 1 | 2 | import createEditTodo from './editTodo' 3 | import { mockTodosVar } from '../../../tests/mocks/mockTodosVar'; 4 | 5 | const editTodo = createEditTodo(mockTodosVar); 6 | 7 | describe('editTodo hook', () => { 8 | beforeEach(() => mockTodosVar([])); 9 | 10 | it('should mark a todo as completed', () => { 11 | mockTodosVar([{ id: 0, text: 'First todo', completed: false }]) 12 | editTodo(0, "This was edited"); 13 | 14 | expect( 15 | mockTodosVar()[0].text 16 | ).toEqual("This was edited") 17 | }); 18 | }) 19 | 20 | 21 | -------------------------------------------------------------------------------- /apollo-local-state/src/operations/mutations/editTodo/editTodo.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { Todo, Todos } from "../../../models/Todos"; 3 | import { ReactiveVar } from "@apollo/client"; 4 | 5 | export default (todosVar: ReactiveVar) => { 6 | return (id: number, text: string) => { 7 | let todosWithEditedTodo = todosVar() 8 | .map((todo: Todo) => todo.id === id ? { ...todo, text } : todo); 9 | 10 | todosVar(todosWithEditedTodo); 11 | } 12 | } -------------------------------------------------------------------------------- /apollo-local-state/src/operations/mutations/index.tsx: -------------------------------------------------------------------------------- 1 | 2 | import createAddTodo from "./addTodo/addTodo"; 3 | import createClearCompletedTodos from "./clearCompletedTodos/clearCompletedTodos"; 4 | import createCompleteTodo from "./completeTodo/completeTodo"; 5 | import createCompleteAllTodos from "./completeAllTodos/completeAllTodos"; 6 | import createDeleteTodo from "./deleteTodo/deleteTodo"; 7 | import createEditTodo from "./editTodo/editTodo"; 8 | import createSetVisibilityFilter from "./setVisibilityFilter/setVisibilityFilter"; 9 | import { todosVar, visibilityFilterVar } from "../../cache"; 10 | 11 | export const todoMutations = { 12 | addTodo: createAddTodo(todosVar), 13 | clearCompletedTodos: createClearCompletedTodos(todosVar), 14 | completeTodo: createCompleteTodo(todosVar), 15 | completeAllTodos: createCompleteAllTodos(todosVar), 16 | deleteTodo: createDeleteTodo(todosVar), 17 | editTodo: createEditTodo(todosVar), 18 | setVisibilityFilter: createSetVisibilityFilter(visibilityFilterVar) 19 | } 20 | -------------------------------------------------------------------------------- /apollo-local-state/src/operations/mutations/setVisibilityFilter/setVisibilityFilter.spec.tsx: -------------------------------------------------------------------------------- 1 | 2 | import createSetVisibilityFilter from './setVisibilityFilter' 3 | import { VisibilityFilters } from '../../../models/VisibilityFilter'; 4 | import { mockVisibilityFilter } from '../../../tests/mocks/mockVisibilityFilter'; 5 | 6 | const setVisibilityFilter = createSetVisibilityFilter(mockVisibilityFilter); 7 | 8 | describe('setVisibilityFilter hook', () => { 9 | beforeEach(() => mockVisibilityFilter(VisibilityFilters.SHOW_ALL)); 10 | 11 | it('should change the visibility filter', () => { 12 | setVisibilityFilter(VisibilityFilters.SHOW_ALL); 13 | 14 | expect( 15 | mockVisibilityFilter().displayName 16 | ).toEqual("All") 17 | 18 | setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED); 19 | 20 | expect( 21 | mockVisibilityFilter().displayName 22 | ).toEqual("Completed") 23 | }); 24 | }) 25 | 26 | 27 | -------------------------------------------------------------------------------- /apollo-local-state/src/operations/mutations/setVisibilityFilter/setVisibilityFilter.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { VisibilityFilter } from "../../../models/VisibilityFilter"; 3 | import { ReactiveVar } from "@apollo/client"; 4 | 5 | export default (visibilityFilterVar: ReactiveVar) => { 6 | return (filter: VisibilityFilter) => { 7 | visibilityFilterVar(filter); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /apollo-local-state/src/operations/queries/getAllTodos.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { gql } from "@apollo/client"; 3 | 4 | export const GET_ALL_TODOS = gql` 5 | query GetAllTodos { 6 | todos @client { 7 | id 8 | text 9 | completed 10 | } 11 | } 12 | ` -------------------------------------------------------------------------------- /apollo-local-state/src/operations/queries/getVisibilityFilter.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { gql } from "@apollo/client"; 3 | 4 | export const GET_VISIBILITY_FILTER = gql` 5 | query GetVisibilityFilter { 6 | visibilityFilter @client { 7 | id 8 | displayName 9 | } 10 | } 11 | ` -------------------------------------------------------------------------------- /apollo-local-state/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /apollo-local-state/src/tests/createMockReactiveVar.spec.ts: -------------------------------------------------------------------------------- 1 | import { mockTodosVar } from "./mocks/mockTodosVar"; 2 | 3 | describe('Testing mock reactive variables', () => { 4 | beforeEach(() => mockTodosVar([])); 5 | 6 | it('should set the current value if provided', () => { 7 | mockTodosVar([{ id: 0, text: 'First todo', completed: false }]); 8 | expect(mockTodosVar()?.length).toEqual(1); 9 | }) 10 | 11 | it('should overwrite the current value', () => { 12 | mockTodosVar([{ id: 0, text: 'First todo', completed: false }]); 13 | expect(mockTodosVar()?.length).toEqual(1); 14 | 15 | mockTodosVar([{ id: 1, text: 'Second todo', completed: false }]); 16 | expect(mockTodosVar()?.length).toEqual(1); 17 | }); 18 | 19 | it('should not overwrite the current value if no value provided', () => { 20 | mockTodosVar([ 21 | { id: 0, text: 'First todo', completed: false }, 22 | { id: 1, text: 'Second todo', completed: false } 23 | ]); 24 | expect(mockTodosVar()?.length).toEqual(2); 25 | 26 | mockTodosVar(); 27 | expect(mockTodosVar()?.length).toEqual(2); 28 | }) 29 | }) -------------------------------------------------------------------------------- /apollo-local-state/src/tests/createMockReactiveVar.ts: -------------------------------------------------------------------------------- 1 | 2 | import { ReactiveVar } from "@apollo/client"; 3 | 4 | // This is how to create a mock reactive variable. 5 | // It's a good idea to do this because then we can test our 6 | // interaction logic. 7 | 8 | export function createMockReactiveVar (defaultValue?: T): any { 9 | let currentValue: T | undefined = defaultValue; 10 | 11 | return function mockReactiveVar ( 12 | newValue?: T 13 | ) : T { 14 | 15 | if (newValue) { 16 | currentValue = newValue; 17 | } 18 | return currentValue as T; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /apollo-local-state/src/tests/mocks/mockTodosVar.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Todos } from "../../models/Todos"; 3 | import { createMockReactiveVar } from "../createMockReactiveVar"; 4 | 5 | export const mockTodosVar = createMockReactiveVar([]); -------------------------------------------------------------------------------- /apollo-local-state/src/tests/mocks/mockVisibilityFilter.ts: -------------------------------------------------------------------------------- 1 | 2 | import { createMockReactiveVar } from "../createMockReactiveVar"; 3 | import { VisibilityFilter, VisibilityFilters } from "../../models/VisibilityFilter"; 4 | 5 | export const mockVisibilityFilter = createMockReactiveVar( 6 | VisibilityFilters.SHOW_ALL 7 | ); 8 | -------------------------------------------------------------------------------- /apollo-local-state/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react" 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/README.md: -------------------------------------------------------------------------------- 1 | # apollo-remote-state-advanced-cache-apis 2 | 3 | Summary: Hooking Apollo Client up to a remote GraphQL API, the client-side cache is smart enough to automatically update the cache after most mutations successfully complete. For mutations that perform interactions against arrays or have additional client-side side-effects, we can help the cache decide what to do next by writing our update logic in the `useMutation`'s `update` function. 4 | 5 | This example uses the new Apollo Client 3 cache manipulation APIs: `cache.modify` and `cache.evict`. For users familiar with how cache normalization works, this approach is generally preferred because it provides more direct access to the cache over the simpler `cache.readQuery`/`cache.writeQuery` approach. 6 | 7 | For those just starting out with Apollo Client, we recommend using the `cache.readQuery` and `cache.writeQuery` API example [found here](https://github.com/apollographql/ac3-state-management-examples/tree/master/apollo-remote-state) instead. 8 | 9 | ## Getting started 10 | 11 | There is a `client/` and `server/` portion to this example. 12 | 13 | ### Running the client 14 | 15 | To start the project, run the following commands: 16 | 17 | ``` 18 | cd client 19 | npm install && npm run start 20 | ``` 21 | 22 | The app should start at http://localhost:3000/. 23 | 24 | ### Running the server 25 | 26 | To start the project, run the following commands: 27 | 28 | ``` 29 | cd server 30 | npm install && npm run start 31 | ``` 32 | 33 | The server should start at http://localhost:4000/. 34 | -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/client/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # production 7 | build 8 | 9 | # misc 10 | .DS_Store 11 | npm-debug.log 12 | .env -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/client/__generated__/globalTypes.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | //============================================================== 6 | // START Enums and Input Objects 7 | //============================================================== 8 | 9 | //============================================================== 10 | // END Enums and Input Objects 11 | //============================================================== 12 | -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/client/apollo.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | client: { 3 | service: { 4 | name: 'ac3-todos-backend', 5 | url: `http://localhost:4000`, 6 | }, 7 | }, 8 | }; -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todomvc-apollo", 3 | "version": "0.0.1", 4 | "private": true, 5 | "devDependencies": { 6 | "@types/classnames": "^2.2.9", 7 | "@types/react-test-renderer": "^16.9.2", 8 | "react-scripts": "^3.0.0", 9 | "react-test-renderer": "^16.8.6" 10 | }, 11 | "dependencies": { 12 | "@apollo/client": "^3.0.1", 13 | "@types/jest": "^25.1.3", 14 | "@types/node": "^13.7.6", 15 | "@types/react": "^16.9.23", 16 | "@types/react-dom": "^16.9.5", 17 | "classnames": "^2.2.6", 18 | "graphql": "^14.6.0", 19 | "prop-types": "^15.7.2", 20 | "react": "^16.8.6", 21 | "react-dom": "^16.8.6", 22 | "reselect": "^4.0.0", 23 | "todomvc-app-css": "^2.2.0", 24 | "typescript": "^3.8.2" 25 | }, 26 | "scripts": { 27 | "start": "react-scripts start", 28 | "build": "react-scripts build", 29 | "eject": "react-scripts eject", 30 | "test": "react-scripts test --env=node", 31 | "generate": "apollo client:codegen --target typescript --excludes src/operations/queries/getVisibilityFilter.tsx" 32 | }, 33 | "browserslist": [ 34 | ">0.2%", 35 | "not dead", 36 | "not ie <= 11", 37 | "not op_mini all" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Redux TodoMVC Example 7 | 8 | 9 |
    10 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/client/src/cache.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { InMemoryCache, makeVar } from "@apollo/client"; 3 | import { VisibilityFilters, VisibilityFilter } from "./models/VisibilityFilter"; 4 | 5 | export const cache: InMemoryCache = new InMemoryCache({ 6 | typePolicies: { 7 | Query: { 8 | fields: { 9 | visibilityFilter: { 10 | read () { 11 | return visibilityFilterVar(); 12 | } 13 | } 14 | } 15 | } 16 | } 17 | }); 18 | 19 | /** 20 | * Set initial values when we create cache variables. 21 | */ 22 | 23 | export const visibilityFilterVar = makeVar( 24 | VisibilityFilters.SHOW_ALL 25 | ) 26 | -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/client/src/components/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Header from '../containers/Header' 3 | import MainSection from '../containers/MainSection' 4 | 5 | const App = () => ( 6 |
    7 |
    8 | 9 |
    10 | ) 11 | 12 | export default App 13 | -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/client/src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import Link from './Link' 4 | import { VisibilityFilters, VisibilityFilter } from '../models/VisibilityFilter'; 5 | 6 | interface FooterProps { 7 | activeVisibilityFilter: VisibilityFilter; 8 | activeCount: number; 9 | completedCount: number; 10 | onClearCompleted: () => void; 11 | setVisibilityFilter: (filter: VisibilityFilter) => void; 12 | } 13 | 14 | const Footer = (props: FooterProps) => { 15 | const { activeCount, completedCount, onClearCompleted, activeVisibilityFilter, setVisibilityFilter } = props; 16 | const itemWord = activeCount === 1 ? "item" : "items"; 17 | return ( 18 |
    19 | 20 | {activeCount || "No"} {itemWord} left 21 | 22 |
      23 | {Object.keys(VisibilityFilters).map((key) => VisibilityFilters[key]).map((filter) => ( 24 |
    • 25 | setVisibilityFilter(filter)}> 28 | {filter.displayName} 29 |
    • 30 | ))} 31 |
    32 | {!!completedCount && ( 33 | 36 | )} 37 |
    38 | ); 39 | }; 40 | 41 | Footer.propTypes = { 42 | completedCount: PropTypes.number.isRequired, 43 | activeCount: PropTypes.number.isRequired, 44 | onClearCompleted: PropTypes.func.isRequired, 45 | setVisibilityFilter: PropTypes.func.isRequired 46 | } 47 | 48 | export default Footer 49 | -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/client/src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import TodoTextInput from './TodoTextInput' 4 | 5 | interface HeaderProps { 6 | addTodo: (text: string) => void; 7 | } 8 | 9 | const Header = ({ addTodo }: HeaderProps) => ( 10 |
    11 |

    todos

    12 | { 15 | if (text.length !== 0) { 16 | addTodo(text); 17 | } 18 | }} 19 | placeholder="What needs to be done?" 20 | /> 21 |
    22 | ); 23 | 24 | Header.propTypes = { 25 | addTodo: PropTypes.func.isRequired 26 | } 27 | 28 | export default Header 29 | -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/client/src/components/Link.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import classnames from 'classnames' 4 | 5 | interface LinkProps { 6 | setFilter: () => any; 7 | active: boolean; 8 | children: any; 9 | } 10 | 11 | const Link = ({ active, children, setFilter }: LinkProps) => 12 | ( 13 | // eslint-disable-next-line jsx-a11y/anchor-is-valid 14 | setFilter()} 18 | > 19 | {children} 20 | 21 | ) 22 | 23 | 24 | Link.propTypes = { 25 | active: PropTypes.bool.isRequired, 26 | children: PropTypes.node.isRequired, 27 | setFilter: PropTypes.func.isRequired 28 | } 29 | 30 | export default Link 31 | -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/client/src/components/MainSection.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import Footer from './Footer' 4 | import VisibleTodoList from '../containers/VisibleTodoList' 5 | import { VisibilityFilter } from '../models/VisibilityFilter'; 6 | 7 | interface MainSectionProps { 8 | activeVisibilityFilter: VisibilityFilter; 9 | todosCount: number; 10 | completedCount: number; 11 | actions: any; 12 | } 13 | 14 | /** 15 | * This is a view component. It doesn't define anything that 16 | * is responsible for querying or mutating, it just relies 17 | * on it from the upper layer component (namely, actions) 18 | */ 19 | 20 | const MainSection = ({ 21 | activeVisibilityFilter, 22 | todosCount, 23 | completedCount, 24 | actions, 25 | }: MainSectionProps) => ( 26 |
    27 | {!!todosCount && ( 28 | 29 | 35 | 37 | )} 38 | 39 | {!!todosCount && ( 40 |
    47 | )} 48 |
    49 | ); 50 | 51 | MainSection.propTypes = { 52 | todosCount: PropTypes.number.isRequired, 53 | completedCount: PropTypes.number.isRequired, 54 | actions: PropTypes.object.isRequired 55 | } 56 | 57 | export default MainSection; 58 | -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/client/src/components/TodoItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import classnames from 'classnames' 4 | import TodoTextInput from './TodoTextInput' 5 | 6 | type Props = any; 7 | 8 | export default class TodoItem extends Component { 9 | static propTypes = { 10 | todo: PropTypes.object.isRequired, 11 | editTodo: PropTypes.func.isRequired, 12 | deleteTodo: PropTypes.func.isRequired, 13 | completeTodo: PropTypes.func.isRequired 14 | } 15 | 16 | state = { 17 | editing: false 18 | } 19 | 20 | handleDoubleClick = () => { 21 | this.setState({ editing: true }) 22 | } 23 | 24 | handleSave = (id: number, text: string) => { 25 | if (text.length === 0) { 26 | this.props.deleteTodo(id) 27 | } else { 28 | this.props.editTodo(id, text) 29 | } 30 | this.setState({ editing: false }) 31 | } 32 | 33 | render() { 34 | const { todo, completeTodo, deleteTodo } = this.props 35 | 36 | let element 37 | if (this.state.editing) { 38 | element = ( 39 | this.handleSave(todo.id, text)} /> 42 | ) 43 | } else { 44 | element = ( 45 |
    46 | { 50 | completeTodo(todo.id) 51 | }} /> 52 | 55 |
    58 | ) 59 | } 60 | 61 | return ( 62 |
  • 66 | {element} 67 |
  • 68 | ) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/client/src/components/TodoList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import TodoItem from './TodoItem' 4 | import { Todo } from '../models/Todos' 5 | 6 | const TodoList = ({ filteredTodos, actions }: any) => ( 7 |
      8 | {filteredTodos.map((todo: Todo) => 9 | 10 | )} 11 |
    12 | ) 13 | 14 | TodoList.propTypes = { 15 | filteredTodos: PropTypes.arrayOf(PropTypes.shape({ 16 | id: PropTypes.number.isRequired, 17 | completed: PropTypes.bool.isRequired, 18 | text: PropTypes.string.isRequired 19 | }).isRequired).isRequired, 20 | actions: PropTypes.object.isRequired 21 | } 22 | 23 | export default TodoList 24 | -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/client/src/components/TodoTextInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import classnames from 'classnames' 4 | 5 | export default class TodoTextInput extends Component { 6 | static propTypes = { 7 | onSave: PropTypes.func.isRequired, 8 | text: PropTypes.string, 9 | placeholder: PropTypes.string, 10 | editing: PropTypes.bool, 11 | newTodo: PropTypes.bool 12 | } 13 | 14 | state = { 15 | text: this.props.text || '' 16 | } 17 | 18 | handleSubmit = (e: any) => { 19 | const text = e.target.value.trim() 20 | if (e.which === 13) { 21 | this.props.onSave(text) 22 | if (this.props.newTodo) { 23 | this.setState({ text: '' }) 24 | } 25 | } 26 | } 27 | 28 | handleChange = (e: any) => { 29 | this.setState({ text: e.target.value }) 30 | } 31 | 32 | handleBlur = (e: any) => { 33 | if (!this.props.newTodo) { 34 | this.props.onSave(e.target.value) 35 | } 36 | } 37 | 38 | render() { 39 | return ( 40 | 52 | ) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/client/src/containers/Header.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import Header from '../components/Header' 4 | import { useAddTodo } from '../operations/mutations/addTodo'; 5 | 6 | export default function () { 7 | const { mutate } = useAddTodo(); 8 | return
    mutate({ variables: { text } })} />; 9 | } -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/client/src/containers/MainSection.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import MainSection from '../components/MainSection' 4 | import { useQuery, gql } from '@apollo/client' 5 | import { VisibilityFilter } from '../models/VisibilityFilter' 6 | import { GET_ALL_TODOS } from '../operations/queries/getAllTodos' 7 | import { GET_VISIBILITY_FILTER } from '../operations/queries/getVisibilityFilter' 8 | import { GetAllTodos } from '../operations/__generated__/GetAllTodos' 9 | import { useClearCompletedTodos } from '../operations/mutations/clearCompletedTodos' 10 | import { useCompleteAllTodos } from '../operations/mutations/completeAllTodos' 11 | import { setVisibilityFilter } from '../operations/mutations/setVisibilityFilter' 12 | 13 | export default function Main () { 14 | const { loading: isTodosLoading, data: todosConnection, error: todosError } = useQuery(GET_ALL_TODOS); 15 | const { data: visibilityFilter } = useQuery(GET_VISIBILITY_FILTER); 16 | 17 | const { mutate: clearCompletedTodos } = useClearCompletedTodos(); 18 | const { mutate: completeAllTodos } = useCompleteAllTodos(); 19 | 20 | if (isTodosLoading) return
    Loading...
    21 | if (todosError) return
    An error occurred {JSON.stringify(todosError)}
    22 | if (!todosConnection) return
    None
    ; 23 | const todos = todosConnection.todos.edges.map((t) => t?.node) 24 | 25 | return ( 26 | t ? t.completed : false).length} 30 | actions={{ 31 | completeAllTodos, 32 | setVisibilityFilter, 33 | clearCompletedTodos, 34 | }} 35 | /> 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/client/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import App from './components/App' 4 | import { ApolloClient, ApolloProvider } from '@apollo/client'; 5 | import 'todomvc-app-css/index.css' 6 | import { cache } from './cache'; 7 | 8 | export const client = new ApolloClient({ 9 | cache, 10 | uri: 'http://localhost:4000/', 11 | headers: { 12 | authorization: localStorage.getItem('token') || '', 13 | 'client-name': 'ac3-todos-backend', 14 | 'client-version': '1.0.0', 15 | }, 16 | connectToDevTools: true, 17 | }); 18 | 19 | render( 20 | 21 | 22 | , 23 | document.getElementById('root') 24 | ) 25 | -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/client/src/models/Todos.tsx: -------------------------------------------------------------------------------- 1 | import { GetAllTodos_todos_edges_node } from "../operations/__generated__/GetAllTodos"; 2 | 3 | export type Todo = GetAllTodos_todos_edges_node; 4 | 5 | export type Todos = Todo[]; -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/client/src/models/VisibilityFilter.tsx: -------------------------------------------------------------------------------- 1 | 2 | export type VisibilityFilter = { 3 | id: string; 4 | displayName: string; 5 | } 6 | 7 | export const VisibilityFilters: { [filter: string]: VisibilityFilter } = { 8 | SHOW_ALL: { 9 | id: "show_all", 10 | displayName: "All" 11 | }, 12 | SHOW_COMPLETED: { 13 | id: "show_completed", 14 | displayName: "Completed" 15 | }, 16 | SHOW_ACTIVE: { 17 | id: "show_active", 18 | displayName: "Active" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/client/src/operations/__generated__/GetAllTodos.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | // ==================================================== 6 | // GraphQL query operation: GetAllTodos 7 | // ==================================================== 8 | 9 | export interface GetAllTodos_todos_edges_node { 10 | __typename: "Todo"; 11 | id: number; 12 | text: string; 13 | completed: boolean; 14 | } 15 | 16 | export interface GetAllTodos_todos_edges { 17 | __typename: "TodosEdge"; 18 | node: GetAllTodos_todos_edges_node; 19 | } 20 | 21 | export interface GetAllTodos_todos { 22 | __typename: "TodosConnection"; 23 | edges: (GetAllTodos_todos_edges | null)[]; 24 | } 25 | 26 | export interface GetAllTodos { 27 | todos: GetAllTodos_todos; 28 | } 29 | -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/client/src/operations/mutations/__generated__/AddTodo.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | // ==================================================== 6 | // GraphQL mutation operation: AddTodo 7 | // ==================================================== 8 | 9 | export interface AddTodo_addTodo_todo { 10 | __typename: "Todo"; 11 | id: number; 12 | text: string; 13 | completed: boolean; 14 | } 15 | 16 | export interface AddTodo_addTodo_error { 17 | __typename: "TodoValidationError"; 18 | message: string; 19 | } 20 | 21 | export interface AddTodo_addTodo { 22 | __typename: "AddTodoResult"; 23 | success: boolean; 24 | todo: AddTodo_addTodo_todo | null; 25 | error: AddTodo_addTodo_error | null; 26 | } 27 | 28 | export interface AddTodo { 29 | addTodo: AddTodo_addTodo; 30 | } 31 | 32 | export interface AddTodoVariables { 33 | text: string; 34 | } 35 | -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/client/src/operations/mutations/__generated__/ClearCompletedTodos.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | // ==================================================== 6 | // GraphQL mutation operation: ClearCompletedTodos 7 | // ==================================================== 8 | 9 | export interface ClearCompletedTodos_clearCompletedTodos_todos { 10 | __typename: "Todo"; 11 | id: number; 12 | text: string; 13 | completed: boolean; 14 | } 15 | 16 | export interface ClearCompletedTodos_clearCompletedTodos { 17 | __typename: "ClearCompletedTodosResult"; 18 | success: boolean; 19 | todos: (ClearCompletedTodos_clearCompletedTodos_todos | null)[]; 20 | } 21 | 22 | export interface ClearCompletedTodos { 23 | clearCompletedTodos: ClearCompletedTodos_clearCompletedTodos; 24 | } 25 | -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/client/src/operations/mutations/__generated__/CompleteAllTodos.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | // ==================================================== 6 | // GraphQL mutation operation: CompleteAllTodos 7 | // ==================================================== 8 | 9 | export interface CompleteAllTodos_completeAllTodos_todos { 10 | __typename: "Todo"; 11 | id: number; 12 | text: string; 13 | completed: boolean; 14 | } 15 | 16 | export interface CompleteAllTodos_completeAllTodos { 17 | __typename: "CompleteAllTodosResult"; 18 | success: boolean; 19 | todos: (CompleteAllTodos_completeAllTodos_todos | null)[]; 20 | } 21 | 22 | export interface CompleteAllTodos { 23 | completeAllTodos: CompleteAllTodos_completeAllTodos; 24 | } 25 | -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/client/src/operations/mutations/__generated__/CompleteTodo.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | // ==================================================== 6 | // GraphQL mutation operation: CompleteTodo 7 | // ==================================================== 8 | 9 | export interface CompleteTodo_completeTodo_todo { 10 | __typename: "Todo"; 11 | id: number; 12 | text: string; 13 | completed: boolean; 14 | } 15 | 16 | export interface CompleteTodo_completeTodo_error_TodoNotFoundError { 17 | __typename: "TodoNotFoundError"; 18 | message: string; 19 | } 20 | 21 | export interface CompleteTodo_completeTodo_error_TodoAlreadyCompletedError { 22 | __typename: "TodoAlreadyCompletedError"; 23 | message: string; 24 | } 25 | 26 | export type CompleteTodo_completeTodo_error = CompleteTodo_completeTodo_error_TodoNotFoundError | CompleteTodo_completeTodo_error_TodoAlreadyCompletedError; 27 | 28 | export interface CompleteTodo_completeTodo { 29 | __typename: "CompleteTodoResult"; 30 | success: boolean; 31 | todo: CompleteTodo_completeTodo_todo | null; 32 | error: CompleteTodo_completeTodo_error | null; 33 | } 34 | 35 | export interface CompleteTodo { 36 | completeTodo: CompleteTodo_completeTodo; 37 | } 38 | 39 | export interface CompleteTodoVariables { 40 | id: number; 41 | } 42 | -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/client/src/operations/mutations/__generated__/DeleteTodo.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | // ==================================================== 6 | // GraphQL mutation operation: DeleteTodo 7 | // ==================================================== 8 | 9 | export interface DeleteTodo_deleteTodo_todo { 10 | __typename: "Todo"; 11 | id: number; 12 | text: string; 13 | completed: boolean; 14 | } 15 | 16 | export interface DeleteTodo_deleteTodo_error { 17 | __typename: "TodoNotFoundError"; 18 | message: string; 19 | } 20 | 21 | export interface DeleteTodo_deleteTodo { 22 | __typename: "DeleteTodoResult"; 23 | success: boolean; 24 | todo: DeleteTodo_deleteTodo_todo | null; 25 | error: DeleteTodo_deleteTodo_error | null; 26 | } 27 | 28 | export interface DeleteTodo { 29 | deleteTodo: DeleteTodo_deleteTodo; 30 | } 31 | 32 | export interface DeleteTodoVariables { 33 | id: number; 34 | } 35 | -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/client/src/operations/mutations/__generated__/EditTodo.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | // ==================================================== 6 | // GraphQL mutation operation: EditTodo 7 | // ==================================================== 8 | 9 | export interface EditTodo_editTodo_todo { 10 | __typename: "Todo"; 11 | id: number; 12 | text: string; 13 | completed: boolean; 14 | } 15 | 16 | export interface EditTodo_editTodo_error_TodoNotFoundError { 17 | __typename: "TodoNotFoundError"; 18 | message: string; 19 | } 20 | 21 | export interface EditTodo_editTodo_error_TodoValidationError { 22 | __typename: "TodoValidationError"; 23 | message: string; 24 | } 25 | 26 | export type EditTodo_editTodo_error = EditTodo_editTodo_error_TodoNotFoundError | EditTodo_editTodo_error_TodoValidationError; 27 | 28 | export interface EditTodo_editTodo { 29 | __typename: "EditTodoResult"; 30 | success: boolean; 31 | todo: EditTodo_editTodo_todo | null; 32 | error: EditTodo_editTodo_error | null; 33 | } 34 | 35 | export interface EditTodo { 36 | editTodo: EditTodo_editTodo; 37 | } 38 | 39 | export interface EditTodoVariables { 40 | id: number; 41 | text: string; 42 | } 43 | -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/client/src/operations/mutations/addTodo.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { gql, useMutation, Reference } from "@apollo/client"; 3 | import * as AddTodoTypes from './__generated__/AddTodo'; 4 | 5 | export const ADD_TODO = gql` 6 | mutation AddTodo ($text: String!) { 7 | addTodo (text: $text) { 8 | success 9 | todo { 10 | id 11 | text 12 | completed 13 | } 14 | error { 15 | message 16 | } 17 | } 18 | } 19 | ` 20 | 21 | export function useAddTodo () { 22 | const [mutate, { data, error }] = useMutation< 23 | AddTodoTypes.AddTodo, 24 | AddTodoTypes.AddTodoVariables 25 | >( 26 | ADD_TODO, 27 | { 28 | update (cache, { data }) { 29 | cache.modify({ 30 | fields: { 31 | todos (existingTodos, { toReference }) { 32 | return { 33 | ...existingTodos, 34 | edges: [...existingTodos.edges, 35 | //@ts-ignore 36 | { __typename: 'TodosEdge', node: toReference(data.addTodo.todo) } 37 | ] 38 | } 39 | } 40 | } 41 | }) 42 | } 43 | } 44 | ) 45 | return { mutate, data, error }; 46 | } -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/client/src/operations/mutations/clearCompletedTodos.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { gql, useMutation } from "@apollo/client"; 3 | import * as ClearCompletedTodosTypes from './__generated__/ClearCompletedTodos' 4 | 5 | export const CLEAR_COMPLETED_TODOS = gql` 6 | mutation ClearCompletedTodos { 7 | clearCompletedTodos { 8 | success 9 | todos { 10 | id 11 | text 12 | completed 13 | } 14 | } 15 | } 16 | ` 17 | 18 | export function useClearCompletedTodos () { 19 | const [mutate, { data, error }] = useMutation< 20 | ClearCompletedTodosTypes.ClearCompletedTodos 21 | >( 22 | CLEAR_COMPLETED_TODOS, 23 | { 24 | update (cache) { 25 | let todoIdsToDelete: number[] = []; 26 | 27 | cache.modify({ 28 | fields: { 29 | todos (existingTodosConnection, { readField }) { 30 | 31 | const completedTodosCleared = { 32 | ...existingTodosConnection, 33 | edges: existingTodosConnection.edges.filter((edge: any) => { 34 | const shouldDelete = readField('completed', edge.node); 35 | 36 | if (shouldDelete) { 37 | todoIdsToDelete.push(readField('id', edge.node) as number) 38 | } 39 | 40 | return !shouldDelete; 41 | }) 42 | } 43 | 44 | return completedTodosCleared; 45 | } 46 | } 47 | }) 48 | 49 | todoIdsToDelete?.forEach((todoId) => { 50 | cache.evict({ id: `Todo:${todoId}` }) 51 | }) 52 | } 53 | } 54 | ) 55 | 56 | return { mutate, data, error }; 57 | } -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/client/src/operations/mutations/completeAllTodos.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { gql, useMutation } from "@apollo/client"; 3 | import * as CompleteAllTodosTypes from './__generated__/CompleteAllTodos' 4 | 5 | export const COMPLETE_ALL_TODOS = gql` 6 | mutation CompleteAllTodos { 7 | completeAllTodos { 8 | success 9 | todos { 10 | id 11 | text 12 | completed 13 | } 14 | } 15 | } 16 | ` 17 | 18 | export function useCompleteAllTodos () { 19 | const [mutate, { data, error }] = useMutation< 20 | CompleteAllTodosTypes.CompleteAllTodos 21 | >( 22 | COMPLETE_ALL_TODOS 23 | ) 24 | 25 | return { mutate, data, error }; 26 | } -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/client/src/operations/mutations/completeTodo.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { gql, useMutation } from "@apollo/client"; 3 | import * as CompleteTodoTypes from './__generated__/CompleteTodo' 4 | import { GET_ALL_TODOS } from "../queries/getAllTodos"; 5 | 6 | export const COMPLETE_TODO = gql` 7 | mutation CompleteTodo ($id: Int!) { 8 | completeTodo (id: $id) { 9 | success 10 | todo { 11 | id 12 | text 13 | completed 14 | } 15 | error { 16 | ... on TodoNotFoundError { 17 | message 18 | } 19 | ... on TodoAlreadyCompletedError { 20 | message 21 | } 22 | } 23 | } 24 | } 25 | ` 26 | 27 | export function useCompleteTodo () { 28 | const [mutate, { data, error }] = useMutation< 29 | CompleteTodoTypes.CompleteTodo, 30 | CompleteTodoTypes.CompleteTodoVariables 31 | >( 32 | COMPLETE_TODO 33 | ) 34 | 35 | return { mutate, data, error }; 36 | } -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/client/src/operations/mutations/deleteTodo.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { gql, useMutation } from "@apollo/client"; 3 | import * as DeleteTodoTypes from './__generated__/DeleteTodo' 4 | 5 | export const DELETE_TODO = gql` 6 | mutation DeleteTodo ($id: Int!) { 7 | deleteTodo (id: $id) { 8 | success 9 | todo { 10 | id 11 | text 12 | completed 13 | } 14 | error { 15 | ... on TodoNotFoundError { 16 | message 17 | } 18 | } 19 | } 20 | } 21 | ` 22 | 23 | export function useDeleteTodo () { 24 | const [mutate, { data, error }] = useMutation< 25 | DeleteTodoTypes.DeleteTodo, 26 | DeleteTodoTypes.DeleteTodoVariables 27 | >( 28 | DELETE_TODO, 29 | { 30 | update (cache, el) { 31 | const deletedId = el.data?.deleteTodo.todo?.id 32 | 33 | cache.modify({ 34 | fields: { 35 | todos (existingTodos, { readField }) { 36 | 37 | const newTodos = { 38 | ...existingTodos, 39 | edges: existingTodos.edges.filter((edge: any) => { 40 | return deletedId !== readField('id', edge.node); 41 | }) 42 | } 43 | 44 | return newTodos; 45 | } 46 | } 47 | }); 48 | 49 | cache.evict({ id: `Todo:${deletedId}` }) 50 | } 51 | } 52 | ) 53 | 54 | return { mutate, data, error }; 55 | } -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/client/src/operations/mutations/editTodo.tsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import { gql, useMutation } from "@apollo/client"; 4 | import * as EditTodoTypes from './__generated__/EditTodo' 5 | 6 | export const EDIT_TODO = gql` 7 | mutation EditTodo ($id: Int!, $text: String!) { 8 | editTodo (id: $id, text: $text) { 9 | success 10 | todo { 11 | id 12 | text 13 | completed 14 | } 15 | error { 16 | ... on TodoNotFoundError { 17 | message 18 | } 19 | ... on TodoValidationError { 20 | message 21 | } 22 | } 23 | } 24 | } 25 | ` 26 | 27 | /** 28 | * An update function is not needed for edits against single-item updates 29 | * because the cache will automatically update the cached version of the 30 | * item stored flat on the cache if we return the new item in the mutation 31 | * response. 32 | */ 33 | 34 | export function useEditTodo () { 35 | const [mutate, { data, error }] = useMutation< 36 | EditTodoTypes.EditTodo, 37 | EditTodoTypes.EditTodoVariables 38 | >( 39 | EDIT_TODO 40 | ) 41 | 42 | return { mutate, data, error }; 43 | } -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/client/src/operations/mutations/setVisibilityFilter/index.tsx: -------------------------------------------------------------------------------- 1 | 2 | import createSetVisibilityFilter from './setVisibilityFilter' 3 | import { visibilityFilterVar } from '../../../cache' 4 | 5 | export const setVisibilityFilter = createSetVisibilityFilter(visibilityFilterVar) 6 | -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/client/src/operations/mutations/setVisibilityFilter/setVisibilityFilter.spec.tsx: -------------------------------------------------------------------------------- 1 | 2 | import createSetVisibilityFilter from './setVisibilityFilter' 3 | import { VisibilityFilters } from '../../../models/VisibilityFilter'; 4 | import { mockVisibilityFilter } from '../../../tests/mocks/mockVisibilityFilter'; 5 | 6 | const setVisibilityFilter = createSetVisibilityFilter(mockVisibilityFilter); 7 | 8 | describe('setVisibilityFilter hook', () => { 9 | beforeEach(() => mockVisibilityFilter(VisibilityFilters.SHOW_ALL)); 10 | 11 | it('should change the visibility filter', () => { 12 | setVisibilityFilter(VisibilityFilters.SHOW_ALL); 13 | 14 | expect( 15 | mockVisibilityFilter().displayName 16 | ).toEqual("All") 17 | 18 | setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED); 19 | 20 | expect( 21 | mockVisibilityFilter().displayName 22 | ).toEqual("Completed") 23 | }); 24 | }) 25 | 26 | 27 | -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/client/src/operations/mutations/setVisibilityFilter/setVisibilityFilter.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { VisibilityFilter } from "../../../models/VisibilityFilter"; 3 | import { ReactiveVar } from "@apollo/client"; 4 | 5 | export default (visibilityFilterVar: ReactiveVar) => { 6 | return (filter: VisibilityFilter) => { 7 | visibilityFilterVar(filter); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/client/src/operations/queries/__generated__/GetAllTodos.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | // ==================================================== 6 | // GraphQL query operation: GetAllTodos 7 | // ==================================================== 8 | 9 | export interface GetAllTodos_todos_edges_node { 10 | __typename: "Todo"; 11 | id: number; 12 | text: string; 13 | completed: boolean; 14 | } 15 | 16 | export interface GetAllTodos_todos_edges { 17 | __typename: "TodosEdge"; 18 | node: GetAllTodos_todos_edges_node; 19 | } 20 | 21 | export interface GetAllTodos_todos { 22 | __typename: "TodosConnection"; 23 | edges: (GetAllTodos_todos_edges | null)[]; 24 | } 25 | 26 | export interface GetAllTodos { 27 | todos: GetAllTodos_todos; 28 | } 29 | -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/client/src/operations/queries/getAllTodos.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { gql } from "@apollo/client"; 3 | 4 | export const GET_ALL_TODOS = gql` 5 | query GetAllTodos { 6 | todos { 7 | edges { 8 | node { 9 | id 10 | text 11 | completed 12 | } 13 | } 14 | } 15 | } 16 | ` -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/client/src/operations/queries/getVisibilityFilter.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { gql } from "@apollo/client"; 3 | 4 | export const GET_VISIBILITY_FILTER = gql` 5 | query GetVisibilityFilter { 6 | visibilityFilter @client { 7 | id 8 | displayName 9 | } 10 | } 11 | ` -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/client/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/client/src/tests/createMockReactiveVar.spec.ts: -------------------------------------------------------------------------------- 1 | import { mockTodosVar } from "./mocks/mockTodosVar"; 2 | 3 | describe('Testing mock reactive variables', () => { 4 | beforeEach(() => mockTodosVar([])); 5 | 6 | it('should set the current value if provided', () => { 7 | mockTodosVar([{ id: 0, text: 'First todo', completed: false }]); 8 | expect(mockTodosVar()?.length).toEqual(1); 9 | }) 10 | 11 | it('should overwrite the current value', () => { 12 | mockTodosVar([{ id: 0, text: 'First todo', completed: false }]); 13 | expect(mockTodosVar()?.length).toEqual(1); 14 | 15 | mockTodosVar([{ id: 1, text: 'Second todo', completed: false }]); 16 | expect(mockTodosVar()?.length).toEqual(1); 17 | }); 18 | 19 | it('should not overwrite the current value if no value provided', () => { 20 | mockTodosVar([ 21 | { id: 0, text: 'First todo', completed: false }, 22 | { id: 1, text: 'Second todo', completed: false } 23 | ]); 24 | expect(mockTodosVar()?.length).toEqual(2); 25 | 26 | mockTodosVar(); 27 | expect(mockTodosVar()?.length).toEqual(2); 28 | }) 29 | }) -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/client/src/tests/createMockReactiveVar.ts: -------------------------------------------------------------------------------- 1 | 2 | import { ReactiveVar } from "@apollo/client"; 3 | 4 | // This is how to create a mock reactive variable. 5 | // It's a good idea to do this because then we can test our 6 | // interaction logic. 7 | 8 | export function createMockReactiveVar (defaultValue?: T): ReactiveVar { 9 | let currentValue: T | undefined = defaultValue; 10 | 11 | return function mockReactiveVar ( 12 | newValue?: T 13 | ) : T { 14 | if (newValue) { 15 | currentValue = newValue; 16 | } 17 | return currentValue as T; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/client/src/tests/mocks/mockTodosVar.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Todos } from "../../models/Todos"; 3 | import { createMockReactiveVar } from "../createMockReactiveVar"; 4 | 5 | export const mockTodosVar = createMockReactiveVar([]); -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/client/src/tests/mocks/mockVisibilityFilter.ts: -------------------------------------------------------------------------------- 1 | 2 | import { createMockReactiveVar } from "../createMockReactiveVar"; 3 | import { VisibilityFilter, VisibilityFilters } from "../../models/VisibilityFilter"; 4 | 5 | export const mockVisibilityFilter = createMockReactiveVar( 6 | VisibilityFilters.SHOW_ALL 7 | ); 8 | -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react" 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/server/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/server/README.md: -------------------------------------------------------------------------------- 1 | # apollo-remote-state 2 | 3 | Summary: Hooking Apollo Client up to a remote GraphQL API, the client-side cache is smart enough to automatically update the cache after most mutations successfully complete. For mutations that perform interactions against arrays or have additional client-side side-effects, we can help the cache decide what to do next by writing our update logic in the `useMutation`'s `update` function. 4 | 5 | ## Getting started 6 | 7 | There is a `client/` and `server/` portion to this example. 8 | 9 | ### Running the client 10 | 11 | To start the project, run the following commands: 12 | 13 | ``` 14 | cd client 15 | npm install && npm run start 16 | ``` 17 | 18 | The app should start at http://localhost:3000/. 19 | 20 | ### Running the server 21 | 22 | To start the project, run the following commands: 23 | 24 | ``` 25 | cd server 26 | npm install && npm run start 27 | ``` 28 | 29 | The server should start at http://localhost:4000/. 30 | -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/server/apollo.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | service: { 3 | name: "ac3-todos-backend" 4 | } 5 | } -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/server/codegen.yml: -------------------------------------------------------------------------------- 1 | overwrite: true 2 | schema: "http://localhost:4000" 3 | generates: 4 | ./src/generated/graphql.ts: 5 | plugins: 6 | - "typescript" 7 | - "typescript-resolvers" 8 | ./graphql.schema.json: 9 | plugins: 10 | - "introspection" 11 | -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/server/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src", "bin"], 3 | "ext": ".ts,.js", 4 | "ignore": [], 5 | "exec": "ts-node --transpile-only ./src/index.ts" 6 | } -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "nodemon", 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "codegen": "graphql-codegen --config ./codegen.yml" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "apollo-server": "^2.11.0", 16 | "graphql": "^14.6.0" 17 | }, 18 | "devDependencies": { 19 | "@graphql-codegen/cli": "^1.13.1", 20 | "@graphql-codegen/introspection": "1.13.0", 21 | "@graphql-codegen/typescript": "1.13.0", 22 | "@graphql-codegen/typescript-resolvers": "1.13.0", 23 | "nodemon": "^2.0.2", 24 | "ts-node": "^8.6.2", 25 | "typescript": "^3.8.3" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/server/src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import { ApolloServer } from 'apollo-server' 3 | import { typeDefs } from './schema' 4 | import { resolvers } from './resolvers' 5 | import { todosRepo } from './modules/todos/repos'; 6 | import { TodoRepo } from './modules/todos/repos/todoRepo'; 7 | 8 | export type Context = { todosRepo: TodoRepo } 9 | 10 | const server = new ApolloServer({ 11 | context: () => ({ todosRepo } as Context), 12 | typeDefs, 13 | resolvers 14 | }); 15 | 16 | server.listen().then(({ url }) => { 17 | console.log(`🚀 Server ready at ${url}`); 18 | }); -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/server/src/modules/todos/repos/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import { InMemoryTodoRepo } from "./implementations/InMemoryTodoRepo"; 3 | 4 | const todosRepo = new InMemoryTodoRepo(); 5 | 6 | export { todosRepo }; -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/server/src/modules/todos/repos/todoRepo.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Todo } from "../../../generated/graphql"; 3 | 4 | export interface TodoRepo { 5 | addTodo(text: string): Promise; 6 | completeTodo (id: number): Promise; 7 | clearCompletedTodos(): Promise; 8 | completeAllTodos (): Promise; 9 | deleteTodo(id: number): Promise; 10 | editTodo(id: number, text: string): Promise; 11 | getAllTodos (): Promise; 12 | getTodoById (id: number): Promise; 13 | getLastTodo (): Promise; 14 | } 15 | -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/server/src/shared/mappers/todoMapper.ts: -------------------------------------------------------------------------------- 1 | import { Todo, TodosConnection, TodosEdge } from "../../generated/graphql"; 2 | 3 | export class TodoMapper { 4 | 5 | public static toEdge (todo: Todo): TodosEdge { 6 | return { 7 | node: todo, 8 | cursor: "" 9 | }; 10 | } 11 | 12 | public static toTodosConnection (todos: Todo[]) : TodosConnection { 13 | return { 14 | edges: todos.map(t => this.toEdge(t)), 15 | pageInfo: { 16 | hasPreviousPage: false, 17 | hasNextPage: false, 18 | startCursor: "", 19 | endCursor: "" 20 | } 21 | }; 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/server/src/shared/utils/numberUtils.ts: -------------------------------------------------------------------------------- 1 | 2 | export class NumberUtils { 3 | public static isANumber (num: any): boolean { 4 | return !isNaN(num); 5 | } 6 | 7 | public static isANonNegativeNumber (num?: any): boolean { 8 | return !isNaN(num) && num >= 0; 9 | } 10 | } -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/server/src/shared/utils/paginationUtils.ts: -------------------------------------------------------------------------------- 1 | import { Result } from "../core/result"; 2 | import { NumberUtils } from "./numberUtils"; 3 | 4 | interface Node { 5 | id: number 6 | } 7 | 8 | type GenericNodeList = Node[]; 9 | 10 | export class PaginationUtils { 11 | public static filterByBeforeAndAfter ( 12 | items: GenericNodeList, 13 | after?: string, 14 | before?: string, 15 | ) : GenericNodeList { 16 | const isAfterSet = !!after === true; 17 | const isBeforeSet = !!before === true; 18 | 19 | if (isAfterSet) { 20 | const afterIndex = items.findIndex((t) => t.id === Number(after)) 21 | const afterIndexFound = afterIndex !== -1; 22 | 23 | if (afterIndexFound) { 24 | items = items.slice(afterIndex + 1) 25 | } 26 | } 27 | 28 | if (isBeforeSet) { 29 | const beforeIndex = items.findIndex((t) => t.id === Number(after)) 30 | const beforeIndexFond = beforeIndex !== -1; 31 | 32 | if (beforeIndexFond) { 33 | items = items.slice(0, beforeIndex) 34 | } 35 | } 36 | 37 | return items; 38 | } 39 | 40 | public static limitByFirstAndLast( 41 | items: T[], 42 | first?: number, 43 | last?: number 44 | ): Result { 45 | const isFirstSet = NumberUtils.isANumber(first); 46 | const isLastSet = NumberUtils.isANumber(last); 47 | 48 | if (isFirstSet) { 49 | const isFirstAPositiveNumber = NumberUtils.isANonNegativeNumber(first); 50 | 51 | if (!isFirstAPositiveNumber) { 52 | return Result.fail("First has to be greater than 0"); 53 | } 54 | 55 | if (items.length > first) { 56 | return Result.ok(items.slice(0, first)); 57 | } 58 | } 59 | 60 | if (isLastSet) { 61 | const isLastAPositiveNumber = NumberUtils.isANonNegativeNumber(last); 62 | 63 | if (!isLastAPositiveNumber) { 64 | return Result.fail("Last has to be greater than 0"); 65 | } 66 | 67 | if (items.length > last) { 68 | return Result.ok(items.slice(0, last)); 69 | } 70 | } 71 | 72 | return Result.ok(items); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /apollo-remote-state-advanced-cache-apis/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "pretty": true, 7 | "sourceMap": true, 8 | "target": "es2017", 9 | "outDir": "./dist", 10 | "lib": ["es6"], 11 | "resolveJsonModule": true, 12 | "types": ["node"], 13 | "typeRoots" : ["./node_modules/@types", "./src/@types"], 14 | "experimentalDecorators": true, 15 | "emitDecoratorMetadata": true, 16 | "esModuleInterop": true 17 | }, 18 | "include": [ 19 | "src/**/*.ts", 20 | ], 21 | "exclude": [ 22 | "node_modules", 23 | ] 24 | } -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/ac3-state-management-examples/57ddd7aff0b86e12ea7f63db625c004add712ba4/apollo-remote-state-no-relay/.DS_Store -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/README.md: -------------------------------------------------------------------------------- 1 | # apollo-remote-state 2 | 3 | Summary: Hooking Apollo Client up to a remote GraphQL API, the client-side cache is smart enough to automatically update the cache after most mutations successfully complete. For mutations that perform interactions against arrays or have additional client-side side-effects, we can help the cache decide what to do next by writing our update logic in the `useMutation`'s `update` function. 4 | 5 | ## Getting started 6 | 7 | There is a `client/` and `server/` portion to this example. 8 | 9 | ### Running the client 10 | 11 | To start the project, run the following commands: 12 | 13 | ``` 14 | cd client 15 | npm install && npm run start 16 | ``` 17 | 18 | The app should start at http://localhost:3000/. 19 | 20 | ### Running the server 21 | 22 | To start the project, run the following commands: 23 | 24 | ``` 25 | cd server 26 | npm install && npm run start 27 | ``` 28 | 29 | The server should start at http://localhost:4000/. 30 | -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/client/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # production 7 | build 8 | 9 | # misc 10 | .DS_Store 11 | npm-debug.log 12 | .env -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/client/NOTES.md: -------------------------------------------------------------------------------- 1 | 2 | ## How do I set initial data for something? 3 | 4 | Q: In Redux, we have the concept of initial state. What would you recommend for doing this? 5 | 6 | A: You can use makeVar and give it some default data. 7 | 8 | ```typescript 9 | import { InMemoryCache, Reference } from "@apollo/client"; 10 | 11 | export const cache: InMemoryCache = new InMemoryCache({ 12 | typePolicies: { 13 | Query: { 14 | fields: { 15 | todos () { 16 | return todosVar(); 17 | }, 18 | } 19 | } 20 | } 21 | }); 22 | 23 | const todosInitialValue: Todos = [ 24 | { 25 | id: 0, 26 | completed: false, 27 | text: "Use Apollo Client 3" 28 | } 29 | ] 30 | 31 | export const todosVar = cache.makeVar(todosInitialValue); 32 | ``` 33 | 34 | ## If there is no remote GraphQL uri yet (because I'm doing everything locally), or if I never actually intend for there to be one at all, what can I do? It says I need to pass in a valid uri. 35 | 36 | A: If you just need to stub out a GraphQL endpoint for some reason and you don't ever really want to connect to a remote one, you can use 37 | 38 | ```typescript 39 | const client = new ApolloClient({ 40 | cache, 41 | link: ApolloLink.empty() 42 | }); 43 | ``` 44 | 45 | This is nice because you can be explicit about _nothingness_. 46 | 47 | It's common for when: 48 | 49 | - We're building something on the client-side and don't want to try to reach out to a remote GraphQL server yet because (A: it's not ready, B: we will never need one). 50 | 51 | ## How to resolve data that is only on the client side 52 | 53 | Use the `@client` directive. 54 | 55 | ```typescript 56 | client 57 | .query({ 58 | query: gql` 59 | { 60 | todos { 61 | id @client 62 | text @client 63 | completed @client 64 | } 65 | } 66 | ` 67 | }) 68 | .then(result => console.log(result)); 69 | ``` -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/client/__generated__/globalTypes.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | //============================================================== 6 | // START Enums and Input Objects 7 | //============================================================== 8 | 9 | //============================================================== 10 | // END Enums and Input Objects 11 | //============================================================== 12 | -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/client/apollo.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | client: { 3 | service: { 4 | name: 'ac3-todos-backend', 5 | url: `http://localhost:4000`, 6 | }, 7 | }, 8 | }; -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todomvc-apollo", 3 | "version": "0.0.1", 4 | "private": true, 5 | "devDependencies": { 6 | "@types/classnames": "^2.2.9", 7 | "@types/react-test-renderer": "^16.9.2", 8 | "react-scripts": "^3.0.0", 9 | "react-test-renderer": "^16.8.6" 10 | }, 11 | "dependencies": { 12 | "@apollo/client": "^3.0.1", 13 | "@types/jest": "^25.1.3", 14 | "@types/node": "^13.7.6", 15 | "@types/react": "^16.9.23", 16 | "@types/react-dom": "^16.9.5", 17 | "classnames": "^2.2.6", 18 | "graphql": "^14.6.0", 19 | "prop-types": "^15.7.2", 20 | "react": "^16.8.6", 21 | "react-dom": "^16.8.6", 22 | "reselect": "^4.0.0", 23 | "todomvc-app-css": "^2.2.0", 24 | "typescript": "^3.8.2" 25 | }, 26 | "scripts": { 27 | "start": "react-scripts start", 28 | "build": "react-scripts build", 29 | "eject": "react-scripts eject", 30 | "test": "react-scripts test --env=node", 31 | "codegen": "apollo client:codegen --target typescript --excludes src/operations/queries/getVisibilityFilter.tsx" 32 | }, 33 | "browserslist": [ 34 | ">0.2%", 35 | "not dead", 36 | "not ie <= 11", 37 | "not op_mini all" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Apollo Client 3 Todo MVC Example 7 | 8 | 9 |
    10 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/client/src/cache.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Todos } from "./models/Todos"; 3 | import { VisibilityFilter, VisibilityFilters } from "./models/VisibilityFilter"; 4 | 5 | let currentTodosValue: Todos = []; 6 | 7 | export function mockTodosVar (newValue?: Todos | undefined) : Todos { 8 | if (newValue) { 9 | currentTodosValue = newValue; 10 | } 11 | return currentTodosValue; 12 | } 13 | 14 | let currentVisibilityFilter: VisibilityFilter = VisibilityFilters.SHOW_ALL; 15 | 16 | export function mockVisibilityFilter (newValue?: VisibilityFilter | undefined) : VisibilityFilter { 17 | if (newValue) { 18 | currentVisibilityFilter = newValue; 19 | } 20 | return currentVisibilityFilter; 21 | } 22 | 23 | describe('mockTodosVar', () => { 24 | beforeEach(() => mockTodosVar([])); 25 | 26 | it('should set the current value if provided', () => { 27 | mockTodosVar([{ id: 0, text: 'First todo', completed: false }]); 28 | expect(mockTodosVar().length).toEqual(1); 29 | }) 30 | 31 | it('should overwrite the current value', () => { 32 | mockTodosVar([{ id: 0, text: 'First todo', completed: false }]); 33 | expect(mockTodosVar().length).toEqual(1); 34 | 35 | mockTodosVar([{ id: 1, text: 'Second todo', completed: false }]); 36 | expect(mockTodosVar().length).toEqual(1); 37 | }); 38 | 39 | it('should not overwrite the current value if no value provided', () => { 40 | mockTodosVar([ 41 | { id: 0, text: 'First todo', completed: false }, 42 | { id: 1, text: 'Second todo', completed: false } 43 | ]); 44 | expect(mockTodosVar().length).toEqual(2); 45 | 46 | mockTodosVar(); 47 | expect(mockTodosVar().length).toEqual(2); 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/client/src/cache.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { InMemoryCache, makeVar } from "@apollo/client"; 3 | import { VisibilityFilters, VisibilityFilter } from "./models/VisibilityFilter"; 4 | 5 | export const cache: InMemoryCache = new InMemoryCache({ 6 | typePolicies: { 7 | Query: { 8 | fields: { 9 | visibilityFilter: { 10 | read () { 11 | return visibilityFilterVar(); 12 | } 13 | } 14 | } 15 | } 16 | } 17 | }); 18 | 19 | /** 20 | * Set initial values when we create cache variables. 21 | */ 22 | 23 | export const visibilityFilterVar = makeVar( 24 | VisibilityFilters.SHOW_ALL 25 | ) 26 | -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/client/src/components/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Header from '../containers/Header' 3 | import MainSection from '../containers/MainSection' 4 | 5 | const App = () => ( 6 |
    7 |
    8 | 9 |
    10 | ) 11 | 12 | export default App 13 | -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/client/src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import Link from './Link' 4 | import { VisibilityFilters, VisibilityFilter } from '../models/VisibilityFilter'; 5 | 6 | interface FooterProps { 7 | activeVisibilityFilter: VisibilityFilter; 8 | activeCount: number; 9 | completedCount: number; 10 | onClearCompleted: () => void; 11 | setVisibilityFilter: (filter: VisibilityFilter) => void; 12 | } 13 | 14 | const Footer = (props: FooterProps) => { 15 | const { activeCount, completedCount, onClearCompleted, activeVisibilityFilter, setVisibilityFilter } = props; 16 | const itemWord = activeCount === 1 ? "item" : "items"; 17 | return ( 18 |
    19 | 20 | {activeCount || "No"} {itemWord} left 21 | 22 |
      23 | {Object.keys(VisibilityFilters).map((key) => VisibilityFilters[key]).map((filter) => ( 24 |
    • 25 | setVisibilityFilter(filter)}> 28 | {filter.displayName} 29 |
    • 30 | ))} 31 |
    32 | {!!completedCount && ( 33 | 36 | )} 37 |
    38 | ); 39 | }; 40 | 41 | Footer.propTypes = { 42 | completedCount: PropTypes.number.isRequired, 43 | activeCount: PropTypes.number.isRequired, 44 | onClearCompleted: PropTypes.func.isRequired, 45 | setVisibilityFilter: PropTypes.func.isRequired 46 | } 47 | 48 | export default Footer 49 | -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/client/src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import TodoTextInput from './TodoTextInput' 4 | 5 | interface HeaderProps { 6 | addTodo: (text: string) => void; 7 | } 8 | 9 | const Header = ({ addTodo }: HeaderProps) => ( 10 |
    11 |

    todos

    12 | { 15 | if (text.length !== 0) { 16 | addTodo(text); 17 | } 18 | }} 19 | placeholder="What needs to be done?" 20 | /> 21 |
    22 | ); 23 | 24 | Header.propTypes = { 25 | addTodo: PropTypes.func.isRequired 26 | } 27 | 28 | export default Header 29 | -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/client/src/components/Link.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import classnames from 'classnames' 4 | 5 | interface LinkProps { 6 | setFilter: () => any; 7 | active: boolean; 8 | children: any; 9 | } 10 | 11 | const Link = ({ active, children, setFilter }: LinkProps) => 12 | ( 13 | // eslint-disable-next-line jsx-a11y/anchor-is-valid 14 | setFilter()} 18 | > 19 | {children} 20 | 21 | ) 22 | 23 | 24 | Link.propTypes = { 25 | active: PropTypes.bool.isRequired, 26 | children: PropTypes.node.isRequired, 27 | setFilter: PropTypes.func.isRequired 28 | } 29 | 30 | export default Link 31 | -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/client/src/components/MainSection.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import Footer from './Footer' 4 | import VisibleTodoList from '../containers/VisibleTodoList' 5 | import { VisibilityFilter } from '../models/VisibilityFilter'; 6 | 7 | interface MainSectionProps { 8 | activeVisibilityFilter: VisibilityFilter; 9 | todosCount: number; 10 | completedCount: number; 11 | actions: any; 12 | } 13 | 14 | /** 15 | * This is a view component. It doesn't define anything that 16 | * is responsible for querying or mutating, it just relies 17 | * on it from the upper layer component (namely, actions) 18 | */ 19 | 20 | const MainSection = ({ 21 | activeVisibilityFilter, 22 | todosCount, 23 | completedCount, 24 | actions, 25 | }: MainSectionProps) => ( 26 |
    27 | {!!todosCount && ( 28 | 29 | 35 | 37 | )} 38 | 39 | {!!todosCount && ( 40 |
    47 | )} 48 |
    49 | ); 50 | 51 | MainSection.propTypes = { 52 | todosCount: PropTypes.number.isRequired, 53 | completedCount: PropTypes.number.isRequired, 54 | actions: PropTypes.object.isRequired 55 | } 56 | 57 | export default MainSection; 58 | -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/client/src/components/TodoItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import classnames from 'classnames' 4 | import TodoTextInput from './TodoTextInput' 5 | 6 | type Props = any; 7 | 8 | export default class TodoItem extends Component { 9 | static propTypes = { 10 | todo: PropTypes.object.isRequired, 11 | editTodo: PropTypes.func.isRequired, 12 | deleteTodo: PropTypes.func.isRequired, 13 | completeTodo: PropTypes.func.isRequired 14 | } 15 | 16 | state = { 17 | editing: false 18 | } 19 | 20 | handleDoubleClick = () => { 21 | this.setState({ editing: true }) 22 | } 23 | 24 | handleSave = (id: number, text: string) => { 25 | if (text.length === 0) { 26 | this.props.deleteTodo(id) 27 | } else { 28 | this.props.editTodo(id, text) 29 | } 30 | this.setState({ editing: false }) 31 | } 32 | 33 | render() { 34 | const { todo, completeTodo, deleteTodo } = this.props 35 | 36 | let element 37 | if (this.state.editing) { 38 | element = ( 39 | this.handleSave(todo.id, text)} /> 42 | ) 43 | } else { 44 | element = ( 45 |
    46 | { 50 | completeTodo(todo.id) 51 | }} /> 52 | 55 |
    58 | ) 59 | } 60 | 61 | return ( 62 |
  • 66 | {element} 67 |
  • 68 | ) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/client/src/components/TodoList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import TodoItem from './TodoItem' 4 | import { Todo } from '../models/Todos' 5 | 6 | const TodoList = ({ filteredTodos, actions }: any) => ( 7 |
      8 | {filteredTodos.map((todo: Todo) => 9 | 10 | )} 11 |
    12 | ) 13 | 14 | TodoList.propTypes = { 15 | filteredTodos: PropTypes.arrayOf(PropTypes.shape({ 16 | id: PropTypes.number.isRequired, 17 | completed: PropTypes.bool.isRequired, 18 | text: PropTypes.string.isRequired 19 | }).isRequired).isRequired, 20 | actions: PropTypes.object.isRequired 21 | } 22 | 23 | export default TodoList 24 | -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/client/src/components/TodoTextInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import classnames from 'classnames' 4 | 5 | export default class TodoTextInput extends Component { 6 | static propTypes = { 7 | onSave: PropTypes.func.isRequired, 8 | text: PropTypes.string, 9 | placeholder: PropTypes.string, 10 | editing: PropTypes.bool, 11 | newTodo: PropTypes.bool 12 | } 13 | 14 | state = { 15 | text: this.props.text || '' 16 | } 17 | 18 | handleSubmit = (e: any) => { 19 | const text = e.target.value.trim() 20 | if (e.which === 13) { 21 | this.props.onSave(text) 22 | if (this.props.newTodo) { 23 | this.setState({ text: '' }) 24 | } 25 | } 26 | } 27 | 28 | handleChange = (e: any) => { 29 | this.setState({ text: e.target.value }) 30 | } 31 | 32 | handleBlur = (e: any) => { 33 | if (!this.props.newTodo) { 34 | this.props.onSave(e.target.value) 35 | } 36 | } 37 | 38 | render() { 39 | return ( 40 | 52 | ) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/client/src/containers/Header.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import Header from '../components/Header' 4 | import { useAddTodo } from '../operations/mutations/addTodo'; 5 | 6 | export default function () { 7 | const { mutate } = useAddTodo(); 8 | return
    mutate({ variables: { text } })} />; 9 | } -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/client/src/containers/MainSection.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import MainSection from '../components/MainSection' 4 | import { useQuery } from '@apollo/client' 5 | import { VisibilityFilter } from '../models/VisibilityFilter' 6 | import { GET_ALL_TODOS } from '../operations/queries/getAllTodos' 7 | import { GET_VISIBILITY_FILTER } from '../operations/queries/getVisibilityFilter' 8 | import { GetAllTodos } from '../operations/queries/__generated__/GetAllTodos' 9 | import { useClearCompletedTodos } from '../operations/mutations/clearCompletedTodos' 10 | import { useCompleteAllTodos } from '../operations/mutations/completeAllTodos' 11 | import { setVisibilityFilter } from '../operations/mutations/setVisibilityFilter' 12 | 13 | export default function Main () { 14 | const { 15 | loading: isTodosLoading, 16 | data, 17 | error: todosError 18 | } = useQuery(GET_ALL_TODOS); 19 | const { data: visibilityFilter } = useQuery(GET_VISIBILITY_FILTER); 20 | 21 | const { mutate: clearCompletedTodos } = useClearCompletedTodos(); 22 | const { mutate: completeAllTodos } = useCompleteAllTodos(); 23 | 24 | if (isTodosLoading) return
    Loading...
    25 | if (todosError) return
    An error occurred {JSON.stringify(todosError)}
    26 | if (!data) return
    None
    ; 27 | 28 | return ( 29 | t ? t.completed : false).length} 33 | actions={{ 34 | completeAllTodos, 35 | setVisibilityFilter, 36 | clearCompletedTodos, 37 | }} 38 | /> 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/client/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import App from './components/App' 4 | import { ApolloClient } from '@apollo/client'; 5 | import { ApolloProvider } from '@apollo/client'; 6 | import 'todomvc-app-css/index.css' 7 | import { cache } from './cache'; 8 | 9 | export const client = new ApolloClient({ 10 | cache, 11 | uri: 'http://localhost:4000/', 12 | headers: { 13 | authorization: localStorage.getItem('token') || '', 14 | 'client-name': 'ac3-todos-backend', 15 | 'client-version': '1.0.0', 16 | }, 17 | connectToDevTools: true, 18 | }); 19 | 20 | render( 21 | 22 | 23 | , 24 | document.getElementById('root') 25 | ) 26 | -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/client/src/models/Todos.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { GetAllTodos_todos } from "../operations/queries/__generated__/GetAllTodos"; 3 | 4 | export type Todo = GetAllTodos_todos; 5 | 6 | export type Todos = Todo[]; -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/client/src/models/VisibilityFilter.tsx: -------------------------------------------------------------------------------- 1 | 2 | export type VisibilityFilter = { 3 | id: string; 4 | displayName: string; 5 | } 6 | 7 | export const VisibilityFilters: { [filter: string]: VisibilityFilter } = { 8 | SHOW_ALL: { 9 | id: "show_all", 10 | displayName: "All" 11 | }, 12 | SHOW_COMPLETED: { 13 | id: "show_completed", 14 | displayName: "Completed" 15 | }, 16 | SHOW_ACTIVE: { 17 | id: "show_active", 18 | displayName: "Active" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/client/src/operations/mutations/__generated__/AddTodo.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | // ==================================================== 6 | // GraphQL mutation operation: AddTodo 7 | // ==================================================== 8 | 9 | export interface AddTodo_addTodo_todo { 10 | __typename: "Todo"; 11 | id: number; 12 | text: string; 13 | completed: boolean; 14 | } 15 | 16 | export interface AddTodo_addTodo_error { 17 | __typename: "TodoValidationError"; 18 | message: string; 19 | } 20 | 21 | export interface AddTodo_addTodo { 22 | __typename: "AddTodoResult"; 23 | success: boolean; 24 | todo: AddTodo_addTodo_todo | null; 25 | error: AddTodo_addTodo_error | null; 26 | } 27 | 28 | export interface AddTodo { 29 | addTodo: AddTodo_addTodo; 30 | } 31 | 32 | export interface AddTodoVariables { 33 | text: string; 34 | } 35 | -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/client/src/operations/mutations/__generated__/ClearCompletedTodos.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | // ==================================================== 6 | // GraphQL mutation operation: ClearCompletedTodos 7 | // ==================================================== 8 | 9 | export interface ClearCompletedTodos_clearCompletedTodos_todos { 10 | __typename: "Todo"; 11 | id: number; 12 | text: string; 13 | completed: boolean; 14 | } 15 | 16 | export interface ClearCompletedTodos_clearCompletedTodos { 17 | __typename: "ClearCompletedTodosResult"; 18 | success: boolean; 19 | todos: (ClearCompletedTodos_clearCompletedTodos_todos | null)[]; 20 | } 21 | 22 | export interface ClearCompletedTodos { 23 | clearCompletedTodos: ClearCompletedTodos_clearCompletedTodos; 24 | } 25 | -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/client/src/operations/mutations/__generated__/CompleteAllTodos.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | // ==================================================== 6 | // GraphQL mutation operation: CompleteAllTodos 7 | // ==================================================== 8 | 9 | export interface CompleteAllTodos_completeAllTodos_todos { 10 | __typename: "Todo"; 11 | id: number; 12 | text: string; 13 | completed: boolean; 14 | } 15 | 16 | export interface CompleteAllTodos_completeAllTodos { 17 | __typename: "CompleteAllTodosResult"; 18 | success: boolean; 19 | todos: (CompleteAllTodos_completeAllTodos_todos | null)[]; 20 | } 21 | 22 | export interface CompleteAllTodos { 23 | completeAllTodos: CompleteAllTodos_completeAllTodos; 24 | } 25 | -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/client/src/operations/mutations/__generated__/CompleteTodo.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | // ==================================================== 6 | // GraphQL mutation operation: CompleteTodo 7 | // ==================================================== 8 | 9 | export interface CompleteTodo_completeTodo_todo { 10 | __typename: "Todo"; 11 | id: number; 12 | text: string; 13 | completed: boolean; 14 | } 15 | 16 | export interface CompleteTodo_completeTodo_error_TodoNotFoundError { 17 | __typename: "TodoNotFoundError"; 18 | message: string; 19 | } 20 | 21 | export interface CompleteTodo_completeTodo_error_TodoAlreadyCompletedError { 22 | __typename: "TodoAlreadyCompletedError"; 23 | message: string; 24 | } 25 | 26 | export type CompleteTodo_completeTodo_error = CompleteTodo_completeTodo_error_TodoNotFoundError | CompleteTodo_completeTodo_error_TodoAlreadyCompletedError; 27 | 28 | export interface CompleteTodo_completeTodo { 29 | __typename: "CompleteTodoResult"; 30 | success: boolean; 31 | todo: CompleteTodo_completeTodo_todo | null; 32 | error: CompleteTodo_completeTodo_error | null; 33 | } 34 | 35 | export interface CompleteTodo { 36 | completeTodo: CompleteTodo_completeTodo; 37 | } 38 | 39 | export interface CompleteTodoVariables { 40 | id: number; 41 | } 42 | -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/client/src/operations/mutations/__generated__/DeleteTodo.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | // ==================================================== 6 | // GraphQL mutation operation: DeleteTodo 7 | // ==================================================== 8 | 9 | export interface DeleteTodo_deleteTodo_todo { 10 | __typename: "Todo"; 11 | id: number; 12 | text: string; 13 | completed: boolean; 14 | } 15 | 16 | export interface DeleteTodo_deleteTodo_error { 17 | __typename: "TodoNotFoundError"; 18 | message: string; 19 | } 20 | 21 | export interface DeleteTodo_deleteTodo { 22 | __typename: "DeleteTodoResult"; 23 | success: boolean; 24 | todo: DeleteTodo_deleteTodo_todo | null; 25 | error: DeleteTodo_deleteTodo_error | null; 26 | } 27 | 28 | export interface DeleteTodo { 29 | deleteTodo: DeleteTodo_deleteTodo; 30 | } 31 | 32 | export interface DeleteTodoVariables { 33 | id: number; 34 | } 35 | -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/client/src/operations/mutations/__generated__/EditTodo.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | // ==================================================== 6 | // GraphQL mutation operation: EditTodo 7 | // ==================================================== 8 | 9 | export interface EditTodo_editTodo_todo { 10 | __typename: "Todo"; 11 | id: number; 12 | text: string; 13 | completed: boolean; 14 | } 15 | 16 | export interface EditTodo_editTodo_error_TodoNotFoundError { 17 | __typename: "TodoNotFoundError"; 18 | message: string; 19 | } 20 | 21 | export interface EditTodo_editTodo_error_TodoValidationError { 22 | __typename: "TodoValidationError"; 23 | message: string; 24 | } 25 | 26 | export type EditTodo_editTodo_error = EditTodo_editTodo_error_TodoNotFoundError | EditTodo_editTodo_error_TodoValidationError; 27 | 28 | export interface EditTodo_editTodo { 29 | __typename: "EditTodoResult"; 30 | success: boolean; 31 | todo: EditTodo_editTodo_todo | null; 32 | error: EditTodo_editTodo_error | null; 33 | } 34 | 35 | export interface EditTodo { 36 | editTodo: EditTodo_editTodo; 37 | } 38 | 39 | export interface EditTodoVariables { 40 | id: number; 41 | text: string; 42 | } 43 | -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/client/src/operations/mutations/addTodo.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { gql, useMutation } from "@apollo/client"; 3 | import * as AddTodoTypes from './__generated__/AddTodo'; 4 | import { GET_ALL_TODOS } from "../queries/getAllTodos"; 5 | import { GetAllTodos } from "../queries/__generated__/GetAllTodos"; 6 | 7 | export const ADD_TODO = gql` 8 | mutation AddTodo ($text: String!) { 9 | addTodo (text: $text) { 10 | success 11 | todo { 12 | id 13 | text 14 | completed 15 | } 16 | error { 17 | message 18 | } 19 | } 20 | } 21 | ` 22 | 23 | export function useAddTodo () { 24 | const [mutate, { data, error }] = useMutation< 25 | AddTodoTypes.AddTodo, 26 | AddTodoTypes.AddTodoVariables 27 | >( 28 | ADD_TODO, 29 | { 30 | update (cache, { data }) { 31 | const newTodoFromResponse = data?.addTodo.todo; 32 | const existingTodos = cache.readQuery({ 33 | query: GET_ALL_TODOS, 34 | }); 35 | 36 | if (existingTodos && newTodoFromResponse) { 37 | cache.writeQuery({ 38 | query: GET_ALL_TODOS, 39 | data: { 40 | todos: [ 41 | ...existingTodos?.todos, 42 | newTodoFromResponse, 43 | ], 44 | }, 45 | }); 46 | } 47 | } 48 | } 49 | ) 50 | return { mutate, data, error }; 51 | } -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/client/src/operations/mutations/clearCompletedTodos.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { gql, useMutation } from "@apollo/client"; 3 | import * as ClearCompletedTodosTypes from './__generated__/ClearCompletedTodos' 4 | import * as GetAllTodosTypes from '../queries/__generated__/GetAllTodos' 5 | import { GET_ALL_TODOS } from "../queries/getAllTodos"; 6 | 7 | export const CLEAR_COMPLETED_TODOS = gql` 8 | mutation ClearCompletedTodos { 9 | clearCompletedTodos { 10 | success 11 | todos { 12 | id 13 | text 14 | completed 15 | } 16 | } 17 | } 18 | ` 19 | 20 | export function useClearCompletedTodos () { 21 | const [mutate, { data, error }] = useMutation< 22 | ClearCompletedTodosTypes.ClearCompletedTodos 23 | >( 24 | CLEAR_COMPLETED_TODOS, 25 | { 26 | refetchQueries: [{ 27 | query: GET_ALL_TODOS 28 | }], 29 | update (cache) { 30 | const result = cache.readQuery({ 31 | query: GET_ALL_TODOS 32 | }); 33 | const todosToDelete = result?.todos; 34 | 35 | todosToDelete?.forEach((todo) => { 36 | cache.evict({ id: `Todo:${todo?.id}` }) 37 | }) 38 | } 39 | } 40 | ) 41 | 42 | return { mutate, data, error }; 43 | } -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/client/src/operations/mutations/completeAllTodos.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { gql, useMutation } from "@apollo/client"; 3 | import * as CompleteAllTodosTypes from './__generated__/CompleteAllTodos' 4 | 5 | export const COMPLETE_ALL_TODOS = gql` 6 | mutation CompleteAllTodos { 7 | completeAllTodos { 8 | success 9 | todos { 10 | id 11 | text 12 | completed 13 | } 14 | } 15 | } 16 | ` 17 | 18 | export function useCompleteAllTodos () { 19 | const [mutate, { data, error }] = useMutation< 20 | CompleteAllTodosTypes.CompleteAllTodos 21 | >( 22 | COMPLETE_ALL_TODOS 23 | ) 24 | 25 | return { mutate, data, error }; 26 | } -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/client/src/operations/mutations/completeTodo.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { gql, useMutation } from "@apollo/client"; 3 | import * as CompleteTodoTypes from './__generated__/CompleteTodo' 4 | import { GET_ALL_TODOS } from "../queries/getAllTodos"; 5 | 6 | export const COMPLETE_TODO = gql` 7 | mutation CompleteTodo ($id: Int!) { 8 | completeTodo (id: $id) { 9 | success 10 | todo { 11 | id 12 | text 13 | completed 14 | } 15 | error { 16 | ... on TodoNotFoundError { 17 | message 18 | } 19 | ... on TodoAlreadyCompletedError { 20 | message 21 | } 22 | } 23 | } 24 | } 25 | ` 26 | 27 | export function useCompleteTodo () { 28 | const [mutate, { data, error }] = useMutation< 29 | CompleteTodoTypes.CompleteTodo, 30 | CompleteTodoTypes.CompleteTodoVariables 31 | >( 32 | COMPLETE_TODO, 33 | { 34 | refetchQueries: [{ 35 | query: GET_ALL_TODOS 36 | }] 37 | } 38 | ) 39 | 40 | return { mutate, data, error }; 41 | } -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/client/src/operations/mutations/deleteTodo.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { gql, useMutation } from "@apollo/client"; 3 | import * as DeleteTodoTypes from './__generated__/DeleteTodo' 4 | import { GET_ALL_TODOS } from "../queries/getAllTodos"; 5 | import { GetAllTodos } from "../queries/__generated__/GetAllTodos"; 6 | 7 | export const DELETE_TODO = gql` 8 | mutation DeleteTodo ($id: Int!) { 9 | deleteTodo (id: $id) { 10 | success 11 | todo { 12 | id 13 | text 14 | completed 15 | } 16 | error { 17 | ... on TodoNotFoundError { 18 | message 19 | } 20 | } 21 | } 22 | } 23 | ` 24 | 25 | export function useDeleteTodo () { 26 | const [mutate, { data, error }] = useMutation< 27 | DeleteTodoTypes.DeleteTodo, 28 | DeleteTodoTypes.DeleteTodoVariables 29 | >( 30 | DELETE_TODO, 31 | { 32 | update (cache, el) { 33 | const deletedId = el.data?.deleteTodo.todo?.id 34 | const allTodos = cache.readQuery({ query: GET_ALL_TODOS }); 35 | 36 | cache.writeQuery({ 37 | query: GET_ALL_TODOS, 38 | data: { 39 | todos: allTodos?.todos.filter((t) => t?.id !== deletedId) 40 | } 41 | }); 42 | 43 | cache.evict({ id: `Todo:${deletedId}`}) 44 | } 45 | } 46 | ) 47 | 48 | return { mutate, data, error }; 49 | } -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/client/src/operations/mutations/editTodo.tsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import { gql, useMutation } from "@apollo/client"; 4 | import * as EditTodoTypes from './__generated__/EditTodo' 5 | import { GET_ALL_TODOS } from "../queries/getAllTodos"; 6 | import { GetAllTodos } from "../queries/__generated__/GetAllTodos"; 7 | 8 | export const EDIT_TODO = gql` 9 | mutation EditTodo ($id: Int!, $text: String!) { 10 | editTodo (id: $id, text: $text) { 11 | success 12 | todo { 13 | id 14 | text 15 | completed 16 | } 17 | error { 18 | ... on TodoNotFoundError { 19 | message 20 | } 21 | ... on TodoValidationError { 22 | message 23 | } 24 | } 25 | } 26 | } 27 | ` 28 | 29 | export function useEditTodo () { 30 | const [mutate, { data, error }] = useMutation< 31 | EditTodoTypes.EditTodo, 32 | EditTodoTypes.EditTodoVariables 33 | >( 34 | EDIT_TODO, 35 | { 36 | update: (cache) => { 37 | let allTodos = cache.readQuery({ 38 | query: GET_ALL_TODOS 39 | }); 40 | 41 | if (allTodos) { 42 | cache.writeQuery({ 43 | query: GET_ALL_TODOS, 44 | data: { 45 | todos: allTodos?.todos.map( 46 | (t) => t?.id === data?.editTodo.todo?.id ? { 47 | ...t, 48 | node: { 49 | ...t, 50 | text: data?.editTodo.todo?.text 51 | } 52 | } : t) 53 | }, 54 | }); 55 | } 56 | } 57 | } 58 | ) 59 | 60 | return { mutate, data, error }; 61 | } -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/client/src/operations/mutations/setVisibilityFilter/index.tsx: -------------------------------------------------------------------------------- 1 | 2 | import createSetVisibilityFilter from './setVisibilityFilter' 3 | import { visibilityFilterVar } from '../../../cache' 4 | 5 | export const setVisibilityFilter = createSetVisibilityFilter(visibilityFilterVar) 6 | -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/client/src/operations/mutations/setVisibilityFilter/setVisibilityFilter.spec.tsx: -------------------------------------------------------------------------------- 1 | 2 | import createSetVisibilityFilter from './setVisibilityFilter' 3 | import { mockVisibilityFilter } from '../../../cache.spec' 4 | import { VisibilityFilters } from '../../../models/VisibilityFilter'; 5 | 6 | const setVisibilityFilter = createSetVisibilityFilter(mockVisibilityFilter); 7 | 8 | describe('setVisibilityFilter hook', () => { 9 | beforeEach(() => mockVisibilityFilter(VisibilityFilters.SHOW_ALL)); 10 | 11 | it('should change the visibility filter', () => { 12 | setVisibilityFilter(VisibilityFilters.SHOW_ALL); 13 | 14 | expect( 15 | mockVisibilityFilter().displayName 16 | ).toEqual("All") 17 | 18 | setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED); 19 | 20 | expect( 21 | mockVisibilityFilter().displayName 22 | ).toEqual("Completed") 23 | }); 24 | }) 25 | 26 | 27 | -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/client/src/operations/mutations/setVisibilityFilter/setVisibilityFilter.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { VisibilityFilter } from "../../../models/VisibilityFilter"; 3 | import { ReactiveVar } from "@apollo/client"; 4 | 5 | export default (visibilityFilterVar: ReactiveVar) => { 6 | return (filter: VisibilityFilter) => { 7 | visibilityFilterVar(filter); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/client/src/operations/queries/__generated__/GetAllTodos.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | // ==================================================== 6 | // GraphQL query operation: GetAllTodos 7 | // ==================================================== 8 | 9 | export interface GetAllTodos_todos { 10 | __typename: "Todo"; 11 | id: number; 12 | text: string; 13 | completed: boolean; 14 | } 15 | 16 | export interface GetAllTodos { 17 | todos: (GetAllTodos_todos | null)[]; 18 | } 19 | -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/client/src/operations/queries/getAllTodos.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { gql } from '@apollo/client' 3 | 4 | export const GET_ALL_TODOS = gql` 5 | query GetAllTodos { 6 | todos { 7 | id 8 | text 9 | completed 10 | } 11 | } 12 | ` -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/client/src/operations/queries/getVisibilityFilter.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { gql } from "@apollo/client"; 3 | 4 | export const GET_VISIBILITY_FILTER = gql` 5 | query GetVisibilityFilter { 6 | visibilityFilter @client { 7 | id 8 | displayName 9 | } 10 | } 11 | ` -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/client/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react" 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1 3 | } 4 | -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/server/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/server/apollo.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | service: { 3 | name: "ac3-todos-backend" 4 | } 5 | } -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/server/codegen.yml: -------------------------------------------------------------------------------- 1 | overwrite: true 2 | schema: "http://localhost:4000" 3 | generates: 4 | ./src/generated/graphql.ts: 5 | plugins: 6 | - "typescript" 7 | - "typescript-resolvers" 8 | ./graphql.schema.json: 9 | plugins: 10 | - "introspection" 11 | -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/server/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src", "bin"], 3 | "ext": ".ts,.js", 4 | "ignore": [], 5 | "exec": "ts-node --transpile-only ./src/index.ts" 6 | } -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "nodemon", 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "codegen": "graphql-codegen --config ./codegen.yml" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "apollo-server": "^2.11.0", 16 | "graphql": "^14.6.0" 17 | }, 18 | "devDependencies": { 19 | "@graphql-codegen/cli": "^1.13.1", 20 | "@graphql-codegen/introspection": "1.13.0", 21 | "@graphql-codegen/typescript": "1.13.0", 22 | "@graphql-codegen/typescript-resolvers": "1.13.0", 23 | "nodemon": "^2.0.2", 24 | "ts-node": "^8.6.2", 25 | "typescript": "^3.8.3" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/server/src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import { ApolloServer } from 'apollo-server' 3 | import { typeDefs } from './schema' 4 | import { resolvers } from './resolvers' 5 | import { todosRepo } from './modules/todos/repos'; 6 | import { TodoRepo } from './modules/todos/repos/todoRepo'; 7 | 8 | export type Context = { todosRepo: TodoRepo } 9 | 10 | const server = new ApolloServer({ 11 | context: () => ({ todosRepo } as Context), 12 | typeDefs, 13 | resolvers 14 | }); 15 | 16 | server.listen().then(({ url }) => { 17 | console.log(`🚀 Server ready at ${url}`); 18 | }); -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/server/src/modules/todos/repos/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import { InMemoryTodoRepo } from "./implementations/InMemoryTodoRepo"; 3 | 4 | const todosRepo = new InMemoryTodoRepo(); 5 | 6 | export { todosRepo }; -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/server/src/modules/todos/repos/todoRepo.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Todo } from "../../../generated/graphql"; 3 | 4 | export interface TodoRepo { 5 | addTodo(text: string): Promise; 6 | completeTodo (id: number): Promise; 7 | clearCompletedTodos(): Promise; 8 | completeAllTodos (): Promise; 9 | deleteTodo(id: number): Promise; 10 | editTodo(id: number, text: string): Promise; 11 | getAllTodos (): Promise; 12 | getTodoById (id: number): Promise; 13 | getLastTodo (): Promise; 14 | } 15 | -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/server/src/schema.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "apollo-server"; 2 | 3 | const typeDefs = gql` 4 | type PageInfo { 5 | hasPreviousPage: Boolean! 6 | hasNextPage: Boolean! 7 | startCursor: String! 8 | endCursor: String! 9 | } 10 | 11 | type Todo { 12 | id: Int! 13 | text: String! 14 | completed: Boolean! 15 | } 16 | 17 | type TodoNotFoundError { 18 | message: String! 19 | } 20 | 21 | union TodoResult = Todo | TodoNotFoundError 22 | 23 | type Query { 24 | todos ( 25 | after: String, 26 | before: String, 27 | first: Int, 28 | last: Int 29 | ): [Todo]! 30 | 31 | todo (id: Int!): TodoResult! 32 | } 33 | 34 | type TodoAlreadyCompletedError { 35 | message: String! 36 | } 37 | 38 | union CompleteTodoError = TodoNotFoundError | TodoAlreadyCompletedError 39 | 40 | type CompleteTodoResult { 41 | success: Boolean! 42 | todo: Todo 43 | error: CompleteTodoError 44 | } 45 | 46 | type TodoValidationError { 47 | message: String! 48 | } 49 | 50 | type AddTodoResult { 51 | success: Boolean! 52 | todo: Todo 53 | error: TodoValidationError 54 | } 55 | 56 | type ClearCompletedTodosResult { 57 | success: Boolean! 58 | todos: [Todo]! 59 | } 60 | 61 | type CompleteAllTodosResult { 62 | success: Boolean! 63 | todos: [Todo]! 64 | } 65 | 66 | type DeleteTodoResult { 67 | success: Boolean! 68 | todo: Todo 69 | error: TodoNotFoundError 70 | } 71 | 72 | union EditTodoError = TodoNotFoundError | TodoValidationError 73 | 74 | type EditTodoResult { 75 | success: Boolean! 76 | todo: Todo 77 | error: EditTodoError 78 | } 79 | 80 | type Mutation { 81 | addTodo (text: String!): AddTodoResult! 82 | clearCompletedTodos: ClearCompletedTodosResult! 83 | completeTodo (id: Int!): CompleteTodoResult! 84 | completeAllTodos: CompleteAllTodosResult! 85 | deleteTodo (id: Int!): DeleteTodoResult! 86 | editTodo (id: Int!, text: String!): EditTodoResult! 87 | } 88 | ` 89 | 90 | export { typeDefs } -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/server/src/shared/mappers/todoMapper.ts: -------------------------------------------------------------------------------- 1 | import { Todo, TodosConnection, TodosEdge } from "../../generated/graphql"; 2 | 3 | export class TodoMapper { 4 | 5 | public static toEdge (todo: Todo): TodosEdge { 6 | return { 7 | node: todo, 8 | cursor: "" 9 | }; 10 | } 11 | 12 | public static toTodosConnection (todos: Todo[]) : TodosConnection { 13 | return { 14 | edges: todos.map(t => this.toEdge(t)), 15 | pageInfo: { 16 | hasPreviousPage: false, 17 | hasNextPage: false, 18 | startCursor: "", 19 | endCursor: "" 20 | } 21 | }; 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/server/src/shared/utils/numberUtils.ts: -------------------------------------------------------------------------------- 1 | 2 | export class NumberUtils { 3 | public static isANumber (num: any): boolean { 4 | return !isNaN(num); 5 | } 6 | 7 | public static isANonNegativeNumber (num?: any): boolean { 8 | return !isNaN(num) && num >= 0; 9 | } 10 | } -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/server/src/shared/utils/paginationUtils.ts: -------------------------------------------------------------------------------- 1 | import { Result } from "../core/result"; 2 | import { NumberUtils } from "./numberUtils"; 3 | 4 | interface Node { 5 | id: number 6 | } 7 | 8 | type GenericNodeList = Node[]; 9 | 10 | export class PaginationUtils { 11 | public static filterByBeforeAndAfter ( 12 | items: GenericNodeList, 13 | after?: string, 14 | before?: string, 15 | ) : GenericNodeList { 16 | const isAfterSet = !!after === true; 17 | const isBeforeSet = !!before === true; 18 | 19 | if (isAfterSet) { 20 | const afterIndex = items.findIndex((t) => t.id === Number(after)) 21 | const afterIndexFound = afterIndex !== -1; 22 | 23 | if (afterIndexFound) { 24 | items = items.slice(afterIndex + 1) 25 | } 26 | } 27 | 28 | if (isBeforeSet) { 29 | const beforeIndex = items.findIndex((t) => t.id === Number(after)) 30 | const beforeIndexFond = beforeIndex !== -1; 31 | 32 | if (beforeIndexFond) { 33 | items = items.slice(0, beforeIndex) 34 | } 35 | } 36 | 37 | return items; 38 | } 39 | 40 | public static limitByFirstAndLast( 41 | items: T[], 42 | first?: number, 43 | last?: number 44 | ): Result { 45 | const isFirstSet = NumberUtils.isANumber(first); 46 | const isLastSet = NumberUtils.isANumber(last); 47 | 48 | if (isFirstSet) { 49 | const isFirstAPositiveNumber = NumberUtils.isANonNegativeNumber(first); 50 | 51 | if (!isFirstAPositiveNumber) { 52 | return Result.fail("First has to be greater than 0"); 53 | } 54 | 55 | if (items.length > first) { 56 | return Result.ok(items.slice(0, first)); 57 | } 58 | } 59 | 60 | if (isLastSet) { 61 | const isLastAPositiveNumber = NumberUtils.isANonNegativeNumber(last); 62 | 63 | if (!isLastAPositiveNumber) { 64 | return Result.fail("Last has to be greater than 0"); 65 | } 66 | 67 | if (items.length > last) { 68 | return Result.ok(items.slice(0, last)); 69 | } 70 | } 71 | 72 | return Result.ok(items); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /apollo-remote-state-no-relay/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "pretty": true, 7 | "sourceMap": true, 8 | "target": "es2017", 9 | "outDir": "./dist", 10 | "lib": ["es6"], 11 | "resolveJsonModule": true, 12 | "types": ["node"], 13 | "typeRoots" : ["./node_modules/@types", "./src/@types"], 14 | "experimentalDecorators": true, 15 | "emitDecoratorMetadata": true, 16 | "esModuleInterop": true 17 | }, 18 | "include": [ 19 | "src/**/*.ts", 20 | ], 21 | "exclude": [ 22 | "node_modules", 23 | ] 24 | } -------------------------------------------------------------------------------- /apollo-remote-state/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/ac3-state-management-examples/57ddd7aff0b86e12ea7f63db625c004add712ba4/apollo-remote-state/.DS_Store -------------------------------------------------------------------------------- /apollo-remote-state/README.md: -------------------------------------------------------------------------------- 1 | # apollo-remote-state 2 | 3 | Summary: Hooking Apollo Client up to a remote GraphQL API, the client-side cache is smart enough to automatically update the cache after most mutations successfully complete. For mutations that perform interactions against arrays (_cached collections_) or have additional client-side side-effects, we can help the cache decide what to do next by writing our update logic in the `useMutation`'s `update` function. 4 | 5 | ## Getting started 6 | 7 | There is a `client/` and `server/` portion to this example. 8 | 9 | ### Running the client 10 | 11 | To start the project, run the following commands: 12 | 13 | ``` 14 | cd client 15 | npm install && npm run start 16 | ``` 17 | 18 | The app should start at http://localhost:3000/. 19 | 20 | ### Running the server 21 | 22 | To start the project, run the following commands: 23 | 24 | ``` 25 | cd server 26 | npm install && npm run start 27 | ``` 28 | 29 | The server should start at http://localhost:4000/. 30 | -------------------------------------------------------------------------------- /apollo-remote-state/client/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # production 7 | build 8 | 9 | # misc 10 | .DS_Store 11 | npm-debug.log 12 | .env -------------------------------------------------------------------------------- /apollo-remote-state/client/__generated__/globalTypes.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | //============================================================== 7 | // START Enums and Input Objects 8 | //============================================================== 9 | 10 | //============================================================== 11 | // END Enums and Input Objects 12 | //============================================================== 13 | -------------------------------------------------------------------------------- /apollo-remote-state/client/apollo.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | client: { 3 | service: { 4 | name: 'ac3-todos-backend', 5 | url: `http://localhost:4000`, 6 | }, 7 | }, 8 | }; -------------------------------------------------------------------------------- /apollo-remote-state/client/local-schema.graphql: -------------------------------------------------------------------------------- 1 | 2 | type VisibilityFilter { 3 | id: String! 4 | displayName: String! 5 | } 6 | 7 | extend type Query { 8 | visibilityFilter: VisibilityFilter! 9 | } -------------------------------------------------------------------------------- /apollo-remote-state/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todomvc-apollo", 3 | "version": "0.0.1", 4 | "private": true, 5 | "devDependencies": { 6 | "@types/classnames": "^2.2.9", 7 | "@types/react-test-renderer": "^16.9.2", 8 | "react-scripts": "^3.0.0", 9 | "react-test-renderer": "^16.8.6" 10 | }, 11 | "dependencies": { 12 | "@apollo/client": "^3.0.1", 13 | "@types/graphql": "^14.5.0", 14 | "@types/jest": "^25.1.3", 15 | "@types/node": "^13.7.6", 16 | "@types/react": "^16.9.23", 17 | "@types/react-dom": "^16.9.5", 18 | "apollo": "^2.32.5", 19 | "classnames": "^2.2.6", 20 | "graphql": "^14.6.0", 21 | "prop-types": "^15.7.2", 22 | "react": "^16.8.6", 23 | "react-dom": "^16.8.6", 24 | "reselect": "^4.0.0", 25 | "todomvc-app-css": "^2.2.0", 26 | "typescript": "^3.8.2" 27 | }, 28 | "scripts": { 29 | "start": "react-scripts start", 30 | "build": "react-scripts build", 31 | "eject": "react-scripts eject", 32 | "test": "react-scripts test --env=node", 33 | "download-schema": "apollo service:download --endpoint=http://localhost:4000/ graphql-schema.json", 34 | "generate": "npm run download-schema && apollo codegen:generate --localSchemaFile=graphql-schema.json,client-schema.graphql --target=typescript --tagName=gql" 35 | }, 36 | "browserslist": [ 37 | ">0.2%", 38 | "not dead", 39 | "not ie <= 11", 40 | "not op_mini all" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /apollo-remote-state/client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Redux TodoMVC Example 7 | 8 | 9 |
    10 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /apollo-remote-state/client/src/cache.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { InMemoryCache, makeVar } from "@apollo/client"; 3 | import { VisibilityFilters, VisibilityFilter } from "./models/VisibilityFilter"; 4 | 5 | export const cache: InMemoryCache = new InMemoryCache({ 6 | typePolicies: { 7 | Query: { 8 | fields: { 9 | visibilityFilter: { 10 | read () { 11 | return visibilityFilterVar(); 12 | } 13 | } 14 | } 15 | } 16 | } 17 | }); 18 | 19 | /** 20 | * Set initial values when we create cache variables. 21 | */ 22 | 23 | export const visibilityFilterVar = makeVar( 24 | VisibilityFilters.SHOW_ALL 25 | ) 26 | -------------------------------------------------------------------------------- /apollo-remote-state/client/src/components/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Header from '../containers/Header' 3 | import MainSection from '../containers/MainSection' 4 | 5 | const App = () => ( 6 |
    7 |
    8 | 9 |
    10 | ) 11 | 12 | export default App 13 | -------------------------------------------------------------------------------- /apollo-remote-state/client/src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import Link from './Link' 4 | import { VisibilityFilters, VisibilityFilter } from '../models/VisibilityFilter'; 5 | 6 | interface FooterProps { 7 | activeVisibilityFilter: VisibilityFilter; 8 | activeCount: number; 9 | completedCount: number; 10 | onClearCompleted: () => void; 11 | setVisibilityFilter: (filter: VisibilityFilter) => void; 12 | } 13 | 14 | const Footer = (props: FooterProps) => { 15 | const { activeCount, completedCount, onClearCompleted, activeVisibilityFilter, setVisibilityFilter } = props; 16 | const itemWord = activeCount === 1 ? "item" : "items"; 17 | return ( 18 |
    19 | 20 | {activeCount || "No"} {itemWord} left 21 | 22 |
      23 | {Object.keys(VisibilityFilters).map((key) => VisibilityFilters[key]).map((filter) => ( 24 |
    • 25 | setVisibilityFilter(filter)}> 28 | {filter.displayName} 29 |
    • 30 | ))} 31 |
    32 | {!!completedCount && ( 33 | 36 | )} 37 |
    38 | ); 39 | }; 40 | 41 | Footer.propTypes = { 42 | completedCount: PropTypes.number.isRequired, 43 | activeCount: PropTypes.number.isRequired, 44 | onClearCompleted: PropTypes.func.isRequired, 45 | setVisibilityFilter: PropTypes.func.isRequired 46 | } 47 | 48 | export default Footer 49 | -------------------------------------------------------------------------------- /apollo-remote-state/client/src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import TodoTextInput from './TodoTextInput' 4 | 5 | interface HeaderProps { 6 | addTodo: (text: string) => void; 7 | } 8 | 9 | const Header = ({ addTodo }: HeaderProps) => ( 10 |
    11 |

    todos

    12 | { 15 | if (text.length !== 0) { 16 | addTodo(text); 17 | } 18 | }} 19 | placeholder="What needs to be done?" 20 | /> 21 |
    22 | ); 23 | 24 | Header.propTypes = { 25 | addTodo: PropTypes.func.isRequired 26 | } 27 | 28 | export default Header 29 | -------------------------------------------------------------------------------- /apollo-remote-state/client/src/components/Link.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import classnames from 'classnames' 4 | 5 | interface LinkProps { 6 | setFilter: () => any; 7 | active: boolean; 8 | children: any; 9 | } 10 | 11 | const Link = ({ active, children, setFilter }: LinkProps) => 12 | ( 13 | // eslint-disable-next-line jsx-a11y/anchor-is-valid 14 | setFilter()} 18 | > 19 | {children} 20 | 21 | ) 22 | 23 | 24 | Link.propTypes = { 25 | active: PropTypes.bool.isRequired, 26 | children: PropTypes.node.isRequired, 27 | setFilter: PropTypes.func.isRequired 28 | } 29 | 30 | export default Link 31 | -------------------------------------------------------------------------------- /apollo-remote-state/client/src/components/MainSection.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import Footer from './Footer' 4 | import VisibleTodoList from '../containers/VisibleTodoList' 5 | import { VisibilityFilter } from '../models/VisibilityFilter'; 6 | 7 | interface MainSectionProps { 8 | activeVisibilityFilter: VisibilityFilter; 9 | todosCount: number; 10 | completedCount: number; 11 | actions: any; 12 | } 13 | 14 | /** 15 | * This is a view component. It doesn't define anything that 16 | * is responsible for querying or mutating, it just relies 17 | * on it from the upper layer component (namely, actions) 18 | */ 19 | 20 | const MainSection = ({ 21 | activeVisibilityFilter, 22 | todosCount, 23 | completedCount, 24 | actions, 25 | }: MainSectionProps) => ( 26 |
    27 | {!!todosCount && ( 28 | 29 | 35 | 37 | )} 38 | 39 | {!!todosCount && ( 40 |
    47 | )} 48 |
    49 | ); 50 | 51 | MainSection.propTypes = { 52 | todosCount: PropTypes.number.isRequired, 53 | completedCount: PropTypes.number.isRequired, 54 | actions: PropTypes.object.isRequired 55 | } 56 | 57 | export default MainSection; 58 | -------------------------------------------------------------------------------- /apollo-remote-state/client/src/components/TodoItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import classnames from 'classnames' 4 | import TodoTextInput from './TodoTextInput' 5 | 6 | type Props = any; 7 | 8 | export default class TodoItem extends Component { 9 | static propTypes = { 10 | todo: PropTypes.object.isRequired, 11 | editTodo: PropTypes.func.isRequired, 12 | deleteTodo: PropTypes.func.isRequired, 13 | completeTodo: PropTypes.func.isRequired 14 | } 15 | 16 | state = { 17 | editing: false 18 | } 19 | 20 | handleDoubleClick = () => { 21 | this.setState({ editing: true }) 22 | } 23 | 24 | handleSave = (id: number, text: string) => { 25 | if (text.length === 0) { 26 | this.props.deleteTodo(id) 27 | } else { 28 | this.props.editTodo(id, text) 29 | } 30 | this.setState({ editing: false }) 31 | } 32 | 33 | render() { 34 | const { todo, completeTodo, deleteTodo } = this.props 35 | 36 | let element 37 | if (this.state.editing) { 38 | element = ( 39 | this.handleSave(todo.id, text)} /> 42 | ) 43 | } else { 44 | element = ( 45 |
    46 | { 50 | completeTodo(todo.id) 51 | }} /> 52 | 55 |
    58 | ) 59 | } 60 | 61 | return ( 62 |
  • 66 | {element} 67 |
  • 68 | ) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /apollo-remote-state/client/src/components/TodoList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import TodoItem from './TodoItem' 4 | import { Todo } from '../models/Todos' 5 | 6 | const TodoList = ({ filteredTodos, actions }: any) => ( 7 |
      8 | {filteredTodos.map((todo: Todo) => 9 | 10 | )} 11 |
    12 | ) 13 | 14 | TodoList.propTypes = { 15 | filteredTodos: PropTypes.arrayOf(PropTypes.shape({ 16 | id: PropTypes.number.isRequired, 17 | completed: PropTypes.bool.isRequired, 18 | text: PropTypes.string.isRequired 19 | }).isRequired).isRequired, 20 | actions: PropTypes.object.isRequired 21 | } 22 | 23 | export default TodoList 24 | -------------------------------------------------------------------------------- /apollo-remote-state/client/src/components/TodoTextInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import classnames from 'classnames' 4 | 5 | export default class TodoTextInput extends Component { 6 | static propTypes = { 7 | onSave: PropTypes.func.isRequired, 8 | text: PropTypes.string, 9 | placeholder: PropTypes.string, 10 | editing: PropTypes.bool, 11 | newTodo: PropTypes.bool 12 | } 13 | 14 | state = { 15 | text: this.props.text || '' 16 | } 17 | 18 | handleSubmit = (e: any) => { 19 | const text = e.target.value.trim() 20 | if (e.which === 13) { 21 | this.props.onSave(text) 22 | if (this.props.newTodo) { 23 | this.setState({ text: '' }) 24 | } 25 | } 26 | } 27 | 28 | handleChange = (e: any) => { 29 | this.setState({ text: e.target.value }) 30 | } 31 | 32 | handleBlur = (e: any) => { 33 | if (!this.props.newTodo) { 34 | this.props.onSave(e.target.value) 35 | } 36 | } 37 | 38 | render() { 39 | return ( 40 | 52 | ) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /apollo-remote-state/client/src/containers/Header.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import Header from '../components/Header' 4 | import { useAddTodo } from '../operations/mutations/addTodo'; 5 | 6 | export default function () { 7 | const { mutate } = useAddTodo(); 8 | return
    mutate({ variables: { text } })} />; 9 | } -------------------------------------------------------------------------------- /apollo-remote-state/client/src/containers/MainSection.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import MainSection from '../components/MainSection' 4 | import { useQuery, gql } from '@apollo/client' 5 | import { VisibilityFilter } from '../models/VisibilityFilter' 6 | import { GET_ALL_TODOS } from '../operations/queries/getAllTodos' 7 | import { GET_VISIBILITY_FILTER } from '../operations/queries/getVisibilityFilter' 8 | import { GetAllTodos } from '../operations/__generated__/GetAllTodos' 9 | import { useClearCompletedTodos } from '../operations/mutations/clearCompletedTodos' 10 | import { useCompleteAllTodos } from '../operations/mutations/completeAllTodos' 11 | import setVisibilityFilter from '../operations/mutations/setVisibilityFilter/setVisibilityFilter' 12 | import { GetVisibilityFilter } from '../operations/queries/__generated__/GetVisibilityFilter' 13 | 14 | export default function Main () { 15 | const { loading: isTodosLoading, data: todosConnection, error: todosError } = useQuery(GET_ALL_TODOS); 16 | const { data: visibilityFilter } = useQuery(GET_VISIBILITY_FILTER); 17 | 18 | const { mutate: clearCompletedTodos } = useClearCompletedTodos(); 19 | const { mutate: completeAllTodos } = useCompleteAllTodos(); 20 | 21 | if (isTodosLoading) return
    Loading...
    22 | if (todosError) return
    An error occurred {JSON.stringify(todosError)}
    23 | if (!todosConnection) return
    None
    ; 24 | const todos = todosConnection.todos.edges.map((t) => t?.node) 25 | 26 | return ( 27 | t ? t.completed : false).length} 31 | actions={{ 32 | completeAllTodos, 33 | setVisibilityFilter, 34 | clearCompletedTodos, 35 | }} 36 | /> 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /apollo-remote-state/client/src/containers/__generated__/GetLastTodos.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | // ==================================================== 7 | // GraphQL query operation: GetLastTodos 8 | // ==================================================== 9 | 10 | export interface GetLastTodos_todos_edges_node { 11 | __typename: "Todo"; 12 | id: number; 13 | text: string; 14 | completed: boolean; 15 | } 16 | 17 | export interface GetLastTodos_todos_edges { 18 | __typename: "TodosEdge"; 19 | node: GetLastTodos_todos_edges_node; 20 | } 21 | 22 | export interface GetLastTodos_todos { 23 | __typename: "TodosConnection"; 24 | edges: (GetLastTodos_todos_edges | null)[]; 25 | } 26 | 27 | export interface GetLastTodos { 28 | todos: GetLastTodos_todos; 29 | } 30 | -------------------------------------------------------------------------------- /apollo-remote-state/client/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import App from './components/App' 4 | import { ApolloClient, ApolloProvider } from '@apollo/client'; 5 | import 'todomvc-app-css/index.css' 6 | import { cache } from './cache'; 7 | 8 | export const client = new ApolloClient({ 9 | cache, 10 | uri: 'http://localhost:4000/', 11 | headers: { 12 | authorization: localStorage.getItem('token') || '', 13 | 'client-name': 'ac3-todos-backend', 14 | 'client-version': '1.0.0', 15 | }, 16 | connectToDevTools: true, 17 | }); 18 | 19 | render( 20 | 21 | 22 | , 23 | document.getElementById('root') 24 | ) 25 | -------------------------------------------------------------------------------- /apollo-remote-state/client/src/models/Todos.tsx: -------------------------------------------------------------------------------- 1 | import { GetAllTodos_todos_edges_node } from "../operations/__generated__/GetAllTodos"; 2 | 3 | export type Todo = GetAllTodos_todos_edges_node; 4 | 5 | export type Todos = Todo[]; -------------------------------------------------------------------------------- /apollo-remote-state/client/src/models/VisibilityFilter.tsx: -------------------------------------------------------------------------------- 1 | 2 | export type VisibilityFilter = { 3 | id: string; 4 | displayName: string; 5 | } 6 | 7 | export const VisibilityFilters: { [filter: string]: VisibilityFilter } = { 8 | SHOW_ALL: { 9 | id: "show_all", 10 | displayName: "All" 11 | }, 12 | SHOW_COMPLETED: { 13 | id: "show_completed", 14 | displayName: "Completed" 15 | }, 16 | SHOW_ACTIVE: { 17 | id: "show_active", 18 | displayName: "Active" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /apollo-remote-state/client/src/operations/__generated__/GetAllTodos.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | // ==================================================== 6 | // GraphQL query operation: GetAllTodos 7 | // ==================================================== 8 | 9 | export interface GetAllTodos_todos_edges_node { 10 | __typename: "Todo"; 11 | id: number; 12 | text: string; 13 | completed: boolean; 14 | } 15 | 16 | export interface GetAllTodos_todos_edges { 17 | __typename: "TodosEdge"; 18 | node: GetAllTodos_todos_edges_node; 19 | } 20 | 21 | export interface GetAllTodos_todos { 22 | __typename: "TodosConnection"; 23 | edges: (GetAllTodos_todos_edges | null)[]; 24 | } 25 | 26 | export interface GetAllTodos { 27 | todos: GetAllTodos_todos; 28 | } 29 | -------------------------------------------------------------------------------- /apollo-remote-state/client/src/operations/mutations/__generated__/AddTodo.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | // ==================================================== 7 | // GraphQL mutation operation: AddTodo 8 | // ==================================================== 9 | 10 | export interface AddTodo_addTodo_todo { 11 | __typename: "Todo"; 12 | id: number; 13 | text: string; 14 | completed: boolean; 15 | } 16 | 17 | export interface AddTodo_addTodo_error { 18 | __typename: "TodoValidationError"; 19 | message: string; 20 | } 21 | 22 | export interface AddTodo_addTodo { 23 | __typename: "AddTodoResult"; 24 | success: boolean; 25 | todo: AddTodo_addTodo_todo | null; 26 | error: AddTodo_addTodo_error | null; 27 | } 28 | 29 | export interface AddTodo { 30 | addTodo: AddTodo_addTodo; 31 | } 32 | 33 | export interface AddTodoVariables { 34 | text: string; 35 | } 36 | -------------------------------------------------------------------------------- /apollo-remote-state/client/src/operations/mutations/__generated__/ClearCompletedTodos.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | // ==================================================== 7 | // GraphQL mutation operation: ClearCompletedTodos 8 | // ==================================================== 9 | 10 | export interface ClearCompletedTodos_clearCompletedTodos_todos { 11 | __typename: "Todo"; 12 | id: number; 13 | text: string; 14 | completed: boolean; 15 | } 16 | 17 | export interface ClearCompletedTodos_clearCompletedTodos { 18 | __typename: "ClearCompletedTodosResult"; 19 | success: boolean; 20 | todos: (ClearCompletedTodos_clearCompletedTodos_todos | null)[]; 21 | } 22 | 23 | export interface ClearCompletedTodos { 24 | clearCompletedTodos: ClearCompletedTodos_clearCompletedTodos; 25 | } 26 | -------------------------------------------------------------------------------- /apollo-remote-state/client/src/operations/mutations/__generated__/CompleteAllTodos.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | // ==================================================== 7 | // GraphQL mutation operation: CompleteAllTodos 8 | // ==================================================== 9 | 10 | export interface CompleteAllTodos_completeAllTodos_todos { 11 | __typename: "Todo"; 12 | id: number; 13 | text: string; 14 | completed: boolean; 15 | } 16 | 17 | export interface CompleteAllTodos_completeAllTodos { 18 | __typename: "CompleteAllTodosResult"; 19 | success: boolean; 20 | todos: (CompleteAllTodos_completeAllTodos_todos | null)[]; 21 | } 22 | 23 | export interface CompleteAllTodos { 24 | completeAllTodos: CompleteAllTodos_completeAllTodos; 25 | } 26 | -------------------------------------------------------------------------------- /apollo-remote-state/client/src/operations/mutations/__generated__/CompleteTodo.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | // ==================================================== 7 | // GraphQL mutation operation: CompleteTodo 8 | // ==================================================== 9 | 10 | export interface CompleteTodo_completeTodo_todo { 11 | __typename: "Todo"; 12 | id: number; 13 | text: string; 14 | completed: boolean; 15 | } 16 | 17 | export interface CompleteTodo_completeTodo_error_TodoNotFoundError { 18 | __typename: "TodoNotFoundError"; 19 | message: string; 20 | } 21 | 22 | export interface CompleteTodo_completeTodo_error_TodoAlreadyCompletedError { 23 | __typename: "TodoAlreadyCompletedError"; 24 | message: string; 25 | } 26 | 27 | export type CompleteTodo_completeTodo_error = CompleteTodo_completeTodo_error_TodoNotFoundError | CompleteTodo_completeTodo_error_TodoAlreadyCompletedError; 28 | 29 | export interface CompleteTodo_completeTodo { 30 | __typename: "CompleteTodoResult"; 31 | success: boolean; 32 | todo: CompleteTodo_completeTodo_todo | null; 33 | error: CompleteTodo_completeTodo_error | null; 34 | } 35 | 36 | export interface CompleteTodo { 37 | completeTodo: CompleteTodo_completeTodo; 38 | } 39 | 40 | export interface CompleteTodoVariables { 41 | id: number; 42 | } 43 | -------------------------------------------------------------------------------- /apollo-remote-state/client/src/operations/mutations/__generated__/DeleteTodo.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | // ==================================================== 7 | // GraphQL mutation operation: DeleteTodo 8 | // ==================================================== 9 | 10 | export interface DeleteTodo_deleteTodo_todo { 11 | __typename: "Todo"; 12 | id: number; 13 | text: string; 14 | completed: boolean; 15 | } 16 | 17 | export interface DeleteTodo_deleteTodo_error { 18 | __typename: "TodoNotFoundError"; 19 | message: string; 20 | } 21 | 22 | export interface DeleteTodo_deleteTodo { 23 | __typename: "DeleteTodoResult"; 24 | success: boolean; 25 | todo: DeleteTodo_deleteTodo_todo | null; 26 | error: DeleteTodo_deleteTodo_error | null; 27 | } 28 | 29 | export interface DeleteTodo { 30 | deleteTodo: DeleteTodo_deleteTodo; 31 | } 32 | 33 | export interface DeleteTodoVariables { 34 | id: number; 35 | } 36 | -------------------------------------------------------------------------------- /apollo-remote-state/client/src/operations/mutations/__generated__/EditTodo.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | // ==================================================== 7 | // GraphQL mutation operation: EditTodo 8 | // ==================================================== 9 | 10 | export interface EditTodo_editTodo_todo { 11 | __typename: "Todo"; 12 | id: number; 13 | text: string; 14 | completed: boolean; 15 | } 16 | 17 | export interface EditTodo_editTodo_error_TodoNotFoundError { 18 | __typename: "TodoNotFoundError"; 19 | message: string; 20 | } 21 | 22 | export interface EditTodo_editTodo_error_TodoValidationError { 23 | __typename: "TodoValidationError"; 24 | message: string; 25 | } 26 | 27 | export type EditTodo_editTodo_error = EditTodo_editTodo_error_TodoNotFoundError | EditTodo_editTodo_error_TodoValidationError; 28 | 29 | export interface EditTodo_editTodo { 30 | __typename: "EditTodoResult"; 31 | success: boolean; 32 | todo: EditTodo_editTodo_todo | null; 33 | error: EditTodo_editTodo_error | null; 34 | } 35 | 36 | export interface EditTodo { 37 | editTodo: EditTodo_editTodo; 38 | } 39 | 40 | export interface EditTodoVariables { 41 | id: number; 42 | text: string; 43 | } 44 | -------------------------------------------------------------------------------- /apollo-remote-state/client/src/operations/mutations/addTodo.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { gql, useMutation } from "@apollo/client"; 3 | import * as AddTodoTypes from './__generated__/AddTodo'; 4 | import { GET_ALL_TODOS } from "../queries/getAllTodos"; 5 | import { GetAllTodos } from "../__generated__/GetAllTodos"; 6 | 7 | export const ADD_TODO = gql` 8 | mutation AddTodo ($text: String!) { 9 | addTodo (text: $text) { 10 | success 11 | todo { 12 | id 13 | text 14 | completed 15 | } 16 | error { 17 | message 18 | } 19 | } 20 | } 21 | ` 22 | 23 | export function useAddTodo () { 24 | const [mutate, { data, error }] = useMutation< 25 | AddTodoTypes.AddTodo, 26 | AddTodoTypes.AddTodoVariables 27 | >( 28 | ADD_TODO, 29 | { 30 | update (cache, { data }) { 31 | const newTodoFromResponse = data?.addTodo.todo; 32 | const existingTodos = cache.readQuery({ 33 | query: GET_ALL_TODOS, 34 | }); 35 | 36 | if (existingTodos && newTodoFromResponse) { 37 | cache.writeQuery({ 38 | query: GET_ALL_TODOS, 39 | data: { 40 | todos: { 41 | edges: [ 42 | ...existingTodos?.todos.edges, 43 | { __typename: 'TodosEdge', node: newTodoFromResponse }, 44 | ], 45 | }, 46 | }, 47 | }); 48 | } 49 | } 50 | } 51 | ) 52 | return { mutate, data, error }; 53 | } -------------------------------------------------------------------------------- /apollo-remote-state/client/src/operations/mutations/clearCompletedTodos.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { gql, useMutation } from "@apollo/client"; 3 | import * as ClearCompletedTodosTypes from './__generated__/ClearCompletedTodos' 4 | import * as GetAllTodosTypes from '../queries/__generated__/GetAllTodos' 5 | import { GET_ALL_TODOS } from "../queries/getAllTodos"; 6 | 7 | export const CLEAR_COMPLETED_TODOS = gql` 8 | mutation ClearCompletedTodos { 9 | clearCompletedTodos { 10 | success 11 | todos { 12 | id 13 | text 14 | completed 15 | } 16 | } 17 | } 18 | ` 19 | 20 | export function useClearCompletedTodos () { 21 | const [mutate, { data, error }] = useMutation< 22 | ClearCompletedTodosTypes.ClearCompletedTodos 23 | >( 24 | CLEAR_COMPLETED_TODOS, 25 | { 26 | update (cache) { 27 | const allTodos = cache.readQuery({ 28 | query: GET_ALL_TODOS 29 | }); 30 | 31 | cache.writeQuery({ 32 | query: GET_ALL_TODOS, 33 | data: { 34 | todos: { 35 | edges: allTodos?.todos.edges 36 | .filter((t) => !t?.node.completed) 37 | }, 38 | }, 39 | }); 40 | } 41 | } 42 | ) 43 | 44 | return { mutate, data, error }; 45 | } -------------------------------------------------------------------------------- /apollo-remote-state/client/src/operations/mutations/completeAllTodos.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { gql, useMutation } from "@apollo/client"; 3 | import * as CompleteAllTodosTypes from './__generated__/CompleteAllTodos' 4 | 5 | export const COMPLETE_ALL_TODOS = gql` 6 | mutation CompleteAllTodos { 7 | completeAllTodos { 8 | success 9 | todos { 10 | id 11 | text 12 | completed 13 | } 14 | } 15 | } 16 | ` 17 | 18 | export function useCompleteAllTodos () { 19 | const [mutate, { data, error }] = useMutation< 20 | CompleteAllTodosTypes.CompleteAllTodos 21 | >( 22 | COMPLETE_ALL_TODOS 23 | ) 24 | 25 | return { mutate, data, error }; 26 | } -------------------------------------------------------------------------------- /apollo-remote-state/client/src/operations/mutations/completeTodo.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { gql, useMutation } from "@apollo/client"; 3 | import * as CompleteTodoTypes from './__generated__/CompleteTodo' 4 | 5 | export const COMPLETE_TODO = gql` 6 | mutation CompleteTodo ($id: Int!) { 7 | completeTodo (id: $id) { 8 | success 9 | todo { 10 | id 11 | text 12 | completed 13 | } 14 | error { 15 | ... on TodoNotFoundError { 16 | message 17 | } 18 | ... on TodoAlreadyCompletedError { 19 | message 20 | } 21 | } 22 | } 23 | } 24 | ` 25 | 26 | export function useCompleteTodo () { 27 | const [mutate, { data, error }] = useMutation< 28 | CompleteTodoTypes.CompleteTodo, 29 | CompleteTodoTypes.CompleteTodoVariables 30 | >( 31 | COMPLETE_TODO 32 | ) 33 | 34 | return { mutate, data, error }; 35 | } -------------------------------------------------------------------------------- /apollo-remote-state/client/src/operations/mutations/deleteTodo.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { gql, useMutation } from "@apollo/client"; 3 | import * as DeleteTodoTypes from './__generated__/DeleteTodo' 4 | import { GET_ALL_TODOS } from "../queries/getAllTodos"; 5 | import { GetAllTodos } from "../__generated__/GetAllTodos"; 6 | 7 | export const DELETE_TODO = gql` 8 | mutation DeleteTodo ($id: Int!) { 9 | deleteTodo (id: $id) { 10 | success 11 | todo { 12 | id 13 | text 14 | completed 15 | } 16 | error { 17 | ... on TodoNotFoundError { 18 | message 19 | } 20 | } 21 | } 22 | } 23 | ` 24 | 25 | export function useDeleteTodo () { 26 | const [mutate, { data, error }] = useMutation< 27 | DeleteTodoTypes.DeleteTodo, 28 | DeleteTodoTypes.DeleteTodoVariables 29 | >( 30 | DELETE_TODO, 31 | { 32 | update (cache, { data }) { 33 | const deletedTodoId = data?.deleteTodo.todo?.id; 34 | const allTodos = cache.readQuery({ 35 | query: GET_ALL_TODOS 36 | }); 37 | 38 | cache.writeQuery({ 39 | query: GET_ALL_TODOS, 40 | data: { 41 | todos: { 42 | edges: allTodos?.todos.edges.filter((t) => t?.node.id !== deletedTodoId) 43 | }, 44 | }, 45 | }); 46 | } 47 | } 48 | ) 49 | 50 | return { mutate, data, error }; 51 | } -------------------------------------------------------------------------------- /apollo-remote-state/client/src/operations/mutations/editTodo.tsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import { gql, useMutation } from "@apollo/client"; 4 | import * as EditTodoTypes from './__generated__/EditTodo' 5 | import { GET_ALL_TODOS } from "../queries/getAllTodos"; 6 | import { GetAllTodos } from "../__generated__/GetAllTodos"; 7 | 8 | export const EDIT_TODO = gql` 9 | mutation EditTodo ($id: Int!, $text: String!) { 10 | editTodo (id: $id, text: $text) { 11 | success 12 | todo { 13 | id 14 | text 15 | completed 16 | } 17 | error { 18 | ... on TodoNotFoundError { 19 | message 20 | } 21 | ... on TodoValidationError { 22 | message 23 | } 24 | } 25 | } 26 | } 27 | ` 28 | 29 | export function useEditTodo () { 30 | const [mutate, { data, error }] = useMutation< 31 | EditTodoTypes.EditTodo, 32 | EditTodoTypes.EditTodoVariables 33 | >( 34 | EDIT_TODO, 35 | { 36 | update: (cache) => { 37 | let allTodos = cache.readQuery({ 38 | query: GET_ALL_TODOS 39 | }); 40 | 41 | if (allTodos) { 42 | cache.writeQuery({ 43 | query: GET_ALL_TODOS, 44 | data: { 45 | todos: { 46 | edges: allTodos?.todos.edges.map( 47 | (t) => t?.node.id === data?.editTodo.todo?.id ? { 48 | ...t, 49 | node: { 50 | ...t?.node, 51 | text: data?.editTodo.todo?.text 52 | } 53 | } : t) 54 | }, 55 | }, 56 | }); 57 | } 58 | } 59 | } 60 | ) 61 | 62 | return { mutate, data, error }; 63 | } -------------------------------------------------------------------------------- /apollo-remote-state/client/src/operations/mutations/setVisibilityFilter/index.tsx: -------------------------------------------------------------------------------- 1 | 2 | import createSetVisibilityFilter from './setVisibilityFilter' 3 | import { visibilityFilterVar } from '../../../cache' 4 | 5 | export const setVisibilityFilter = createSetVisibilityFilter(visibilityFilterVar) 6 | -------------------------------------------------------------------------------- /apollo-remote-state/client/src/operations/mutations/setVisibilityFilter/setVisibilityFilter.spec.tsx: -------------------------------------------------------------------------------- 1 | 2 | import createSetVisibilityFilter from './setVisibilityFilter' 3 | import { VisibilityFilters } from '../../../models/VisibilityFilter'; 4 | import { mockVisibilityFilter } from '../../../tests/mocks/mockVisibilityFilter'; 5 | 6 | const setVisibilityFilter = createSetVisibilityFilter(mockVisibilityFilter); 7 | 8 | describe('setVisibilityFilter hook', () => { 9 | beforeEach(() => mockVisibilityFilter(VisibilityFilters.SHOW_ALL)); 10 | 11 | it('should change the visibility filter', () => { 12 | setVisibilityFilter(VisibilityFilters.SHOW_ALL); 13 | 14 | expect( 15 | mockVisibilityFilter().displayName 16 | ).toEqual("All") 17 | 18 | setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED); 19 | 20 | expect( 21 | mockVisibilityFilter().displayName 22 | ).toEqual("Completed") 23 | }); 24 | }) 25 | 26 | 27 | -------------------------------------------------------------------------------- /apollo-remote-state/client/src/operations/mutations/setVisibilityFilter/setVisibilityFilter.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { VisibilityFilter } from "../../../models/VisibilityFilter"; 3 | import { ReactiveVar } from "@apollo/client"; 4 | 5 | export default (visibilityFilterVar: ReactiveVar) => { 6 | return (filter: VisibilityFilter) => { 7 | visibilityFilterVar(filter); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /apollo-remote-state/client/src/operations/queries/__generated__/GetAllTodos.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | // ==================================================== 7 | // GraphQL query operation: GetAllTodos 8 | // ==================================================== 9 | 10 | export interface GetAllTodos_todos_edges_node { 11 | __typename: "Todo"; 12 | id: number; 13 | text: string; 14 | completed: boolean; 15 | } 16 | 17 | export interface GetAllTodos_todos_edges { 18 | __typename: "TodosEdge"; 19 | node: GetAllTodos_todos_edges_node; 20 | } 21 | 22 | export interface GetAllTodos_todos { 23 | __typename: "TodosConnection"; 24 | edges: (GetAllTodos_todos_edges | null)[]; 25 | } 26 | 27 | export interface GetAllTodos { 28 | todos: GetAllTodos_todos; 29 | } 30 | -------------------------------------------------------------------------------- /apollo-remote-state/client/src/operations/queries/__generated__/GetVisibilityFilter.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | // ==================================================== 7 | // GraphQL query operation: GetVisibilityFilter 8 | // ==================================================== 9 | 10 | export interface GetVisibilityFilter_visibilityFilter { 11 | __typename: "VisibilityFilter"; 12 | id: string; 13 | displayName: string; 14 | } 15 | 16 | export interface GetVisibilityFilter { 17 | visibilityFilter: GetVisibilityFilter_visibilityFilter; 18 | } 19 | -------------------------------------------------------------------------------- /apollo-remote-state/client/src/operations/queries/getAllTodos.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { gql } from "@apollo/client"; 3 | 4 | export const GET_ALL_TODOS = gql` 5 | query GetAllTodos { 6 | todos { 7 | edges { 8 | node { 9 | id 10 | text 11 | completed 12 | } 13 | } 14 | } 15 | } 16 | ` -------------------------------------------------------------------------------- /apollo-remote-state/client/src/operations/queries/getVisibilityFilter.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { gql } from "@apollo/client"; 3 | 4 | export const GET_VISIBILITY_FILTER = gql` 5 | query GetVisibilityFilter { 6 | visibilityFilter @client { 7 | id 8 | displayName 9 | } 10 | } 11 | ` -------------------------------------------------------------------------------- /apollo-remote-state/client/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /apollo-remote-state/client/src/tests/createMockReactiveVar.spec.ts: -------------------------------------------------------------------------------- 1 | import { mockTodosVar } from "./mocks/mockTodosVar"; 2 | 3 | describe('Testing mock reactive variables', () => { 4 | beforeEach(() => mockTodosVar([])); 5 | 6 | it('should set the current value if provided', () => { 7 | mockTodosVar([{ id: 0, text: 'First todo', completed: false }]); 8 | expect(mockTodosVar()?.length).toEqual(1); 9 | }) 10 | 11 | it('should overwrite the current value', () => { 12 | mockTodosVar([{ id: 0, text: 'First todo', completed: false }]); 13 | expect(mockTodosVar()?.length).toEqual(1); 14 | 15 | mockTodosVar([{ id: 1, text: 'Second todo', completed: false }]); 16 | expect(mockTodosVar()?.length).toEqual(1); 17 | }); 18 | 19 | it('should not overwrite the current value if no value provided', () => { 20 | mockTodosVar([ 21 | { id: 0, text: 'First todo', completed: false }, 22 | { id: 1, text: 'Second todo', completed: false } 23 | ]); 24 | expect(mockTodosVar()?.length).toEqual(2); 25 | 26 | mockTodosVar(); 27 | expect(mockTodosVar()?.length).toEqual(2); 28 | }) 29 | }) -------------------------------------------------------------------------------- /apollo-remote-state/client/src/tests/createMockReactiveVar.ts: -------------------------------------------------------------------------------- 1 | 2 | import { ReactiveVar } from "@apollo/client"; 3 | 4 | // This is how to create a mock reactive variable. 5 | // It's a good idea to do this because then we can test our 6 | // interaction logic. 7 | 8 | export function createMockReactiveVar (defaultValue?: T): ReactiveVar { 9 | let currentValue: T | undefined = defaultValue; 10 | 11 | return function mockReactiveVar ( 12 | newValue?: T 13 | ) : T { 14 | if (newValue) { 15 | currentValue = newValue; 16 | } 17 | return currentValue as T; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /apollo-remote-state/client/src/tests/mocks/mockTodosVar.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Todos } from "../../models/Todos"; 3 | import { createMockReactiveVar } from "../createMockReactiveVar"; 4 | 5 | export const mockTodosVar = createMockReactiveVar([]); -------------------------------------------------------------------------------- /apollo-remote-state/client/src/tests/mocks/mockVisibilityFilter.ts: -------------------------------------------------------------------------------- 1 | 2 | import { createMockReactiveVar } from "../createMockReactiveVar"; 3 | import { VisibilityFilter, VisibilityFilters } from "../../models/VisibilityFilter"; 4 | 5 | export const mockVisibilityFilter = createMockReactiveVar( 6 | VisibilityFilters.SHOW_ALL 7 | ); 8 | -------------------------------------------------------------------------------- /apollo-remote-state/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react" 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /apollo-remote-state/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1 3 | } 4 | -------------------------------------------------------------------------------- /apollo-remote-state/server/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env -------------------------------------------------------------------------------- /apollo-remote-state/server/apollo.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | service: { 3 | name: "ac3-todos-backend" 4 | } 5 | } -------------------------------------------------------------------------------- /apollo-remote-state/server/codegen.yml: -------------------------------------------------------------------------------- 1 | overwrite: true 2 | schema: "http://localhost:4000" 3 | generates: 4 | ./src/generated/graphql.ts: 5 | plugins: 6 | - "typescript" 7 | - "typescript-resolvers" 8 | ./graphql.schema.json: 9 | plugins: 10 | - "introspection" 11 | -------------------------------------------------------------------------------- /apollo-remote-state/server/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src", "bin"], 3 | "ext": ".ts,.js", 4 | "ignore": [], 5 | "exec": "ts-node --transpile-only ./src/index.ts" 6 | } -------------------------------------------------------------------------------- /apollo-remote-state/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "nodemon", 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "codegen": "graphql-codegen --config ./codegen.yml" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "apollo-server": "^2.11.0", 16 | "graphql": "^14.6.0" 17 | }, 18 | "devDependencies": { 19 | "@graphql-codegen/cli": "^1.13.1", 20 | "@graphql-codegen/introspection": "1.13.0", 21 | "@graphql-codegen/typescript": "1.13.0", 22 | "@graphql-codegen/typescript-resolvers": "1.13.0", 23 | "nodemon": "^2.0.2", 24 | "ts-node": "^8.6.2", 25 | "typescript": "^3.8.3" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /apollo-remote-state/server/src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import { ApolloServer } from 'apollo-server' 3 | import { typeDefs } from './schema' 4 | import { resolvers } from './resolvers' 5 | import { todosRepo } from './modules/todos/repos'; 6 | import { TodoRepo } from './modules/todos/repos/todoRepo'; 7 | 8 | export type Context = { todosRepo: TodoRepo } 9 | 10 | const server = new ApolloServer({ 11 | context: () => ({ todosRepo } as Context), 12 | typeDefs, 13 | resolvers 14 | }); 15 | 16 | server.listen().then(({ url }) => { 17 | console.log(`🚀 Server ready at ${url}`); 18 | }); -------------------------------------------------------------------------------- /apollo-remote-state/server/src/modules/todos/repos/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import { InMemoryTodoRepo } from "./implementations/InMemoryTodoRepo"; 3 | 4 | const todosRepo = new InMemoryTodoRepo(); 5 | 6 | export { todosRepo }; -------------------------------------------------------------------------------- /apollo-remote-state/server/src/modules/todos/repos/todoRepo.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Todo } from "../../../generated/graphql"; 3 | 4 | export interface TodoRepo { 5 | addTodo(text: string): Promise; 6 | completeTodo (id: number): Promise; 7 | clearCompletedTodos(): Promise; 8 | completeAllTodos (): Promise; 9 | deleteTodo(id: number): Promise; 10 | editTodo(id: number, text: string): Promise; 11 | getAllTodos (): Promise; 12 | getTodoById (id: number): Promise; 13 | getLastTodo (): Promise; 14 | } 15 | -------------------------------------------------------------------------------- /apollo-remote-state/server/src/shared/mappers/todoMapper.ts: -------------------------------------------------------------------------------- 1 | import { Todo, TodosConnection, TodosEdge } from "../../generated/graphql"; 2 | 3 | export class TodoMapper { 4 | 5 | public static toEdge (todo: Todo): TodosEdge { 6 | return { 7 | node: todo, 8 | cursor: "" 9 | }; 10 | } 11 | 12 | public static toTodosConnection (todos: Todo[]) : TodosConnection { 13 | return { 14 | edges: todos.map(t => this.toEdge(t)), 15 | pageInfo: { 16 | hasPreviousPage: false, 17 | hasNextPage: false, 18 | startCursor: "", 19 | endCursor: "" 20 | } 21 | }; 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /apollo-remote-state/server/src/shared/utils/numberUtils.ts: -------------------------------------------------------------------------------- 1 | 2 | export class NumberUtils { 3 | public static isANumber (num: any): boolean { 4 | return !isNaN(num); 5 | } 6 | 7 | public static isANonNegativeNumber (num?: any): boolean { 8 | return !isNaN(num) && num >= 0; 9 | } 10 | } -------------------------------------------------------------------------------- /apollo-remote-state/server/src/shared/utils/paginationUtils.ts: -------------------------------------------------------------------------------- 1 | import { Result } from "../core/result"; 2 | import { NumberUtils } from "./numberUtils"; 3 | 4 | interface Node { 5 | id: number 6 | } 7 | 8 | type GenericNodeList = Node[]; 9 | 10 | export class PaginationUtils { 11 | public static filterByBeforeAndAfter ( 12 | items: GenericNodeList, 13 | after?: string, 14 | before?: string, 15 | ) : GenericNodeList { 16 | const isAfterSet = !!after === true; 17 | const isBeforeSet = !!before === true; 18 | 19 | if (isAfterSet) { 20 | const afterIndex = items.findIndex((t) => t.id === Number(after)) 21 | const afterIndexFound = afterIndex !== -1; 22 | 23 | if (afterIndexFound) { 24 | items = items.slice(afterIndex + 1) 25 | } 26 | } 27 | 28 | if (isBeforeSet) { 29 | const beforeIndex = items.findIndex((t) => t.id === Number(after)) 30 | const beforeIndexFond = beforeIndex !== -1; 31 | 32 | if (beforeIndexFond) { 33 | items = items.slice(0, beforeIndex) 34 | } 35 | } 36 | 37 | return items; 38 | } 39 | 40 | public static limitByFirstAndLast( 41 | items: T[], 42 | first?: number, 43 | last?: number 44 | ): Result { 45 | const isFirstSet = NumberUtils.isANumber(first); 46 | const isLastSet = NumberUtils.isANumber(last); 47 | 48 | if (isFirstSet) { 49 | const isFirstAPositiveNumber = NumberUtils.isANonNegativeNumber(first); 50 | 51 | if (!isFirstAPositiveNumber) { 52 | return Result.fail("First has to be greater than 0"); 53 | } 54 | 55 | if (items.length > first) { 56 | return Result.ok(items.slice(0, first)); 57 | } 58 | } 59 | 60 | if (isLastSet) { 61 | const isLastAPositiveNumber = NumberUtils.isANonNegativeNumber(last); 62 | 63 | if (!isLastAPositiveNumber) { 64 | return Result.fail("Last has to be greater than 0"); 65 | } 66 | 67 | if (items.length > last) { 68 | return Result.ok(items.slice(0, last)); 69 | } 70 | } 71 | 72 | return Result.ok(items); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /apollo-remote-state/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "pretty": true, 7 | "sourceMap": true, 8 | "target": "es2017", 9 | "outDir": "./dist", 10 | "lib": ["es6"], 11 | "resolveJsonModule": true, 12 | "types": ["node"], 13 | "typeRoots" : ["./node_modules/@types", "./src/@types"], 14 | "experimentalDecorators": true, 15 | "emitDecoratorMetadata": true, 16 | "esModuleInterop": true 17 | }, 18 | "include": [ 19 | "src/**/*.ts", 20 | ], 21 | "exclude": [ 22 | "node_modules", 23 | ] 24 | } -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1 3 | } 4 | -------------------------------------------------------------------------------- /redux-local-state/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # production 7 | build 8 | 9 | # misc 10 | .DS_Store 11 | npm-debug.log 12 | -------------------------------------------------------------------------------- /redux-local-state/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todomvc-redux", 3 | "version": "0.0.1", 4 | "private": true, 5 | "devDependencies": { 6 | "react-scripts": "^3.0.0", 7 | "react-test-renderer": "^16.8.6" 8 | }, 9 | "dependencies": { 10 | "classnames": "^2.2.6", 11 | "prop-types": "^15.7.2", 12 | "react": "^16.8.6", 13 | "react-dom": "^16.8.6", 14 | "react-redux": "^7.0.2", 15 | "redux": "^4.0.1", 16 | "reselect": "^4.0.0", 17 | "todomvc-app-css": "^2.2.0" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "eject": "react-scripts eject", 23 | "test": "react-scripts test --env=node" 24 | }, 25 | "browserslist": [ 26 | ">0.2%", 27 | "not dead", 28 | "not ie <= 11", 29 | "not op_mini all" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /redux-local-state/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Redux TodoMVC Example 7 | 8 | 9 |
    10 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /redux-local-state/src/actions/index.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/ActionTypes' 2 | 3 | export const addTodo = text => ({ type: types.ADD_TODO, text }) 4 | export const deleteTodo = id => ({ type: types.DELETE_TODO, id }) 5 | export const editTodo = (id, text) => ({ type: types.EDIT_TODO, id, text }) 6 | export const completeTodo = id => ({ type: types.COMPLETE_TODO, id }) 7 | export const completeAllTodos = () => ({ type: types.COMPLETE_ALL_TODOS }) 8 | export const clearCompleted = () => ({ type: types.CLEAR_COMPLETED }) 9 | export const setVisibilityFilter = filter => ({ type: types.SET_VISIBILITY_FILTER, filter}) 10 | -------------------------------------------------------------------------------- /redux-local-state/src/actions/index.spec.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/ActionTypes' 2 | import * as actions from './index' 3 | 4 | describe('todo actions', () => { 5 | it('addTodo should create ADD_TODO action', () => { 6 | expect(actions.addTodo('Use Redux')).toEqual({ 7 | type: types.ADD_TODO, 8 | text: 'Use Redux' 9 | }) 10 | }) 11 | 12 | it('deleteTodo should create DELETE_TODO action', () => { 13 | expect(actions.deleteTodo(1)).toEqual({ 14 | type: types.DELETE_TODO, 15 | id: 1 16 | }) 17 | }) 18 | 19 | it('editTodo should create EDIT_TODO action', () => { 20 | expect(actions.editTodo(1, 'Use Redux everywhere')).toEqual({ 21 | type: types.EDIT_TODO, 22 | id: 1, 23 | text: 'Use Redux everywhere' 24 | }) 25 | }) 26 | 27 | it('completeTodo should create COMPLETE_TODO action', () => { 28 | expect(actions.completeTodo(1)).toEqual({ 29 | type: types.COMPLETE_TODO, 30 | id: 1 31 | }) 32 | }) 33 | 34 | it('completeAll should create COMPLETE_ALL action', () => { 35 | expect(actions.completeAllTodos()).toEqual({ 36 | type: types.COMPLETE_ALL_TODOS 37 | }) 38 | }) 39 | 40 | it('clearCompleted should create CLEAR_COMPLETED action', () => { 41 | expect(actions.clearCompleted()).toEqual({ 42 | type: types.CLEAR_COMPLETED 43 | }) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /redux-local-state/src/components/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Header from '../containers/Header' 3 | import MainSection from '../containers/MainSection' 4 | 5 | const App = () => ( 6 |
    7 |
    8 | 9 |
    10 | ) 11 | 12 | export default App 13 | -------------------------------------------------------------------------------- /redux-local-state/src/components/App.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createRenderer } from 'react-test-renderer/shallow' 3 | import App from './App' 4 | import Header from '../containers/Header' 5 | import MainSection from '../containers/MainSection' 6 | 7 | const setup = _propOverrides => { 8 | const renderer = createRenderer() 9 | renderer.render() 10 | const output = renderer.getRenderOutput() 11 | return output 12 | } 13 | 14 | describe('components', () => { 15 | describe('Header', () => { 16 | it('should render', () => { 17 | const output = setup() 18 | const [header] = output.props.children 19 | expect(header.type).toBe(Header) 20 | }) 21 | }) 22 | 23 | describe('Mainsection', () => { 24 | it('should render', () => { 25 | const output = setup() 26 | const [, mainSection] = output.props.children 27 | expect(mainSection.type).toBe(MainSection) 28 | }) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /redux-local-state/src/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import FilterLink from '../containers/FilterLink' 4 | import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../constants/TodoFilters' 5 | 6 | const FILTER_TITLES = { 7 | [SHOW_ALL]: 'All', 8 | [SHOW_ACTIVE]: 'Active', 9 | [SHOW_COMPLETED]: 'Completed' 10 | } 11 | 12 | const Footer = (props) => { 13 | const { activeCount, completedCount, onClearCompleted } = props 14 | const itemWord = activeCount === 1 ? 'item' : 'items' 15 | return ( 16 |
    17 | 18 | {activeCount || 'No'} {itemWord} left 19 | 20 |
      21 | {Object.keys(FILTER_TITLES).map(filter => 22 |
    • 23 | 24 | {FILTER_TITLES[filter]} 25 | 26 |
    • 27 | )} 28 |
    29 | { 30 | !!completedCount && 31 | 35 | 36 | } 37 |
    38 | ) 39 | } 40 | 41 | Footer.propTypes = { 42 | completedCount: PropTypes.number.isRequired, 43 | activeCount: PropTypes.number.isRequired, 44 | onClearCompleted: PropTypes.func.isRequired, 45 | } 46 | 47 | export default Footer 48 | -------------------------------------------------------------------------------- /redux-local-state/src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import TodoTextInput from './TodoTextInput' 4 | 5 | const Header = ({ addTodo }) => ( 6 |
    7 |

    todos

    8 | { 11 | if (text.length !== 0) { 12 | addTodo(text) 13 | } 14 | }} 15 | placeholder="What needs to be done?" 16 | /> 17 |
    18 | ) 19 | 20 | Header.propTypes = { 21 | addTodo: PropTypes.func.isRequired 22 | } 23 | 24 | export default Header 25 | -------------------------------------------------------------------------------- /redux-local-state/src/components/Header.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createRenderer } from 'react-test-renderer/shallow'; 3 | import Header from './Header' 4 | import TodoTextInput from '../components/TodoTextInput' 5 | 6 | const setup = () => { 7 | const props = { 8 | addTodo: jest.fn() 9 | } 10 | 11 | const renderer = createRenderer(); 12 | renderer.render(
    ) 13 | const output = renderer.getRenderOutput() 14 | 15 | return { 16 | props: props, 17 | output: output, 18 | renderer: renderer 19 | } 20 | } 21 | 22 | describe('components', () => { 23 | describe('Header', () => { 24 | it('should render correctly', () => { 25 | const { output } = setup() 26 | expect(output.type).toBe('header') 27 | expect(output.props.className).toBe('header') 28 | 29 | const [ h1, input ] = output.props.children 30 | expect(h1.type).toBe('h1') 31 | expect(h1.props.children).toBe('todos') 32 | expect(input.type).toBe(TodoTextInput) 33 | expect(input.props.newTodo).toBe(true) 34 | expect(input.props.placeholder).toBe('What needs to be done?') 35 | }) 36 | 37 | it('should call addTodo if length of text is greater than 0', () => { 38 | const { output, props } = setup() 39 | const input = output.props.children[1] 40 | input.props.onSave('') 41 | expect(props.addTodo).not.toBeCalled() 42 | input.props.onSave('Use Redux') 43 | expect(props.addTodo).toBeCalled() 44 | }) 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /redux-local-state/src/components/Link.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import classnames from 'classnames' 4 | 5 | const Link = ({ active, children, setFilter }) => 6 | ( 7 | // eslint-disable-next-line jsx-a11y/anchor-is-valid 8 | setFilter()} 12 | > 13 | {children} 14 | 15 | ) 16 | 17 | 18 | Link.propTypes = { 19 | active: PropTypes.bool.isRequired, 20 | children: PropTypes.node.isRequired, 21 | setFilter: PropTypes.func.isRequired 22 | } 23 | 24 | export default Link 25 | -------------------------------------------------------------------------------- /redux-local-state/src/components/Link.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createRenderer } from 'react-test-renderer/shallow'; 3 | import Link from './Link' 4 | 5 | const setup = (propOverrides) => { 6 | const props = Object.assign({ 7 | active: false, 8 | children: 'All', 9 | setFilter: jest.fn() 10 | }, propOverrides) 11 | 12 | const renderer = createRenderer(); 13 | renderer.render() 14 | const output = renderer.getRenderOutput() 15 | 16 | return { 17 | props: props, 18 | output: output, 19 | } 20 | } 21 | 22 | describe('component', () => { 23 | describe('Link', () => { 24 | it('should render correctly', () => { 25 | const { output } = setup() 26 | expect(output.type).toBe('a') 27 | expect(output.props.style.cursor).toBe('pointer') 28 | expect(output.props.children).toBe('All') 29 | }) 30 | 31 | it('should have class selected if active', () => { 32 | const { output } = setup({ active: true }) 33 | expect(output.props.className).toBe('selected') 34 | }) 35 | 36 | it('should call setFilter on click', () => { 37 | const { output, props } = setup() 38 | output.props.onClick() 39 | expect(props.setFilter).toBeCalled() 40 | }) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /redux-local-state/src/components/MainSection.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import Footer from './Footer' 4 | import VisibleTodoList from '../containers/VisibleTodoList' 5 | 6 | const MainSection = ({ todosCount, completedCount, actions }) => 7 | ( 8 |
    9 | { 10 | !!todosCount && 11 | 12 | 18 | 20 | } 21 | 22 | { 23 | !!todosCount && 24 |
    29 | } 30 |
    31 | ) 32 | 33 | MainSection.propTypes = { 34 | todosCount: PropTypes.number.isRequired, 35 | completedCount: PropTypes.number.isRequired, 36 | actions: PropTypes.object.isRequired 37 | } 38 | 39 | export default MainSection; -------------------------------------------------------------------------------- /redux-local-state/src/components/TodoItem.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import classnames from 'classnames' 4 | import TodoTextInput from './TodoTextInput' 5 | 6 | export default class TodoItem extends Component { 7 | static propTypes = { 8 | todo: PropTypes.object.isRequired, 9 | editTodo: PropTypes.func.isRequired, 10 | deleteTodo: PropTypes.func.isRequired, 11 | completeTodo: PropTypes.func.isRequired 12 | } 13 | 14 | state = { 15 | editing: false 16 | } 17 | 18 | handleDoubleClick = () => { 19 | this.setState({ editing: true }) 20 | } 21 | 22 | handleSave = (id, text) => { 23 | if (text.length === 0) { 24 | this.props.deleteTodo(id) 25 | } else { 26 | this.props.editTodo(id, text) 27 | } 28 | this.setState({ editing: false }) 29 | } 30 | 31 | render() { 32 | const { todo, completeTodo, deleteTodo } = this.props 33 | 34 | let element 35 | if (this.state.editing) { 36 | element = ( 37 | this.handleSave(todo.id, text)} /> 40 | ) 41 | } else { 42 | element = ( 43 |
    44 | completeTodo(todo.id)} /> 48 | 51 |
    54 | ) 55 | } 56 | 57 | return ( 58 |
  • 62 | {element} 63 |
  • 64 | ) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /redux-local-state/src/components/TodoList.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import TodoItem from './TodoItem' 4 | 5 | const TodoList = ({ filteredTodos, actions }) => ( 6 |
      7 | {filteredTodos.map(todo => 8 | 9 | )} 10 |
    11 | ) 12 | 13 | TodoList.propTypes = { 14 | filteredTodos: PropTypes.arrayOf(PropTypes.shape({ 15 | id: PropTypes.number.isRequired, 16 | completed: PropTypes.bool.isRequired, 17 | text: PropTypes.string.isRequired 18 | }).isRequired).isRequired, 19 | actions: PropTypes.object.isRequired 20 | } 21 | 22 | export default TodoList 23 | -------------------------------------------------------------------------------- /redux-local-state/src/components/TodoList.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createRenderer } from 'react-test-renderer/shallow'; 3 | import TodoList from './TodoList' 4 | import TodoItem from './TodoItem' 5 | 6 | const setup = () => { 7 | const props = { 8 | filteredTodos: [ 9 | { 10 | text: 'Use Redux', 11 | completed: false, 12 | id: 0 13 | }, { 14 | text: 'Run the tests', 15 | completed: true, 16 | id: 1 17 | } 18 | ], 19 | actions: { 20 | editTodo: jest.fn(), 21 | deleteTodo: jest.fn(), 22 | completeTodo: jest.fn(), 23 | completeAll: jest.fn(), 24 | clearCompleted: jest.fn() 25 | } 26 | } 27 | 28 | const renderer = createRenderer(); 29 | renderer.render() 30 | const output = renderer.getRenderOutput() 31 | 32 | return { 33 | props: props, 34 | output: output 35 | } 36 | } 37 | 38 | describe('components', () => { 39 | describe('TodoList', () => { 40 | it('should render container', () => { 41 | const { output } = setup() 42 | expect(output.type).toBe('ul') 43 | expect(output.props.className).toBe('todo-list') 44 | }) 45 | 46 | it('should render todos', () => { 47 | const { output, props } = setup() 48 | expect(output.props.children.length).toBe(2) 49 | output.props.children.forEach((todo, i) => { 50 | expect(todo.type).toBe(TodoItem) 51 | expect(Number(todo.key)).toBe(props.filteredTodos[i].id) 52 | expect(todo.props.todo).toBe(props.filteredTodos[i]) 53 | }) 54 | }) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /redux-local-state/src/components/TodoTextInput.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import classnames from 'classnames' 4 | 5 | export default class TodoTextInput extends Component { 6 | static propTypes = { 7 | onSave: PropTypes.func.isRequired, 8 | text: PropTypes.string, 9 | placeholder: PropTypes.string, 10 | editing: PropTypes.bool, 11 | newTodo: PropTypes.bool 12 | } 13 | 14 | state = { 15 | text: this.props.text || '' 16 | } 17 | 18 | handleSubmit = e => { 19 | const text = e.target.value.trim() 20 | if (e.which === 13) { 21 | this.props.onSave(text) 22 | if (this.props.newTodo) { 23 | this.setState({ text: '' }) 24 | } 25 | } 26 | } 27 | 28 | handleChange = e => { 29 | this.setState({ text: e.target.value }) 30 | } 31 | 32 | handleBlur = e => { 33 | if (!this.props.newTodo) { 34 | this.props.onSave(e.target.value) 35 | } 36 | } 37 | 38 | render() { 39 | return ( 40 | 52 | ) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /redux-local-state/src/constants/ActionTypes.js: -------------------------------------------------------------------------------- 1 | export const ADD_TODO = 'ADD_TODO' 2 | export const DELETE_TODO = 'DELETE_TODO' 3 | export const EDIT_TODO = 'EDIT_TODO' 4 | export const COMPLETE_TODO = 'COMPLETE_TODO' 5 | export const COMPLETE_ALL_TODOS = 'COMPLETE_ALL_TODOS' 6 | export const CLEAR_COMPLETED = 'CLEAR_COMPLETED' 7 | export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER' 8 | -------------------------------------------------------------------------------- /redux-local-state/src/constants/TodoFilters.js: -------------------------------------------------------------------------------- 1 | export const SHOW_ALL = 'show_all' 2 | export const SHOW_COMPLETED = 'show_completed' 3 | export const SHOW_ACTIVE = 'show_active' 4 | -------------------------------------------------------------------------------- /redux-local-state/src/containers/FilterLink.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { setVisibilityFilter } from '../actions' 3 | import Link from '../components/Link' 4 | 5 | const mapStateToProps = (state, ownProps) => ({ 6 | active: ownProps.filter === state.visibilityFilter 7 | }) 8 | 9 | const mapDispatchToProps = (dispatch, ownProps) => ({ 10 | setFilter: () => { 11 | dispatch(setVisibilityFilter(ownProps.filter)) 12 | } 13 | }) 14 | 15 | export default connect( 16 | mapStateToProps, 17 | mapDispatchToProps 18 | )(Link) 19 | -------------------------------------------------------------------------------- /redux-local-state/src/containers/Header.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import Header from '../components/Header' 3 | import { addTodo } from '../actions' 4 | 5 | export default connect(null, { addTodo })(Header) 6 | -------------------------------------------------------------------------------- /redux-local-state/src/containers/MainSection.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import * as TodoActions from '../actions' 3 | import { bindActionCreators } from 'redux' 4 | import MainSection from '../components/MainSection' 5 | import { getCompletedTodoCount } from '../selectors' 6 | 7 | 8 | const mapStateToProps = state => ({ 9 | todosCount: state.todos.length, 10 | completedCount: getCompletedTodoCount(state) 11 | }) 12 | 13 | 14 | const mapDispatchToProps = dispatch => ({ 15 | actions: bindActionCreators(TodoActions, dispatch) 16 | }) 17 | 18 | 19 | export default connect( 20 | mapStateToProps, 21 | mapDispatchToProps 22 | )(MainSection) 23 | 24 | -------------------------------------------------------------------------------- /redux-local-state/src/containers/VisibleTodoList.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { bindActionCreators } from 'redux' 3 | import * as TodoActions from '../actions' 4 | import TodoList from '../components/TodoList' 5 | import { getVisibleTodos } from '../selectors' 6 | 7 | const mapStateToProps = state => ({ 8 | filteredTodos: getVisibleTodos(state) 9 | }) 10 | 11 | const mapDispatchToProps = dispatch => ({ 12 | actions: bindActionCreators(TodoActions, dispatch) 13 | }) 14 | 15 | 16 | const VisibleTodoList = connect( 17 | mapStateToProps, 18 | mapDispatchToProps 19 | )(TodoList) 20 | 21 | export default VisibleTodoList 22 | -------------------------------------------------------------------------------- /redux-local-state/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { createStore } from 'redux' 4 | import { Provider } from 'react-redux' 5 | import App from './components/App' 6 | import reducer from './reducers' 7 | import 'todomvc-app-css/index.css' 8 | 9 | const store = createStore(reducer) 10 | 11 | render( 12 | 13 | 14 | , 15 | document.getElementById('root') 16 | ) 17 | -------------------------------------------------------------------------------- /redux-local-state/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import todos from './todos' 3 | import visibilityFilter from './visibilityFilter' 4 | 5 | const rootReducer = combineReducers({ 6 | todos, 7 | visibilityFilter 8 | }) 9 | 10 | export default rootReducer 11 | -------------------------------------------------------------------------------- /redux-local-state/src/reducers/todos.js: -------------------------------------------------------------------------------- 1 | import { 2 | ADD_TODO, 3 | DELETE_TODO, 4 | EDIT_TODO, 5 | COMPLETE_TODO, 6 | COMPLETE_ALL_TODOS, 7 | CLEAR_COMPLETED 8 | } from '../constants/ActionTypes' 9 | 10 | const initialState = [ 11 | { 12 | text: 'Use Redux', 13 | completed: false, 14 | id: 0 15 | } 16 | ] 17 | 18 | export default function todos(state = initialState, action) { 19 | switch (action.type) { 20 | case ADD_TODO: 21 | return [ 22 | ...state, 23 | { 24 | id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1, 25 | completed: false, 26 | text: action.text 27 | } 28 | ] 29 | 30 | case DELETE_TODO: 31 | return state.filter(todo => 32 | todo.id !== action.id 33 | ) 34 | 35 | case EDIT_TODO: 36 | return state.map(todo => 37 | todo.id === action.id ? 38 | { ...todo, text: action.text } : 39 | todo 40 | ) 41 | 42 | case COMPLETE_TODO: 43 | return state.map(todo => 44 | todo.id === action.id ? 45 | { ...todo, completed: !todo.completed } : 46 | todo 47 | ) 48 | 49 | case COMPLETE_ALL_TODOS: 50 | const areAllMarked = state.every(todo => todo.completed) 51 | return state.map(todo => ({ 52 | ...todo, 53 | completed: !areAllMarked 54 | })) 55 | 56 | case CLEAR_COMPLETED: 57 | return state.filter(todo => todo.completed === false) 58 | 59 | default: 60 | return state 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /redux-local-state/src/reducers/visibilityFilter.js: -------------------------------------------------------------------------------- 1 | import { SET_VISIBILITY_FILTER } from '../constants/ActionTypes' 2 | import { SHOW_ALL } from '../constants/TodoFilters' 3 | 4 | const visibilityFilter = (state = SHOW_ALL, action) => { 5 | switch (action.type) { 6 | case SET_VISIBILITY_FILTER: 7 | return action.filter 8 | default: 9 | return state 10 | } 11 | } 12 | 13 | export default visibilityFilter -------------------------------------------------------------------------------- /redux-local-state/src/selectors/index.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect' 2 | import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../constants/TodoFilters' 3 | 4 | const getVisibilityFilter = state => state.visibilityFilter 5 | const getTodos = state => state.todos 6 | 7 | export const getVisibleTodos = createSelector( 8 | [getVisibilityFilter, getTodos], 9 | (visibilityFilter, todos) => { 10 | switch (visibilityFilter) { 11 | case SHOW_ALL: 12 | return todos 13 | case SHOW_COMPLETED: 14 | return todos.filter(t => t.completed) 15 | case SHOW_ACTIVE: 16 | return todos.filter(t => !t.completed) 17 | default: 18 | throw new Error('Unknown filter: ' + visibilityFilter) 19 | } 20 | } 21 | ) 22 | 23 | export const getCompletedTodoCount = createSelector( 24 | [getTodos], 25 | todos => ( 26 | todos.reduce((count, todo) => 27 | todo.completed ? count + 1 : count, 28 | 0 29 | ) 30 | ) 31 | ) --------------------------------------------------------------------------------