├── .DS_Store ├── .env ├── .gitignore ├── LICENSE ├── README.md ├── assets ├── client-diagram.png └── server-diagram.png ├── babel.config.js ├── client-diagram.png ├── client ├── .DS_Store ├── __tests__ │ ├── TodoList.js │ └── utils.js ├── components │ ├── .DS_Store │ ├── App.jsx │ ├── Demo │ │ ├── AddTodo.jsx │ │ ├── Demo.scss │ │ ├── Offline.jsx │ │ ├── TodoList.jsx │ │ ├── UpdateTodo.jsx │ │ └── index.jsx │ ├── Info │ │ ├── Info.scss │ │ └── index.jsx │ ├── Main │ │ ├── AddTodo.jsx │ │ ├── TodoItem.jsx │ │ ├── TodoList.jsx │ │ └── index.jsx │ ├── Navigation.jsx │ └── Team │ │ ├── Andrew.jpg │ │ ├── Chan.jpg │ │ ├── Duy.png │ │ ├── Member.jsx │ │ ├── Nelson.jpg │ │ ├── Team.scss │ │ └── index.jsx ├── hooks │ └── index.jsx ├── index.jsx └── query │ └── index.js ├── dump.rdb ├── fake-database.json ├── filament ├── constants │ └── index.js ├── filamentMiddleware.js ├── hooks │ ├── index.jsx │ ├── useFilamentMutation.jsx │ └── useFilamentQuery.jsx ├── index.js ├── parseClientFilamentQuery.js ├── parseServerFilamentQuery.js └── utils │ ├── getSessionStorageKey.js │ ├── index.js │ ├── mergeTwoArraysById.js │ ├── parseKeyInCache.js │ ├── transformQuery.js │ └── uniqueId.js ├── index.html ├── logoBig.jpg ├── offline-diagram.png ├── package-lock.json ├── package.json ├── placeholderLogo.png ├── pnpm-lock.yaml ├── server-diagram.png ├── server ├── .DS_Store ├── __tests__ │ └── server.js ├── memberModel.js ├── resolvers.js ├── schema.graphql ├── schema.js ├── server.js └── todoModel.js └── webpack.config.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/FilamentQL/d9ba61ff7bab74da0a51da5b281f218990fa26f1/.DS_Store -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | MONGO_URI=mongodb+srv://bobdeei:ajax0401mongoDB@cluster0.qgsjv.mongodb.net/filament?retryWrites=true&w=majority -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 OSLabs Beta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | ## FilamentQL 4 | 5 | FilamentQL is a lightweight caching library for GraphQL queries that utilizes a parsing algorithm to detect differences between incoming queries and existing data stored within the cache. The library offers tools for both client and server side caching as well as tools for offline mode. 6 | 7 | FilamentQL's npm package can be [downloaded here](https://www.npmjs.com/package/filamentql) 8 | 9 | ### Server-Side Caching 10 | 11 | On the server-side, FilamentQL provides a GraphQL endpoint for your Express server with user-defined type definitions and resolvers, and creates a caching layer via a local Redis instance. When a client makes a request, FilamentQL checks the Redis cache for any previously stored data that matches the request. Through a parsing algorithm, FilamentQL then takes the incoming query and identifies any dissimilarities, and makes a subsequent query for just those dissimilarities to the database. Whenever possible, FilamentQL merges data coming back from the database with data from Redis cache and sends it back to the client: 12 | 13 |

14 | 15 | 16 | ### Client-Side Caching 17 | 18 | On the client-side, FilamentQL behaves similarly. For its local cache implementation, FilamentQL utilizes session storage, a built-in property on the browser's window object, which allows data to persist throughout page refreshes. 19 | 20 |

21 | 22 | ### Offline Mode 23 | 24 | FilamentQL also supports an offline mode. If the user gets disconnected from the server, all mutations made when the internet is down will be stored in a queue. At a set interval, FilamentQL checks if the network is back online. Whenever the user is back online, FilamentQL dequeues and sends each mutation to the server. Subsequently, the data that comes back from server will update the state and re-render the frontend components. What the user experiences and sees on their end is a seamless re-syncing of information when they come back online. 25 | 26 |

27 | 28 | ## Installation 29 | 30 | ### 1. Redis 31 | 32 | FilamentQL utilizes Redis for its server-side caching. If Redis is not already installed on your machine: 33 | 34 | - Install on Mac using Homebrew: 35 | - In the terminal, enter `brew install redis` 36 | - When installation completes, start a redis server by entering `redis-server` 37 | - By default redis server runs on `localhost:6379` 38 | - To check if your redis server is working: send a ping to the redis server by entering the command `redis-cli ping`, you will get a `PONG` in response if your redis server is working properly. 39 | - Install on Linux or non-Homebrew: 40 | - Download appropriate version of Redis from [redis.io/download](http://redis.io/download) 41 | - Follow installation instructions 42 | - When installation completes, start a redis server by entering `redis-server` 43 | - By default redis server runs on `localhost:6379` 44 | - To check if your redis server is working: send a ping to the redis server by entering the command `redis-cli ping`, you will get a `PONG` in response if your redis server is working properly. 45 | 46 | ### 2. FilamentQL 47 | Check out our [npm package](https://www.npmjs.com/package/filamentql) 48 | 49 | `npm install filamentql` 50 | 51 | ## Instructions 52 | 53 | FilamentQL comes with `filamentql/client` and `filamentql/server` in order to make all the magic happen. 54 | 55 | On client side, `filamentql` exposes 2 hooks: 56 | - `useFilamentQuery` 57 | - helps query data from GraphQL server 58 | - `useFilamentMutation` 59 | - helps make mutation even though the network is offline 60 | 61 | Both abstract away how to fetch queries and mutations and automatically update state when data is returned from server. 62 | 63 | ### Client Example 64 | 65 | ```JSX 66 | import React, { useState } from 'react'; 67 | import { useFilamentQuery, useFilamentMutation } from 'filamentql/client'; 68 | 69 | const query = ` 70 | query { 71 | todos { 72 | id 73 | text 74 | isCompleted 75 | } 76 | } 77 | `; 78 | 79 | const mutation = (text) => ` 80 | mutation { 81 | addTodo(input: { text: "${text}" }) { 82 | id 83 | text 84 | isCompleted 85 | } 86 | } 87 | `; 88 | 89 | const App = () => { 90 | const [value, setValue] = useState(''); 91 | const { state: todosQuery } = useFilamentQuery(query); 92 | const [callAddTodoMutation, addTodoResponse] = useFilamentMutation( 93 | mutation, 94 | () => { 95 | // this callback is invoked when data is returned from server 96 | // this is a good place to update relevant state now with new data 97 | console.log(addTodoResponse.addTodo); 98 | } 99 | ); 100 | 101 | const handleSubmit = (event) => { 102 | event.preventDefault(); 103 | callAddTodoMutation(value); 104 | setValue('') 105 | }; 106 | 107 | const handleChange = (event) => setValue(event.target.value) 108 | 109 | return ( 110 |
111 |
112 | 113 | 114 |
115 |
116 | {todosQuery && 117 | todosQuery.todos.map((todo) => )} 118 |
119 |
120 | ); 121 | }; 122 | 123 | export default App; 124 | ``` 125 | 126 | ### Server Example 127 | 128 | FilamentQL achieves the caching ability via Express middleware `/filament`. This middleware will determine if it needs to talk to `/graphql` or just returns the data from cache. 129 | 130 | Since `useFilamentQuery` under the hood will send all queries to `/filament`, the middleware `/filament` needs to be setup in order to facilitate caching process. 131 | 132 | And make sure to mount your GraphQL server at route `/graphql`. 133 | 134 | ```js 135 | const express = require('express'); 136 | const { graphqlHTTP } = require('express-graphql'); 137 | const redis = require('redis'); 138 | 139 | // Redis Setup 140 | const client = redis.createClient(); 141 | client 142 | .on('error', (err) => console.log('Error: ' + err)) 143 | .on('connect', () => console.log('Redis client connected')); 144 | 145 | // FilamentQL Setup 146 | const filamentMiddlewareWrapper = require('filamentql/server'); 147 | const filamentMiddleware = filamentMiddlewareWrapper(client); 148 | 149 | // Express setup 150 | const app = express(); 151 | const PORT = 4000; 152 | const schema = require('./schema'); 153 | 154 | app.use(express.json()); 155 | app.use('/filament', filamentMiddleware); 156 | app.use( 157 | '/graphql', 158 | graphqlHTTP((req) => ({ 159 | schema, 160 | graphiql: true, 161 | context: { 162 | req, 163 | }, 164 | })) 165 | ); 166 | 167 | app.listen(PORT, () => console.log(`GraphQL server is on port: ${PORT}`)); 168 | ``` 169 | 170 | ## Contributors 171 | 172 | FilamentQL is an open-source NPM package created in collaboration with [OS Labs](https://github.com/oslabs-beta/) and developed by 173 | - [Andrew Lovato](https://github.com/andrew-lovato) 174 | - [Chan Choi](https://github.com/chanychoi93) 175 | - [Duy Nguyen](https://github.com/bobdeei) 176 | - [Nelson Wu](https://github.com/neljson) 177 | 178 | More infomation about FilamentQL can be found at [filamentql.io](http://filamentql.io/) 179 | 180 | ## Notes 181 | 182 | - Currently, FilamentQL v1.0 can only cache and parse queries without arguments, variables, or directives and do not support caching for mutation. 183 | -------------------------------------------------------------------------------- /assets/client-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/FilamentQL/d9ba61ff7bab74da0a51da5b281f218990fa26f1/assets/client-diagram.png -------------------------------------------------------------------------------- /assets/server-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/FilamentQL/d9ba61ff7bab74da0a51da5b281f218990fa26f1/assets/server-diagram.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', { targets: { node: 'current' } }], 4 | '@babel/preset-react', 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /client-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/FilamentQL/d9ba61ff7bab74da0a51da5b281f218990fa26f1/client-diagram.png -------------------------------------------------------------------------------- /client/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/FilamentQL/d9ba61ff7bab74da0a51da5b281f218990fa26f1/client/.DS_Store -------------------------------------------------------------------------------- /client/__tests__/TodoList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { configure } from 'enzyme'; 3 | import Adapter from 'enzyme-adapter-react-16'; 4 | import TodoItem from '../components/Main/TodoItem.jsx'; 5 | 6 | configure({ adapter: new Adapter() }); 7 | 8 | import { shallow, mount, render } from 'enzyme'; 9 | 10 | describe('initial test', () => { 11 | let wrapper; 12 | 13 | const props = { 14 | id: 1342432, 15 | text: 'test', 16 | isCompleted: false, 17 | number: 5, 18 | toggleTodo: true 19 | } 20 | 21 | beforeAll(() => { 22 | wrapper = shallow(); 23 | }) 24 | 25 | it('should not equal 3', () => { 26 | expect(wrapper.find('button').length).not.toEqual(3) 27 | }) 28 | 29 | it('should pass the test', () => { 30 | expect(wrapper.find('button').length).toEqual(2) 31 | 32 | }) 33 | }) 34 | 35 | -------------------------------------------------------------------------------- /client/__tests__/utils.js: -------------------------------------------------------------------------------- 1 | import parseClientFilamentQuery from '../../filament/parseClientFilamentQuery' 2 | 3 | describe('parseClientFilamentQuery', () => { 4 | 5 | const queryOnlyId = ` 6 | { 7 | todos { 8 | id 9 | text 10 | completed 11 | } 12 | } 13 | `; 14 | 15 | const queryWantToMake = ` 16 | query { 17 | todos { 18 | id 19 | text 20 | difficulty 21 | } 22 | } 23 | `; 24 | 25 | beforeAll(() => { 26 | sessionStorage.setItem('todos', JSON.stringify([ 27 | { 28 | id: 123214251, 29 | text: 'THIS IS MY TODO!!!', 30 | completed: true, 31 | number: 0, 32 | isChecked: false, 33 | friendTodos: [{ 34 | id: '1323423' 35 | }] 36 | }, 37 | { 38 | id: 5, 39 | text: '555555555555555555', 40 | isChecked: false, 41 | completed: true, 42 | number: 0, 43 | friendTodos: [{ 44 | id: '1323423' 45 | }] 46 | }])) 47 | 48 | }) 49 | it('should pass test', () => { 50 | const [newQuery, cacheData] = parseClientFilamentQuery(queryWantToMake) 51 | return expect(cacheData.todos[0].id).toEqual(123214251) 52 | 53 | }) 54 | 55 | it('should not be in the cache', () => { 56 | const [newQuery, cacheData] = parseClientFilamentQuery(queryWantToMake) 57 | return expect(cacheData.todos[0].difficulty).not.toEqual('not in the cache') 58 | }) 59 | }) -------------------------------------------------------------------------------- /client/components/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/FilamentQL/d9ba61ff7bab74da0a51da5b281f218990fa26f1/client/components/.DS_Store -------------------------------------------------------------------------------- /client/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch, Route } from 'react-router-dom'; 3 | 4 | import Navigation from './Navigation'; 5 | import Main from './Main'; 6 | import Demo from './Demo'; 7 | // import Offline from './Offline/Offline'; 8 | import Team from './Team'; 9 | import Info from './Info'; 10 | 11 | sessionStorage.clear(); 12 | 13 | const App = () => ( 14 |
15 | 16 | 17 | 18 | {/* */} 19 | 20 | 21 | 22 | 23 | 24 |
25 | ); 26 | 27 | export default App; 28 | -------------------------------------------------------------------------------- /client/components/Demo/AddTodo.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { useInput } from '../../hooks'; 4 | 5 | const AddTodo = ({ handleAddTodo }) => { 6 | const [value, setValue, handleChange] = useInput(); 7 | 8 | const handleSubmit = (event) => { 9 | event.preventDefault(); 10 | handleAddTodo(value); 11 | setValue(''); 12 | }; 13 | 14 | return ( 15 |
16 |
17 | 20 | 21 |
22 |
23 | ); 24 | }; 25 | 26 | export default AddTodo; 27 | -------------------------------------------------------------------------------- /client/components/Demo/Demo.scss: -------------------------------------------------------------------------------- 1 | $code-editorBG: #072437; 2 | $code-text: rgba(255, 255, 255, 0.947); 3 | 4 | body { 5 | margin: 0; 6 | color: floralwhite; 7 | } 8 | 9 | .mainDisplay { 10 | display: flex; 11 | justify-content: center; 12 | align-items: center; 13 | flex-direction: column; 14 | margin-top: 200px; 15 | } 16 | 17 | .mainDisplay h1 { 18 | font-size: 50px; 19 | animation: fadeIn ease 2000ms; 20 | -webkit-animation: fadeIn ease 2000ms; 21 | -webkit-animation-delay: 200ms; 22 | animation-delay: 200ms; 23 | opacity: 0; 24 | animation-fill-mode: forwards; 25 | } 26 | 27 | .mainDisplay h3 { 28 | width: 550px; 29 | text-align: center; 30 | line-height: 27px; 31 | } 32 | 33 | .offlineMainDiv h3 { 34 | text-decoration: underline; 35 | } 36 | 37 | .mainDisplay p { 38 | color: floralwhite; 39 | margin: 0px; 40 | } 41 | 42 | .subtitle { 43 | color: floralwhite; 44 | margin: 0px; 45 | // opacity: 0; 46 | animation: fadeIn ease 2s; 47 | -webkit-animation: fadeIn ease 2s; 48 | -webkit-animation-delay: 500ms; 49 | animation-delay: 500ms; 50 | opacity: 0; 51 | animation-fill-mode: forwards; 52 | } 53 | 54 | @keyframes fadeIn { 55 | 0% { 56 | opacity: 0; 57 | } 58 | 100% { 59 | opacity: 1; 60 | } 61 | } 62 | 63 | @-moz-keyframes fadeIn { 64 | 0% { 65 | opacity: 0; 66 | } 67 | 100% { 68 | opacity: 1; 69 | } 70 | } 71 | 72 | @-webkit-keyframes fadeIn { 73 | 0% { 74 | opacity: 0; 75 | } 76 | 100% { 77 | opacity: 1; 78 | } 79 | } 80 | 81 | @-o-keyframes fadeIn { 82 | 0% { 83 | opacity: 0; 84 | } 85 | 100% { 86 | opacity: 1; 87 | } 88 | } 89 | 90 | @-ms-keyframes fadeIn { 91 | 0% { 92 | opacity: 0; 93 | } 94 | 100% { 95 | opacity: 1; 96 | } 97 | } 98 | 99 | .developedBy { 100 | padding-top: 50px; 101 | font-size: 15px; 102 | animation: fadeIn ease 2s; 103 | -webkit-animation: fadeIn ease 2s; 104 | -webkit-animation-delay: 800ms; 105 | animation-delay: 800ms; 106 | opacity: 0; 107 | animation-fill-mode: forwards; 108 | } 109 | 110 | .Demo { 111 | border-right: 4px solid darkslategray; 112 | padding-right: 75px; 113 | margin-left: 50px; 114 | .query-text-container { 115 | display: flex; 116 | justify-content: center; 117 | label { 118 | display: flex; 119 | flex-direction: column; 120 | h4 { 121 | align-self: center; 122 | } 123 | } 124 | } 125 | animation: fadeIn ease 2000ms; 126 | -webkit-animation: fadeIn ease 2000ms; 127 | -webkit-animation-delay: 120ms; 128 | animation-delay: 120ms; 129 | opacity: 0; 130 | animation-fill-mode: forwards; 131 | } 132 | 133 | .DB-view { 134 | background: $code-editorBG; 135 | color: $code-text; 136 | width: 320px; 137 | height: 30vh; 138 | overflow-y: scroll; 139 | border-radius: 5px; 140 | box-shadow: 0 5px 18px rgba(0, 0, 0, 0.6); 141 | } 142 | 143 | .cache-view { 144 | background: $code-editorBG; 145 | color: $code-text; 146 | width: 320px; 147 | height: 30vh; 148 | overflow-y: scroll; 149 | border-radius: 5px; 150 | box-shadow: 0 5px 18px rgba(0, 0, 0, 0.6); 151 | } 152 | 153 | .fetched-div { 154 | h4 { 155 | text-align: center; 156 | } 157 | margin-top: 10px; 158 | margin-left: 10px; 159 | margin-bottom: 10px; 160 | } 161 | 162 | .cache-div { 163 | h4 { 164 | text-align: center; 165 | } 166 | margin-top: 10px; 167 | margin-bottom: 10px; 168 | } 169 | 170 | .mainOfflineDiv { 171 | display: flex; 172 | margin-right: 50px; 173 | margin-left: 20px; 174 | animation: fadeIn ease 2000ms; 175 | -webkit-animation: fadeIn ease 2000ms; 176 | -webkit-animation-delay: 500ms; 177 | animation-delay: 500ms; 178 | opacity: 0; 179 | animation-fill-mode: forwards; 180 | } 181 | 182 | .navUl { 183 | padding-bottom: 15px; 184 | font-weight: 200; 185 | border-bottom: 2px solid darkslategrey; 186 | 187 | li { 188 | margin-left: 20px; 189 | margin-right: 20px; 190 | font-size: 23px; 191 | 192 | a { 193 | color: #d1d1d1; 194 | text-decoration: none; 195 | } 196 | a:hover { 197 | color: gray; 198 | } 199 | } 200 | } 201 | 202 | .submitButton { 203 | background-color: lightslategray; 204 | color: floralwhite; 205 | border: none; 206 | margin: 5px; 207 | margin-left: 0; 208 | padding: 5px; 209 | border-radius: 5px; 210 | } 211 | 212 | .updateButton { 213 | background-color: gray; 214 | color: floralwhite; 215 | border: none; 216 | margin: 5px; 217 | margin-left: 0px; 218 | padding: 5px; 219 | border-radius: 5px; 220 | } 221 | 222 | .deleteButton { 223 | background-color: darkslategray; 224 | color: floralwhite; 225 | border: none; 226 | margin: 5px; 227 | margin-left: 20px; 228 | padding: 5px; 229 | border-radius: 5px; 230 | } 231 | 232 | .fetchButton { 233 | background-color: gray; 234 | color: floralwhite; 235 | border: none; 236 | margin: 5px; 237 | margin-left: 0; 238 | padding: 10px; 239 | border-radius: 5px; 240 | } 241 | 242 | .overlayRight { 243 | display: flex; 244 | justify-content: flex-end; 245 | } 246 | .overlayLeft { 247 | display: flex; 248 | justify-content: flex-start; 249 | } 250 | 251 | .addFieldToQuery { 252 | background-color: darkslategray; 253 | color: floralwhite; 254 | border: none; 255 | margin: 5px; 256 | margin-left: 0; 257 | padding: 10px; 258 | border-radius: 5px; 259 | } 260 | 261 | .offlineMainDiv { 262 | display: flex; 263 | justify-content: flex-start; 264 | flex-direction: column; 265 | } 266 | 267 | @media (max-width: 1300px) { 268 | .Demo { 269 | margin: 0; 270 | padding-right: 0; 271 | } 272 | } 273 | 274 | @media (max-width: 1048px) { 275 | .Demo { 276 | border-right: none; 277 | } 278 | .mainDemoContainer { 279 | flex-direction: column; 280 | margin-top: 400px; 281 | justify-content: center; 282 | } 283 | .navUl { 284 | li { 285 | margin-left: 5px; 286 | margin-right: 5px; 287 | font-size: 17px; 288 | } 289 | } 290 | } 291 | 292 | .Info_Headers { 293 | margin-top: 5px; 294 | display: flex; 295 | justify-content: center; 296 | flex-direction: column; 297 | } 298 | 299 | .Info_Headers h3 { 300 | width: 700px; 301 | line-height: 25px; 302 | } 303 | 304 | .Info_Headers h4 { 305 | width: 700px; 306 | } 307 | 308 | .Info_Headers p { 309 | width: 500px; 310 | margin-left: 125px; 311 | } 312 | 313 | .Info_Headers img { 314 | flex-direction: column; 315 | width: 700px; 316 | } 317 | 318 | .infoParentDiv { 319 | display: flex; 320 | justify-content: center; 321 | } 322 | -------------------------------------------------------------------------------- /client/components/Demo/Offline.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import axios from "axios"; 3 | 4 | import { useFilamentMutation } from "filamentql/client"; 5 | 6 | import { 7 | getTodosQuery, 8 | addTodoMutation, 9 | deleteTodoMutation, 10 | updateTodoMutation, 11 | } from "../../query"; 12 | 13 | import UpdateTodo from "./UpdateTodo"; 14 | import TodoList from "./TodoList"; 15 | import AddTodo from "./AddTodo"; 16 | 17 | const Offline = () => { 18 | const [updatedText, setUpdated] = useState(""); 19 | const [todoIdToUpdate, setTodoIdToUpdate] = useState(null); 20 | const [wantsUpdate, setWantsUpdate] = useState(false); 21 | const [todos, setTodos] = useState([]); 22 | const [networkMode, setNetworkMode] = useState(""); 23 | 24 | const [callAddTodoMutation, addTodoResponse] = useFilamentMutation( 25 | addTodoMutation, 26 | () => { 27 | console.log(addTodoResponse.addTodo); 28 | setTodos([...todos, addTodoResponse.addTodo]); 29 | } 30 | ); 31 | const [callDeleteTodoMutation] = useFilamentMutation(deleteTodoMutation); 32 | const [callUpdateTodoMutation] = useFilamentMutation(updateTodoMutation); 33 | 34 | useEffect(() => { 35 | if (navigator.onLine) setNetworkMode("Online"); 36 | else setNetworkMode("Offline"); 37 | }, [navigator.onLine]); 38 | 39 | useEffect(() => { 40 | axios 41 | .post("/graphql", { query: getTodosQuery }) 42 | .then((response) => setTodos(response.data.data.todos)); 43 | }, []); 44 | 45 | const handleAddTodo = (value) => { 46 | if (!value) return; 47 | callAddTodoMutation(value); 48 | }; 49 | 50 | const handleDelete = async (id) => { 51 | callDeleteTodoMutation(id); 52 | const filteredTodos = todos.filter((item) => item.id !== id); 53 | setTodos(filteredTodos); 54 | }; 55 | 56 | const handleUpdate = (id, text) => { 57 | setWantsUpdate(true); 58 | setTodoIdToUpdate(id); 59 | setUpdated(text); 60 | }; 61 | 62 | const handleUpdateChange = (e) => setUpdated(e.target.value); 63 | 64 | const handleUpdateTodo = async () => { 65 | callUpdateTodoMutation(todoIdToUpdate, updatedText); 66 | 67 | const updatedTodos = todos.map((todo) => 68 | todo.id === todoIdToUpdate ? { ...todo, text: updatedText } : todo 69 | ); 70 | 71 | setTodos(updatedTodos); 72 | setWantsUpdate(false); 73 | setTodoIdToUpdate(null); 74 | }; 75 | 76 | return ( 77 |
78 |

Offline Mode Caching

79 |

Todos

80 | 81 | 82 | {wantsUpdate && ( 83 | 88 | )} 89 | 90 | 95 | 96 |
97 |

Set Browser Tab to Offline

98 |
    99 |
  • Open Dev Tools
  • 100 |
  • Go to Network Tab
  • 101 |
  • Look for Dropdown 'Online'
  • 102 |
  • Set to 'Offline'
  • 103 |
  • Make a change to the Todo List
  • 104 |
  • Nothing will show up, but it has been added to the cache
  • 105 |
  • 106 | Check this by going to the dev console and typing 'sessionStorage' 107 |
  • 108 |
  • Set Network back to 'Online'
  • 109 |
  • The new todo will show up in the list!
  • 110 |
111 |
112 |
113 | ); 114 | }; 115 | 116 | export default Offline; 117 | -------------------------------------------------------------------------------- /client/components/Demo/TodoList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const OfflineList = ({ todos, handleDelete, handleUpdate }) => ( 4 |
5 | 16 |
17 | ); 18 | 19 | export default OfflineList; 20 | -------------------------------------------------------------------------------- /client/components/Demo/UpdateTodo.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const UpdateForm = ({ handleUpdateTodo, updatedText, handleUpdateChange }) => { 4 | const handleSubmit = (event) => { 5 | event.preventDefault(); 6 | handleUpdateTodo(); 7 | }; 8 | 9 | return ( 10 |
11 |
12 | 13 | 19 |
20 |
21 | ); 22 | }; 23 | 24 | export default UpdateForm; 25 | -------------------------------------------------------------------------------- /client/components/Demo/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import axios from "axios"; 3 | 4 | import { mergeTwoArraysById, parseKeyInCache } from '../../../filament/utils'; 5 | import parseClientFilamentQuery from '../../../filament/parseClientFilamentQuery'; 6 | import Offline from './Offline' 7 | 8 | import './Demo.scss'; 9 | 10 | const query = ` 11 | query { 12 | todos { 13 | id 14 | text 15 | isCompleted 16 | } 17 | } 18 | `; 19 | 20 | const queryWantToMake = ` 21 | query { 22 | todos { 23 | id 24 | text 25 | isCompleted 26 | difficulty 27 | } 28 | } 29 | `; 30 | 31 | sessionStorage.clear(); 32 | 33 | const Demo = () => { 34 | const [cache, setCache] = useState({ ...sessionStorage }); 35 | const [dataFromDB, setDataFromDB] = useState(null); 36 | const [desiredQuery, setDesiredQuery] = useState(query); 37 | const [actualQuery, setActualQuery] = useState(""); 38 | const [fetchingTime, setFetchingTime] = useState(0); 39 | 40 | const keyInCache = parseKeyInCache(query); 41 | 42 | useEffect(() => { 43 | setCache({ ...sessionStorage }); 44 | }, [dataFromDB, sessionStorage]); 45 | 46 | const handleClick = () => { 47 | const [actualQuery, dataInCache] = parseClientFilamentQuery(desiredQuery); 48 | setActualQuery(actualQuery); 49 | 50 | const startTime = performance.now(); 51 | axios.post('/filament', { query: actualQuery, keyInCache }).then((res) => { 52 | const cacheString = sessionStorage.getItem(keyInCache); 53 | if (cacheString) { 54 | const mergedData = mergeTwoArraysById( 55 | JSON.parse(sessionStorage.getItem(keyInCache)), 56 | res.data.data[keyInCache] 57 | ); 58 | 59 | sessionStorage.setItem(keyInCache, JSON.stringify(mergedData)); 60 | } else { 61 | sessionStorage.setItem( 62 | keyInCache, 63 | JSON.stringify(res.data.data[keyInCache]) 64 | ); 65 | } 66 | 67 | const endTime = performance.now(); 68 | setDataFromDB(res.data.data[keyInCache]); 69 | setFetchingTime((endTime - startTime).toFixed(2)); 70 | }); 71 | }; 72 | 73 | const displayCode = (cache) => { 74 | const result = typeof cache === "string" ? JSON.parse(cache) : cache; 75 | return JSON.stringify(result, null, 2); 76 | }; 77 | 78 | const handleUniqueFieldButtonClick = () => { 79 | setDesiredQuery(queryWantToMake); 80 | }; 81 | 82 | return ( 83 |
84 |
90 |
91 |
92 |

FilamentQL Caching and Parsing

93 |
94 |