├── .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 |
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 |
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 |
6 | {todos.map((todo) => (
7 |
8 | {todo.text}
9 | handleDelete(todo.id)}>Delete
10 | handleUpdate(todo.id, todo.text)}>
11 | Update
12 |
13 |
14 | ))}
15 |
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 |
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 |
95 | Desired Query
96 |
103 |
104 |
105 | Actual Query To Be Fetched
106 |
113 |
114 |
115 |
122 |
126 | Add Unique Field to Query
127 |
128 |
133 | Fetch
134 |
135 | {/* Reset */}
136 |
137 |
138 |
139 |
140 |
Data in cache
141 |
142 |
143 | {displayCode(cache[keyInCache] || null)}
144 |
145 |
146 |
147 |
148 |
Data fetched – Took {fetchingTime} ms
149 |
150 |
151 | {displayCode(dataFromDB)}
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 | );
165 | };
166 |
167 | export default Demo;
168 |
--------------------------------------------------------------------------------
/client/components/Info/Info.scss:
--------------------------------------------------------------------------------
1 | .Info_Headers {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: center;
5 | animation: fadeIn ease 2s;
6 | -webkit-animation: fadeIn ease 1500ms;
7 | -webkit-animation-delay: 200ms;
8 | animation-delay: 200ms;
9 | opacity: 0;
10 | animation-fill-mode: forwards;
11 | }
12 |
--------------------------------------------------------------------------------
/client/components/Info/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './Info.scss';
3 |
4 | const Info = () => {
5 | return (
6 |
13 |
14 |
15 |
16 | FilamentQL is a lightweight caching library for GraphQL queries that
17 | utilizes a parsing algorithm to detect differences between incoming
18 | queries and existing data stored within the cache. The library
19 | offers tools for both client and server side caching as well as
20 | tools for offline mode.{' '}
21 |
22 |
23 | FilamentQL's parsing algorithm only selects the fields within the
24 | query that are not in the cache. It then constructs a new query to
25 | be sent to the GraphQL server as well as package up the data from
26 | the cache. Once the data arrives from the database, FilamentQL
27 | combines the data into the same shape that was initially queried.
28 |
29 |
30 | Offline mode is a feature of the FilamentQL library. It offers tools
31 | to store queries and mutations in a queue which will then detect
32 | internet using navigator.onLine. Once detected the queue will then
33 | send its contents, one by one, off to the server.
34 |
35 |
Below is a diagram of our client side caching system.
36 |
37 |
38 | Below is a diagram of our server side caching system which uses a
39 | unique key to give requests access to the correct data within the
40 | cache.
41 |
42 |
43 |
44 |
45 |
46 | );
47 | };
48 |
49 | export default Info;
50 |
--------------------------------------------------------------------------------
/client/components/Main/AddTodo.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | const AddTodo = ({ addTodo }) => {
4 | const [value, setValue] = useState('');
5 |
6 | const handleChange = ({ target: { value } }) => setValue(value);
7 |
8 | const handleSubmit = (event) => {
9 | event.preventDefault();
10 | addTodo(value);
11 | setValue('');
12 | };
13 |
14 | return (
15 |
24 | );
25 | };
26 |
27 | export default AddTodo;
28 |
--------------------------------------------------------------------------------
/client/components/Main/TodoItem.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const TodoItem = ({ id, text, isCompleted, number, toggleTodo }) => (
4 |
5 |
6 | {text} - {number}
7 |
8 |
toggleTodo(id)}>Toggle
9 |
Delete
10 |
11 | );
12 |
13 | export default TodoItem;
14 |
--------------------------------------------------------------------------------
/client/components/Main/TodoList.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import TodoItem from "./TodoItem";
4 |
5 | const TodoList = ({ todos, toggleTodo }) => {
6 | return (
7 |
8 | {todos.map((item) => (
9 |
17 | ))}
18 |
19 | );
20 | };
21 |
22 | export default TodoList;
23 |
--------------------------------------------------------------------------------
/client/components/Main/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { useFilamentQuery } from '../../../filament';
4 |
5 | sessionStorage.clear();
6 |
7 | const query = `
8 | {
9 | todos {
10 | id
11 | text
12 | isCompleted
13 | }
14 | }
15 | `;
16 |
17 | const Main = () => {
18 | const { state, makeQuery } = useFilamentQuery(query, []);
19 | return (
20 |
21 |
FilamentQL
22 |
23 | A GraphQL Library for client, server and offline caching that includes
24 | custom hooks, and a query parsing algorithm.
25 |
26 |
27 | Developed by: Andrew Lovato - Chan Choi - Duy Nguyen - Nelson Wu
28 |
29 |
30 | );
31 | };
32 |
33 | export default Main;
34 |
--------------------------------------------------------------------------------
/client/components/Navigation.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Link } from 'react-router-dom';
4 |
5 | const Navigation = () => {
6 | return (
7 |
8 |
16 |
17 | Home
18 |
19 |
20 | Demo
21 |
22 |
23 | Team
24 |
25 |
26 | Info
27 |
28 |
29 | GitHub
30 |
31 |
32 | NPM Package
33 |
34 |
35 |
36 | );
37 | };
38 |
39 | export default Navigation;
40 |
--------------------------------------------------------------------------------
/client/components/Team/Andrew.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/FilamentQL/d9ba61ff7bab74da0a51da5b281f218990fa26f1/client/components/Team/Andrew.jpg
--------------------------------------------------------------------------------
/client/components/Team/Chan.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/FilamentQL/d9ba61ff7bab74da0a51da5b281f218990fa26f1/client/components/Team/Chan.jpg
--------------------------------------------------------------------------------
/client/components/Team/Duy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/FilamentQL/d9ba61ff7bab74da0a51da5b281f218990fa26f1/client/components/Team/Duy.png
--------------------------------------------------------------------------------
/client/components/Team/Member.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Member = ({ member: { name, avatar, bio, github, linkedIn } }) => (
4 |
5 |
6 |
7 |
8 |
9 |
10 |
22 |
23 |
24 | {name}
25 |
26 | Software Engineer
27 |
28 |
29 |
30 |
31 |
34 |
35 | );
36 |
37 | export default Member;
38 |
--------------------------------------------------------------------------------
/client/components/Team/Nelson.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/FilamentQL/d9ba61ff7bab74da0a51da5b281f218990fa26f1/client/components/Team/Nelson.jpg
--------------------------------------------------------------------------------
/client/components/Team/Team.scss:
--------------------------------------------------------------------------------
1 | /**** Sass Variables ****/
2 | // $bodyFont: 'Open Sans', sans-serif;
3 | $bodyFont: Arial, Helvetica, sans-serif;
4 |
5 | // * {
6 | // padding: 0;
7 | // margin: 0;
8 | // box-sizing: border-box;
9 | // }
10 | .bio {
11 | display: flex;
12 | flex-wrap: wrap;
13 | animation: fadeIn ease 1400ms;
14 | -webkit-animation: fadeIn ease 1400ms;
15 | -webkit-animation-delay: 500ms;
16 | animation-delay: 500ms;
17 | opacity: 0;
18 | animation-fill-mode: forwards;
19 | }
20 |
21 | body {
22 | font-family: $bodyFont;
23 | background-color: #333;
24 | // background-color: #12232e;
25 | // background-image: linear-gradient(to top, #09203f 0%, #537895 100%);
26 | }
27 |
28 | .container {
29 | max-width: 900px;
30 | display: flex;
31 | flex-wrap: wrap;
32 | justify-content: space-evenly;
33 | margin: 0 auto;
34 | animation: fadeIn ease 1400ms;
35 | -webkit-animation: fadeIn ease 1400ms;
36 | -webkit-animation-delay: 310ms;
37 | animation-delay: 310ms;
38 | opacity: 0;
39 | animation-fill-mode: forwards;
40 | }
41 |
42 | .card-wrapper {
43 | width: 350px;
44 | height: 500px;
45 | position: relative;
46 | }
47 |
48 | .card-container {
49 | width: 350px;
50 | }
51 |
52 | .card {
53 | position: absolute;
54 | top: 50%;
55 | left: 50%;
56 | width: 350px;
57 | height: 450px;
58 | transform: translate(-50%, -50%);
59 | border-radius: 16px;
60 | overflow: hidden;
61 | box-shadow: 0 5px 18px rgba(0, 0, 0, 0.6);
62 | cursor: pointer;
63 | transition: 0.5s;
64 |
65 | .card-image {
66 | position: absolute;
67 | top: 0px;
68 | left: 0px;
69 | width: 100%;
70 | height: 100%;
71 | z-index: 2;
72 | background-color: #000;
73 | transition: 0.5s;
74 | }
75 |
76 | &:hover img {
77 | opacity: 0.4;
78 | transition: 0.5s;
79 | }
80 | }
81 |
82 | .card:hover .card-image {
83 | transform: translateY(-100px);
84 | transition: all 0.9s;
85 | }
86 |
87 | .Header {
88 | animation: fadeIn ease 1s;
89 | -webkit-animation: fadeIn ease 1s;
90 | -webkit-animation-delay: 100ms;
91 | animation-delay: 100ms;
92 | opacity: 0;
93 | animation-fill-mode: forwards;
94 | }
95 | /**** Social Icons *****/
96 |
97 | .social-icons {
98 | position: absolute;
99 | top: 50%;
100 | left: 45%;
101 | transform: translate(-50%, -50%);
102 | z-index: 3;
103 | display: flex;
104 |
105 | li {
106 | list-style: none;
107 |
108 | a {
109 | position: relative;
110 | display: block;
111 | width: 50px;
112 | height: 50px;
113 | line-height: 50px;
114 | text-align: center;
115 | background: #fff;
116 | font-size: 23px;
117 | color: #333;
118 | font-weight: bold;
119 | margin: 0 6px;
120 | transition: 0.4s;
121 | transform: translateY(200px);
122 | opacity: 0;
123 | }
124 | }
125 | }
126 |
127 | .card:hover .social-icons li a {
128 | transform: translateY(0px);
129 | opacity: 1;
130 | }
131 |
132 | .social-icons li a:hover {
133 | background: #000;
134 | transition: 0.2s;
135 | .fab {
136 | color: #fff;
137 | }
138 | }
139 |
140 | .social-icons li a .fab {
141 | transition: 0.8s;
142 |
143 | &:hover {
144 | transform: rotateY(360deg);
145 | color: #fff;
146 | }
147 | }
148 |
149 | .card:hover li:nth-child(1) a {
150 | transition-delay: 0.1s;
151 | }
152 | .card:hover li:nth-child(2) a {
153 | transition-delay: 0.2s;
154 | }
155 | .card:hover li:nth-child(3) a {
156 | transition-delay: 0.3s;
157 | }
158 | .card:hover li:nth-child(4) a {
159 | transition-delay: 0.4s;
160 | }
161 |
162 | /**** Personal Details ****/
163 |
164 | .details {
165 | position: absolute;
166 | bottom: 0;
167 | left: -3%;
168 | background: #fff;
169 | width: 100%;
170 | height: 120px;
171 | z-index: 1;
172 | padding: 10px;
173 |
174 | h2 {
175 | margin: 50px 0;
176 | padding: 0;
177 | text-align: center;
178 | color: #333;
179 |
180 | .job-title {
181 | font-size: 1rem;
182 | line-height: 2.5rem;
183 | color: #333;
184 | font-weight: 300;
185 | }
186 | }
187 | }
188 |
189 | @keyframes fadeIn {
190 | 0% {
191 | opacity: 0;
192 | }
193 | 100% {
194 | opacity: 1;
195 | }
196 | }
197 |
198 | @-moz-keyframes fadeIn {
199 | 0% {
200 | opacity: 0;
201 | }
202 | 100% {
203 | opacity: 1;
204 | }
205 | }
206 |
207 | @-webkit-keyframes fadeIn {
208 | 0% {
209 | opacity: 0;
210 | }
211 | 100% {
212 | opacity: 1;
213 | }
214 | }
215 |
216 | @-o-keyframes fadeIn {
217 | 0% {
218 | opacity: 0;
219 | }
220 | 100% {
221 | opacity: 1;
222 | }
223 | }
224 |
225 | @-ms-keyframes fadeIn {
226 | 0% {
227 | opacity: 0;
228 | }
229 | 100% {
230 | opacity: 1;
231 | }
232 | }
233 |
--------------------------------------------------------------------------------
/client/components/Team/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import axios from 'axios';
3 |
4 | import './Team.scss';
5 | import Member from './Member';
6 | import { getMembersQuery } from '../../query';
7 |
8 | const Team = () => {
9 | const [members, setMembers] = useState([]);
10 |
11 | useEffect(() => {
12 | axios.post('/graphql', { query: getMembersQuery }).then((response) => {
13 | const { members } = response.data.data;
14 | setMembers(members);
15 | });
16 | }, []);
17 |
18 | return (
19 |
20 |
28 |
Meet the FilamentQL Team
29 |
30 |
31 | {members.map((member) => (
32 |
33 | ))}
34 |
35 |
36 | );
37 | };
38 |
39 | export default Team;
40 |
--------------------------------------------------------------------------------
/client/hooks/index.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | export const useInput = (defaultInput = '') => {
4 | const [input, setInput] = useState(defaultInput);
5 | const handleChange = ({ target: { value } }) => setInput(value);
6 | return [input, setInput, handleChange];
7 | };
8 |
--------------------------------------------------------------------------------
/client/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { render } from "react-dom";
3 | import { HashRouter as Router } from "react-router-dom";
4 |
5 | import App from "./components/App";
6 |
7 | render(
8 |
9 |
10 | ,
11 | document.getElementById("root")
12 | );
13 |
--------------------------------------------------------------------------------
/client/query/index.js:
--------------------------------------------------------------------------------
1 | export const getTodosQuery = `
2 | query {
3 | todos {
4 | id
5 | text
6 | isCompleted
7 | }
8 | }
9 | `;
10 |
11 | export const getMembersQuery = `
12 | query {
13 | members {
14 | id
15 | name
16 | avatar
17 | bio
18 | github
19 | linkedIn
20 | }
21 | }
22 | `;
23 |
24 | export const addTodoMutation = (value) => `
25 | mutation {
26 | addTodo(input: { text: "${value}" }){
27 | id
28 | text
29 | }
30 | }
31 | `;
32 |
33 | export const deleteTodoMutation = (id) => `
34 | mutation {
35 | deleteTodo(input: { id: "${id}" }) {
36 | id
37 | }
38 | }
39 | `;
40 |
41 | export const updateTodoMutation = (id, text) => `
42 | mutation {
43 | updateTodo(input: { id: "${id}" , text: "${text}" }){
44 | id
45 | text
46 | }
47 | }
48 | `;
49 |
--------------------------------------------------------------------------------
/dump.rdb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/FilamentQL/d9ba61ff7bab74da0a51da5b281f218990fa26f1/dump.rdb
--------------------------------------------------------------------------------
/fake-database.json:
--------------------------------------------------------------------------------
1 | {
2 | "todos": [
3 | {
4 | "id": "1",
5 | "text": "Build GraphQL server",
6 | "isCompleted": false,
7 | "number": 1
8 | },
9 | {
10 | "id": "2",
11 | "text": "Build Frontend App",
12 | "isCompleted": true,
13 | "number": 12
14 | },
15 | {
16 | "id": "3",
17 | "text": "Develop caching system",
18 | "isCompleted": false,
19 | "number": 15
20 | }
21 | ]
22 | }
--------------------------------------------------------------------------------
/filament/constants/index.js:
--------------------------------------------------------------------------------
1 | export const FILAMENT_ROUTE = '/filament';
2 | export const GRAPHQL_ROUTE = '/graphql';
3 | export const GRAPHQL_ROUTE_FROM_SERVER = 'http://localhost:4000/graphql';
4 |
--------------------------------------------------------------------------------
/filament/filamentMiddleware.js:
--------------------------------------------------------------------------------
1 | const axios = require('axios');
2 |
3 | import { GRAPHQL_ROUTE_FROM_SERVER } from './constants';
4 | import { mergeTwoArraysById, transformQuery } from './utils';
5 | import parseServerFilamentQuery from './parseServerFilamentQuery';
6 |
7 | const filamentMiddlewareWrapper = (client) => async (req, res) => {
8 | const clientIP =
9 | req.headers['x-forwarded-for'] || req.connection.remoteAddress;
10 | const { query, keyInCache } = req.body;
11 |
12 | client.get(clientIP, async (err, redisCacheAtIP) => {
13 | // clientIP not found in cache
14 | console.log('redisCacheAtIP: ', redisCacheAtIP);
15 | if (!redisCacheAtIP) {
16 | try {
17 | const resFromGraphQL = await axios.post(GRAPHQL_ROUTE_FROM_SERVER, {
18 | query,
19 | });
20 |
21 | client.set(
22 | clientIP,
23 | JSON.stringify({
24 | [keyInCache]: resFromGraphQL.data.data[keyInCache],
25 | }),
26 | (err, redisCacheAtIPAfterWrite) => {
27 | console.log('redisCacheAtIP after write', redisCacheAtIPAfterWrite);
28 | }
29 | );
30 |
31 | const { data } = resFromGraphQL.data;
32 |
33 | return res.status(200).json({ data });
34 | } catch (err) {
35 | console.log('error', err);
36 | }
37 | return;
38 | }
39 |
40 | // clientIP found in cache
41 | const redisCacheParsed = JSON.parse(redisCacheAtIP);
42 | const transformedQuery = transformQuery(query);
43 | const [parsedQuery, dataInRedisCache, isMatched] = parseServerFilamentQuery(
44 | transformedQuery,
45 | redisCacheParsed
46 | );
47 |
48 | if (isMatched) return res.status(200).json({ data: dataInRedisCache });
49 |
50 | // isMatched === false
51 | try {
52 | const response = await axios.post(GRAPHQL_ROUTE_FROM_SERVER, {
53 | query: parsedQuery,
54 | });
55 | const resTodos = mergeTwoArraysById(
56 | dataInRedisCache[keyInCache],
57 | response.data.data[keyInCache]
58 | );
59 | // set the new data in Redis
60 | const cacheTodos = mergeTwoArraysById(
61 | JSON.parse(redisCacheAtIP)[keyInCache],
62 | response.data.data[keyInCache]
63 | );
64 |
65 | client.set(
66 | clientIP,
67 | JSON.stringify({
68 | [keyInCache]: cacheTodos,
69 | })
70 | );
71 |
72 | const dataSendToClient = {
73 | [keyInCache]: resTodos,
74 | };
75 |
76 | return res.status(200).json({ data: dataSendToClient });
77 | } catch (err) {
78 | console.log(err);
79 | }
80 | });
81 | };
82 |
83 | export default filamentMiddlewareWrapper;
84 |
--------------------------------------------------------------------------------
/filament/hooks/index.jsx:
--------------------------------------------------------------------------------
1 | import useFilamentQuery from './useFilamentQuery';
2 | import useFilamentMutation from './useFilamentMutation';
3 |
4 | export { useFilamentQuery, useFilamentMutation };
5 |
--------------------------------------------------------------------------------
/filament/hooks/useFilamentMutation.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useRef } from 'react';
2 | import axios from 'axios';
3 |
4 | import { GRAPHQL_ROUTE } from '../constants';
5 | import { uniqueId } from '../utils';
6 |
7 | const saveDataToCache = (key, value) =>
8 | sessionStorage.setItem(key, JSON.stringify(value));
9 |
10 | const getDataFromCache = (key) => JSON.parse(sessionStorage.getItem(key));
11 |
12 | const useFilamentMutation = (mutation, callback) => {
13 | const [state, setState] = useState(null);
14 | const [intervalId, setIntervalId] = useState(null);
15 | const { current: mutationId } = useRef(uniqueId());
16 |
17 | const offlineQueueKey = mutationId + '_offline_queue';
18 | const intervalIdKey = mutationId + '_interval';
19 |
20 | // Run callback passed in by `useFilamentMutation`
21 | useEffect(() => {
22 | if (state && callback) callback();
23 | }, [state]);
24 |
25 | // Run once
26 | useEffect(() => {
27 | initializeOfflineQueue();
28 | startOfflineInterval();
29 | return () => clearInterval(intervalId);
30 | }, []);
31 |
32 | const initializeOfflineQueue = () => {
33 | const offlineQueue = getDataFromCache(offlineQueueKey);
34 | if (!offlineQueue) saveDataToCache(offlineQueueKey, []);
35 | };
36 |
37 | const startOfflineInterval = () => {
38 | let offlineIntervalId = getDataFromCache(intervalIdKey);
39 | if (offlineIntervalId) return;
40 |
41 | offlineIntervalId = setInterval(processOfflineQueue, 1000);
42 | saveDataToCache(intervalIdKey, offlineIntervalId);
43 | setIntervalId(offlineIntervalId);
44 | };
45 |
46 | const processOfflineQueue = async () => {
47 | const offlineQueue = getDataFromCache(offlineQueueKey);
48 |
49 | while (navigator.onLine && offlineQueue.length) {
50 | const mutation = offlineQueue.shift();
51 | const response = await axios.post(GRAPHQL_ROUTE, { query: mutation });
52 | setState(response.data.data);
53 | }
54 |
55 | saveDataToCache(offlineQueueKey, offlineQueue);
56 | };
57 |
58 | const makeMutation = async (...args) => {
59 | // online
60 | if (navigator.onLine) {
61 | const response = await axios.post(GRAPHQL_ROUTE, {
62 | query: mutation(...args),
63 | });
64 | setState(response.data.data);
65 | return;
66 | }
67 |
68 | // offline
69 | const offlineQueue = getDataFromCache(offlineQueueKey);
70 | offlineQueue.push(mutation(...args));
71 | saveDataToCache(offlineQueueKey, offlineQueue);
72 | };
73 |
74 | return [makeMutation, state];
75 | };
76 |
77 | export default useFilamentMutation;
78 |
--------------------------------------------------------------------------------
/filament/hooks/useFilamentQuery.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import axios from 'axios';
3 |
4 | import { FILAMENT_ROUTE } from '../constants';
5 | import parseClientFilamentQuery from '../parseClientFilamentQuery';
6 | import {
7 | parseKeyInCache,
8 | mergeTwoArraysById,
9 | getSessionStorageKey,
10 | } from '../utils';
11 |
12 | const useFilamentQuery = (query, defaultState = null) => {
13 | const [state, setState] = useState(defaultState);
14 | const keyInCache = parseKeyInCache(query);
15 |
16 | useEffect(() => {
17 | const key = getSessionStorageKey(query);
18 | const cacheAtKey = sessionStorage.getItem(key);
19 |
20 | // if data is in the cache, return it
21 | if (cacheAtKey) {
22 | const newState = JSON.parse(cacheAtKey);
23 | setState(newState);
24 | } else {
25 | // otherwise, make a axios request
26 |
27 | axios.post(FILAMENT_ROUTE, { query, keyInCache }).then((res) => {
28 | setState(res.data.data);
29 | sessionStorage.setItem(key, JSON.stringify(res.data.data[key]));
30 | });
31 | }
32 | }, []);
33 |
34 | const makeQuery = (query) => {
35 | const key = getSessionStorageKey(query);
36 | const cacheAtKey = sessionStorage.getItem(key);
37 |
38 | if (cacheAtKey) {
39 | const [finalQuery, cacheData] = parseClientFilamentQuery(query);
40 |
41 | // note: parsing for dissimilarities later
42 | axios
43 | .post(FILAMENT_ROUTE, {
44 | query: finalQuery,
45 | keyInCache,
46 | })
47 | .then((res) => {
48 | const cacheAtKeyState = JSON.parse(cacheAtKey);
49 | // Merge with data from server
50 | const newState = mergeTwoArraysById(
51 | cacheAtKeyState,
52 | res.data.data[key]
53 | );
54 |
55 | setState(newState);
56 | sessionStorage.setItem(key, JSON.stringify(newState));
57 | });
58 | } else {
59 | axios.post(FILAMENT_ROUTE, { query }).then((res) => {
60 | setState(res.data.data);
61 | sessionStorage.setItem(key, JSON.stringify(res.data.data[key]));
62 | });
63 | }
64 | };
65 |
66 | return { state, makeQuery };
67 | };
68 |
69 | export default useFilamentQuery;
70 |
--------------------------------------------------------------------------------
/filament/index.js:
--------------------------------------------------------------------------------
1 | import { useFilamentQuery, useFilamentMutation } from './hooks';
2 | import filamentMiddleware from './filamentMiddleware';
3 |
4 | export default filamentMiddleware;
5 | export { useFilamentQuery, useFilamentMutation };
6 |
--------------------------------------------------------------------------------
/filament/parseClientFilamentQuery.js:
--------------------------------------------------------------------------------
1 | function parseClientFilamentQuery(query) {
2 | const cacheData = {};
3 | const tempTypes = [];
4 | const tempTypesForCacheData = [];
5 | const charFindRegex = /[a-zA-Z]/;
6 | let index = 0;
7 | let newQuery = '';
8 | let current = cacheData;
9 | let bracketCount = 0;
10 | let variableForFilter;
11 | let tempCacheObject = {};
12 | let totalTypes = 0;
13 | let keyString = '';
14 | let inputObject = '';
15 | let holdVarString = '';
16 | let typeMatchedWithVariable = '';
17 | let variableAffectedArray = null;
18 | let searchLimiter = 1;
19 | let typeNeedsAdding;
20 |
21 | const foundBracketOrQ = findFirstLetterOrBracket();
22 |
23 | if (foundBracketOrQ === 'q') {
24 | parseTheWordQuery()
25 | parseIfNameOrVariable()
26 | parseOpeningBracket()
27 | }
28 | findNextCharacterAndParse()
29 | return [newQuery, cacheData];
30 |
31 | function findNextCharacterAndParse() {
32 | if (bracketCount === 0) return;
33 | eatWhiteSpace();
34 | createKeyString();
35 | const isFinalBracket = addClosingBracketIfFound()
36 | if (isFinalBracket) return true;
37 | findOpeningCurlyBracketAfterField();
38 |
39 | if (
40 | currElementIsOpeningBracket() &&
41 | bracketCountIsOne() &&
42 | !typeMatchedWithVariable
43 | ) {
44 | getFromCacheOrAddToQuery();
45 | } else if (
46 | currElementIsOpeningBracket() &&
47 | bracketCountNotOne() &&
48 | !typeMatchedWithVariable
49 | ) {
50 | getFromTCOAndNestNewData();
51 | } else if (
52 | currElementIsOpeningBracket() &&
53 | bracketCountIsOne() &&
54 | typeMatchedWithVariable
55 | ) {
56 | filterCachePropertyByVariable();
57 | } else if (
58 | currElementIsOpeningBracket() &&
59 | bracketCountNotOne() &&
60 | typeMatchedWithVariable
61 | ) {
62 | filterTCOPropertyByVariable();
63 | } else if (keyString) {
64 | fieldsInCacheOrNot();
65 | }
66 |
67 | index += 1;
68 | findNextCharacterAndParse();
69 | }
70 |
71 | function fieldsInCacheOrNot() {
72 | if (tempCacheObject[keyString.trim()]) {
73 | addStoredTypesToReturnedDataFromCache();
74 | } else if (!tempCacheObject[keyString.trim()]) {
75 | addFieldToQueryString();
76 | }
77 | }
78 |
79 | function addStoredTypesToReturnedDataFromCache() {
80 | tempTypes.forEach((type) => {
81 | tempTypesForCacheData.push(type.trim());
82 | });
83 | addTypesAndFieldsToCacheObject();
84 | }
85 |
86 | function addTypesAndFieldsToCacheObject() {
87 | if (keyString.trim() === 'id') {
88 | addFieldToQueryString();
89 | keyString = 'id';
90 | }
91 |
92 | let tempCacheData = {};
93 | let currentType = tempTypesForCacheData.shift();
94 | let tempTypeString = sessionStorage.getItem(currentType);
95 | tempCacheData[currentType] = JSON.parse(tempTypeString);
96 | let dataFromCacheArr = tempCacheData[currentType];
97 |
98 | if (tempTypesForCacheDataHasNoLength()) {
99 | if (variableExistsAndMatchesCurrentType()) {
100 | updateDataFromCacheArrFromFiltered()
101 | }
102 | if (!cacheData[currentType]) {
103 | cacheData[currentType] = Array.from(
104 | { length: dataFromCacheArr.length },
105 | (i) => (i = {})
106 | );
107 | let cacheDataArr = cacheData[currentType];
108 |
109 | current = addFields(currentType, dataFromCacheArr, cacheDataArr);
110 | } else {
111 | let cacheDataArr = current;
112 | current = addFields(currentType, dataFromCacheArr, cacheDataArr);
113 | }
114 | } else {
115 | updateNestedCacheDataArrAndCurrent()
116 | }
117 | keyString = '';
118 | }
119 |
120 | function addFields(currentType, dataFromCacheArr, cacheDataArr) {
121 | const newCacheDataArr = [];
122 | for (let i = 0; i < dataFromCacheArr.length; i += 1) {
123 | let tempData = dataFromCacheArr[i];
124 | let data = cacheDataArr[i];
125 |
126 | data[keyString.trim()] = tempData[keyString.trim()];
127 | newCacheDataArr.push(data);
128 | }
129 | cacheData[currentType] = newCacheDataArr;
130 | return newCacheDataArr;
131 | }
132 |
133 | function addNestedFields(currentType, dataFromCacheArr, cacheDataArr) {
134 | const newCacheDataArr = [];
135 |
136 | for (let i = 0; i < dataFromCacheArr.length; i += 1) {
137 | if (variableForFilter && variableForFilter === currentType) {
138 | let variableKey = Object.keys(inputObject);
139 |
140 | dataFromCacheArr = dataFromCacheArr.filter((obj) => {
141 | return obj[variableKey[0]] === inputObject[variableKey[0]];
142 | });
143 | }
144 | let tempData = dataFromCacheArr[i];
145 | let data = cacheDataArr[i];
146 |
147 | if (tempTypesForCacheData.length) {
148 | currentType = tempTypesForCacheData.shift();
149 | if (data[currentType]) {
150 | const returnedArr = addNestedFields(
151 | currentType,
152 | tempData[currentType],
153 | data[currentType]
154 | );
155 | newCacheDataArr.push(returnedArr);
156 | tempTypesForCacheData.unshift(currentType);
157 | } else {
158 | data[currentType] = Array.from(
159 | { length: dataFromCacheArr.length },
160 | (i) => (i = {})
161 | );
162 | const returnedArr = addNestedFields(
163 | currentType,
164 | tempData[currentType],
165 | data[currentType]
166 | );
167 | newCacheDataArr.push(returnedArr);
168 | tempTypesForCacheData.unshift(currentType);
169 | }
170 | } else {
171 | data[keyString.trim()] = tempData[keyString.trim()];
172 | newCacheDataArr.push(data);
173 | }
174 | }
175 | return newCacheDataArr;
176 | }
177 |
178 | function updateNestedCacheDataArrAndCurrent() {
179 | const cacheDataArr = current;
180 | current = addNestedFields(currentType, dataFromCacheArr, cacheDataArr);
181 | }
182 |
183 | function updateDataFromCacheArrFromFiltered() {
184 | let variableKey = Object.keys(inputObject);
185 |
186 | dataFromCacheArr = dataFromCacheArr.filter((obj) => {
187 | return obj[variableKey[0]] === inputObject[variableKey[0]];
188 | });
189 | }
190 |
191 | function addFieldToQueryString() {
192 | if (
193 | typeNeedsAdding &&
194 | typeMatchedWithVariable === tempTypes[tempTypes.length - 1]
195 | ) {
196 | newQuery += tempTypes[tempTypes.length - 1];
197 | newQuery += holdVarString;
198 | newQuery += ' {' + ' id ';
199 | bracketCount += 1;
200 | totalTypes += 1;
201 | holdVarString = '';
202 | typeMatchedWithVariable = '';
203 | typeNeedsAdding = false;
204 | } else if (typeNeedsAdding) {
205 | newQuery += tempTypes[tempTypes.length - 1] + ' {' + ' id ';
206 | totalTypes += 1;
207 | bracketCount += 1;
208 | typeNeedsAdding = false;
209 | } else if (keyString.trim() !== 'id') {
210 | newQuery += keyString;
211 | keyString = '';
212 | }
213 |
214 | keyString = '';
215 | }
216 |
217 | function filterTCOPropertyByVariable() {
218 | if (variableMatchedTypeInTempCache()) {
219 | updateTempCacheObject()
220 | } else {
221 | addToTotalTypesNewQuery()
222 | }
223 | }
224 |
225 | function updateTempCacheObject() {
226 | tempTypes.push(typeMatchedWithVariable);
227 | typeNeedsAdding = true;
228 | variableAffectedObject = tempCacheObject[typeMatchedWithVariable.trim()];
229 | let variableKey = Object.keys(inputObject);
230 | tempArray = variableAffectedObject.filter((obj) => {
231 | return obj[variableKey[0]] === inputObject[variableKey[0]];
232 | });
233 | tempCacheObject = tempArray[0];
234 | }
235 |
236 | function addToTotalTypesNewQuery() {
237 | totalTypes += 1;
238 | newQuery += keyString + typeMatchedWithVariable + ' {' + ' id ';
239 | }
240 |
241 | function filterCachePropertyByVariable() {
242 | if (sessionStorage.getItem(keyString.trim())) {
243 | let tempString = sessionStorage.getItem(keyString.trim());
244 |
245 | variableAffectedArray = JSON.parse(tempString);
246 | let variableKey = Object.keys(inputObject);
247 |
248 | let tempArray = variableAffectedArray.filter((obj) => {
249 | return obj[variableKey[0]] === inputObject[variableKey[0]];
250 | });
251 |
252 | tempCacheObject = tempArray[0];
253 | tempTypes.push(typeMatchedWithVariable);
254 | typeNeedsAdding = true;
255 |
256 | keyString = '';
257 | } else {
258 | totalTypes += 1;
259 | newQuery += keyString + typeMatchedWithVariable + ' {' + ' id ';
260 | keyString = '';
261 | }
262 | }
263 |
264 | function getFromTCOAndNestNewData() {
265 | if (keyStringIsInTempCacheObject()) {
266 | nestNewDataFromTempCacheObject()
267 | } else {
268 | updateGlobalVariablesIfNotFound()
269 | }
270 | }
271 | function keyStringIsInTempCacheObject() {
272 | return tempCacheObject[keyString.trim()] &&
273 | tempCacheObject[keyString.trim()][0]
274 | }
275 |
276 | function nestNewDataFromTempCacheObject() {
277 | tempCacheObject = tempCacheObject[keyString.trim()][0];
278 | tempTypes.push(keyString);
279 | typeNeedsAdding = true;
280 |
281 | keyString = '';
282 | }
283 |
284 | function updateGlobalVariablesIfNotFound() {
285 | totalTypes += 1;
286 | newQuery += keyString + ' {' + ' id ';
287 | keyString = '';
288 | bracketCount += 1;
289 | }
290 |
291 | function getFromCacheOrAddToQuery() {
292 | if (sessionStorageHasItem()) {
293 | updateTempCacheObjectFromSessionStorage()
294 | } else {
295 | updateGlobalVariablesIfNotFound()
296 | }
297 | }
298 |
299 | function updateTempCacheObjectFromSessionStorage() {
300 | let tempString = sessionStorage.getItem(keyString.trim());
301 | tempCacheObject = JSON.parse(tempString)[0];
302 | tempTypes.push(keyString);
303 | typeNeedsAdding = true;
304 | keyString = '';
305 | }
306 |
307 | function findOpeningCurlyBracketAfterField() {
308 | while (currElementIsntOpeningBracket() && searchLimiter > 0) {
309 | index += 1;
310 | searchLimiter -= 1;
311 | }
312 | searchLimiter = 1;
313 | }
314 |
315 | function addClosingBracketIfFound() {
316 | let isFinalBracket = false;
317 | if (currElementIsClosingBracket()) {
318 | if (bracketCount) {
319 | newQuery += '} ';
320 | totalTypes -= 1;
321 | tempCacheObject = {};
322 | bracketCount -= 1;
323 | }
324 |
325 | if (bracketCount === 0) {
326 | isFinalBracket = true;
327 | return isFinalBracket;
328 | }
329 |
330 | index += 1;
331 | }
332 |
333 | return isFinalBracket;
334 | }
335 |
336 |
337 | function createKeyString() {
338 | if (currElementIsAChar()) {
339 | while (currElementIsntASpace()) {
340 | if (query[index] === '(') {
341 | parseAndHoldVarLocation();
342 | break;
343 | }
344 |
345 |
346 | keyString += query[index];
347 | index += 1;
348 | // addElementAndIncrementIndex()
349 | }
350 | }
351 | }
352 |
353 | function parseAndHoldVarLocation() {
354 | typeMatchedWithVariable = keyString;
355 | variableForFilter = keyString;
356 |
357 | while (query[index] !== ')') {
358 | holdVarString += query[index];
359 | index += 1;
360 | }
361 |
362 | holdVarString += query[index];
363 | index += 1;
364 | }
365 |
366 | function eatWhiteSpace() {
367 | while (query[index] === ' ' || query[index] === '\n') {
368 | index += 1;
369 | }
370 | }
371 | // --------------------------
372 |
373 | // General Helper Functions
374 | function variableExistsAndMatchesCurrentType() {
375 | return variableForFilter && variableForFilter === currentType
376 | }
377 | function tempTypesForCacheDataHasNoLength() {
378 | return !tempTypesForCacheData.length
379 | }
380 | function sessionStorageHasItem() {
381 | return sessionStorage.getItem(keyString.trim())
382 | }
383 |
384 | function variableMatchedTypeInTempCache() {
385 | return tempCacheObject[typeMatchedWithVariable.trim()]
386 | }
387 | function bracketCountNotOne() {
388 | return bracketCount !== 1;
389 | }
390 |
391 | function bracketCountIsOne() {
392 | return bracketCount === 1
393 | }
394 |
395 | function currElementIsClosingBracket() {
396 | return query[index] === '}';
397 | }
398 |
399 | function currElementIsntASpace() {
400 | return query[index] !== ' ';
401 | }
402 |
403 | function addElementAndIncrementIndex() {
404 | newQuery += query[index];
405 | index += 1;
406 | }
407 |
408 | function parseOpeningBracket() {
409 | if (currElementIsOpeningBracket()) {
410 | newQuery += query[index] + ' ';
411 | index += 1;
412 | bracketCount += 1;
413 | }
414 | }
415 |
416 | function currElementIsOpeningBracket() {
417 | return query[index] === '{';
418 | }
419 |
420 | function currElementIsntOpeningBracket() {
421 | return query[index] !== '{';
422 | }
423 |
424 | function currElementIsASpace() {
425 | return query[index] === ' '
426 | }
427 |
428 | function currElementIsAChar() {
429 | return query[index].match(charFindRegex)
430 | }
431 |
432 | // Parse Names And Variables
433 | function parseIfNameOrVariable() {
434 |
435 | if (currElementIsAChar()) {
436 | parseName();
437 |
438 | if (currElementIsASpace()) {
439 | index += 1;
440 | } else {
441 | return parseVariable();
442 | }
443 | }
444 | }
445 |
446 | function parseName() {
447 | while (query[index] !== '(' || /\s/.test(query[index])) {
448 | addElementAndIncrementIndex()
449 | }
450 | }
451 |
452 | function parseVariable() {
453 | let inputObjectString = '';
454 | while (currElementIsntOpeningBracket()) {
455 | addElementAndIncrementIndex()
456 | }
457 |
458 | while (query[index] !== '}') {
459 | inputObjectString += query[index];
460 | addElementAndIncrementIndex()
461 | }
462 |
463 | inputObjectString += query[index];
464 | newQuery += query[index];
465 | inputObject = JSON.parse(inputObjectString);
466 |
467 | while (query[index] !== ')') {
468 | addElementAndIncrementIndex()
469 | }
470 |
471 | newQuery += query[index];
472 | index += 2;
473 | }
474 |
475 | function parseTheWordQuery() {
476 | while (query[index] !== ' ') {
477 | addElementAndIncrementIndex()
478 | }
479 | addElementAndIncrementIndex()
480 | }
481 |
482 | function findFirstLetterOrBracket() {
483 | let foundBracketOrQ = 'q';
484 |
485 | while (query[index] !== 'q') {
486 | if (currElementIsOpeningBracket()) {
487 | parseOpeningBracket()
488 | foundBracketOrQ = 'bracket';
489 | break;
490 | }
491 |
492 | index += 1;
493 | }
494 |
495 | return foundBracketOrQ;
496 | }
497 | };
498 |
499 | export default parseClientFilamentQuery
500 |
501 |
502 |
--------------------------------------------------------------------------------
/filament/parseServerFilamentQuery.js:
--------------------------------------------------------------------------------
1 | function serverFilamentQuery(query, cacheObject) {
2 | const cacheData = {};
3 | const tempTypes = [];
4 | const tempTypesForCacheData = [];
5 | const charFindRegex = /[a-zA-Z]/;
6 | let index = 0;
7 | let newQuery = '';
8 | let current = cacheData;
9 | let bracketCount = 0;
10 | let variableForFilter;
11 | let tempCacheObject = {};
12 | let totalTypes = 0;
13 | let keyString = '';
14 | let inputObject = '';
15 | let holdVarString = '';
16 | let typeMatchedWithVariable = '';
17 | let variableAffectedArray = null;
18 | let searchLimiter = 1;
19 | let typeNeedsAdding;
20 | let isMatched = true;
21 |
22 | const foundBracketOrQ = findFirstLetterOrBracket();
23 | if (foundBracketOrQ === 'q') {
24 | parseTheWordQuery()
25 | parseIfNameOrVariable()
26 | parseOpeningBracket()
27 | }
28 | findNextCharacterAndParse()
29 | return [newQuery, cacheData, isMatched];
30 |
31 | function findNextCharacterAndParse() {
32 |
33 | if (bracketCount === 0) return;
34 |
35 | eatWhiteSpace();
36 | createKeyString();
37 | const isFinalBracket = addClosingBracketIfFound()
38 | if (isFinalBracket) return true;
39 |
40 | findOpeningCurlyBracketAfterField();
41 |
42 | if (
43 | currElementIsOpeningBracket() &&
44 | bracketCountIsOne() &&
45 | !typeMatchedWithVariable
46 | ) {
47 | getFromCacheOrAddToQuery();
48 | } else if (
49 | currElementIsOpeningBracket() &&
50 | bracketCountNotOne() &&
51 | !typeMatchedWithVariable
52 | ) {
53 | getFromTCOAndNestNewData();
54 | } else if (
55 | currElementIsOpeningBracket() &&
56 | bracketCountIsOne() &&
57 | typeMatchedWithVariable
58 | ) {
59 | filterCachePropertyByVariable();
60 | } else if (
61 | currElementIsOpeningBracket() &&
62 | bracketCountNotOne() &&
63 | typeMatchedWithVariable
64 | ) {
65 | filterTCOPropertyByVariable();
66 | } else if (keyString) {
67 | fieldsInCacheOrNot();
68 | }
69 | index += 1;
70 |
71 | findNextCharacterAndParse();
72 | }
73 |
74 | function fieldsInCacheOrNot() {
75 | if (tempCacheObject[keyString.trim()]) {
76 |
77 | addStoredTypesToReturnedDataFromCache();
78 | } else if (!tempCacheObject[keyString.trim()]) {
79 | addFieldToQueryString();
80 | }
81 | }
82 |
83 | function addStoredTypesToReturnedDataFromCache() {
84 | tempTypes.forEach((type) => {
85 | tempTypesForCacheData.push(type.trim());
86 | });
87 |
88 |
89 | addTypesAndFieldsToCacheObject();
90 | }
91 |
92 | function addTypesAndFieldsToCacheObject() {
93 | if (keyString.trim() === 'id') {
94 | addFieldToQueryString();
95 | keyString = 'id';
96 | }
97 |
98 | let tempCacheData = {};
99 | let currentType = tempTypesForCacheData.shift();
100 | tempCacheData[currentType] = cacheObject[currentType];
101 |
102 | let dataFromCacheArr = tempCacheData[currentType];
103 |
104 | if (tempTypesForCacheDataHasNoLength()) {
105 | if (variableExistsAndMatchesCurrentType()) {
106 | updateDataFromCacheArrFromFiltered()
107 | }
108 | if (!cacheData[currentType]) {
109 | cacheData[currentType] = Array.from(
110 | { length: dataFromCacheArr.length },
111 | (i) => (i = {})
112 | );
113 | let cacheDataArr = cacheData[currentType];
114 |
115 | current = addFields(currentType, dataFromCacheArr, cacheDataArr);
116 | } else {
117 | let cacheDataArr = current;
118 | current = addFields(currentType, dataFromCacheArr, cacheDataArr);
119 | }
120 | } else {
121 | updateNestedCacheDataArrAndCurrent()
122 | }
123 | keyString = '';
124 | }
125 |
126 | function addFields(currentType, dataFromCacheArr, cacheDataArr) {
127 | const newCacheDataArr = [];
128 | for (let i = 0; i < dataFromCacheArr.length; i += 1) {
129 | let tempData = dataFromCacheArr[i];
130 | let data = cacheDataArr[i];
131 |
132 | data[keyString.trim()] = tempData[keyString.trim()];
133 | newCacheDataArr.push(data);
134 | }
135 | cacheData[currentType] = newCacheDataArr;
136 | return newCacheDataArr;
137 | }
138 |
139 | function addNestedFields(currentType, dataFromCacheArr, cacheDataArr) {
140 | const newCacheDataArr = [];
141 |
142 | for (let i = 0; i < dataFromCacheArr.length; i += 1) {
143 | if (variableForFilter && variableForFilter === currentType) {
144 | let variableKey = Object.keys(inputObject);
145 |
146 | dataFromCacheArr = dataFromCacheArr.filter((obj) => {
147 | return obj[variableKey[0]] === inputObject[variableKey[0]];
148 | });
149 | }
150 | let tempData = dataFromCacheArr[i];
151 | let data = cacheDataArr[i];
152 |
153 | if (tempTypesForCacheData.length) {
154 | currentType = tempTypesForCacheData.shift();
155 | if (data[currentType]) {
156 | const returnedArr = addNestedFields(
157 | currentType,
158 | tempData[currentType],
159 | data[currentType]
160 | );
161 | newCacheDataArr.push(returnedArr);
162 | tempTypesForCacheData.unshift(currentType);
163 | } else {
164 | data[currentType] = Array.from(
165 | { length: dataFromCacheArr.length },
166 | (i) => (i = {})
167 | );
168 | const returnedArr = addNestedFields(
169 | currentType,
170 | tempData[currentType],
171 | data[currentType]
172 | );
173 | newCacheDataArr.push(returnedArr);
174 | tempTypesForCacheData.unshift(currentType);
175 | }
176 | } else {
177 | data[keyString.trim()] = tempData[keyString.trim()];
178 | newCacheDataArr.push(data);
179 | }
180 | }
181 | return newCacheDataArr;
182 | }
183 |
184 | function updateNestedCacheDataArrAndCurrent() {
185 | const cacheDataArr = current;
186 | current = addNestedFields(currentType, dataFromCacheArr, cacheDataArr);
187 | }
188 |
189 | function updateDataFromCacheArrFromFiltered() {
190 | let variableKey = Object.keys(inputObject);
191 |
192 | dataFromCacheArr = dataFromCacheArr.filter((obj) => {
193 | return obj[variableKey[0]] === inputObject[variableKey[0]];
194 | });
195 | }
196 |
197 | function addFieldToQueryString() {
198 | if (
199 | typeNeedsAdding &&
200 | typeMatchedWithVariable === tempTypes[tempTypes.length - 1]
201 | ) {
202 | newQuery += tempTypes[tempTypes.length - 1];
203 | newQuery += holdVarString;
204 | newQuery += ' {' + ' id ';
205 | bracketCount += 1;
206 | totalTypes += 1;
207 | holdVarString = '';
208 | typeMatchedWithVariable = '';
209 | typeNeedsAdding = false;
210 | } else if (typeNeedsAdding) {
211 | newQuery += tempTypes[tempTypes.length - 1] + ' {' + ' id ';
212 | totalTypes += 1;
213 | bracketCount += 1;
214 | typeNeedsAdding = false;
215 | } else if (keyString.trim() !== 'id') {
216 | newQuery += keyString.trim();
217 | isMatched = false;
218 | keyString = '';
219 | }
220 | keyString = '';
221 | }
222 |
223 | function filterTCOPropertyByVariable() {
224 | if (variableMatchedTypeInTempCache()) {
225 | updateTempCacheObject()
226 | } else {
227 | addToTotalTypesNewQuery()
228 | }
229 | }
230 |
231 | function updateTempCacheObject() {
232 | tempTypes.push(typeMatchedWithVariable);
233 | typeNeedsAdding = true;
234 | variableAffectedObject = tempCacheObject[typeMatchedWithVariable.trim()];
235 | let variableKey = Object.keys(inputObject);
236 | tempArray = variableAffectedObject.filter((obj) => {
237 | return obj[variableKey[0]] === inputObject[variableKey[0]];
238 | });
239 | tempCacheObject = tempArray[0];
240 | }
241 |
242 | function addToTotalTypesNewQuery() {
243 | totalTypes += 1;
244 | newQuery += keyString.trim() + typeMatchedWithVariable + ' {' + ' id ';
245 | }
246 |
247 | function filterCachePropertyByVariable() {
248 | if (cacheObjectHasItem()) {
249 | variableAffectedArray = cacheObject[keyString.trim()];
250 |
251 | let variableKey = Object.keys(inputObject);
252 |
253 | let tempArray = variableAffectedArray.filter((obj) => {
254 | return obj[variableKey[0]] === inputObject[variableKey[0]];
255 | });
256 |
257 | tempCacheObject = tempArray[0];
258 | tempTypes.push(typeMatchedWithVariable);
259 | typeNeedsAdding = true;
260 |
261 | keyString = '';
262 | } else {
263 | totalTypes += 1;
264 | newQuery += keyString.trim() + typeMatchedWithVariable + ' {' + ' id ';
265 | keyString = '';
266 | }
267 | }
268 |
269 | function getFromTCOAndNestNewData() {
270 | if (keyStringIsInTempCacheObject()) {
271 | nestNewDataFromTempCacheObject()
272 | } else {
273 | updateGlobalVariablesIfNotFound()
274 | }
275 | }
276 | function keyStringIsInTempCacheObject() {
277 | return tempCacheObject[keyString.trim()] &&
278 | tempCacheObject[keyString.trim()][0]
279 | }
280 |
281 | function nestNewDataFromTempCacheObject() {
282 | tempCacheObject = tempCacheObject[keyString.trim()][0];
283 | tempTypes.push(keyString.trim());
284 | typeNeedsAdding = true;
285 |
286 | keyString = '';
287 | }
288 |
289 | function updateGlobalVariablesIfNotFound() {
290 | totalTypes += 1;
291 | newQuery += keyString.trim() + ' {' + ' id ';
292 | keyString = '';
293 | bracketCount += 1;
294 |
295 | }
296 |
297 | function getFromCacheOrAddToQuery() {
298 | if (cacheObjectHasItem()) {
299 | updateTempCacheObjectFromCacheObject()
300 | } else {
301 | updateGlobalVariablesIfNotFound()
302 | }
303 | }
304 |
305 | function updateTempCacheObjectFromCacheObject() {
306 | tempCacheObject = cacheObject[keyString.trim()][0];
307 | tempTypes.push(keyString.trim());
308 | typeNeedsAdding = true;
309 | keyString = '';
310 |
311 | }
312 |
313 | function findOpeningCurlyBracketAfterField() {
314 | while (currElementIsntOpeningBracket() && searchLimiter > 0) {
315 | index += 1;
316 | searchLimiter -= 1;
317 | }
318 |
319 | searchLimiter = 1;
320 | if (currElementIsntOpeningBracket()) {
321 | index -= 1;
322 | }
323 |
324 | }
325 |
326 | function addClosingBracketIfFound() {
327 | let isFinalBracket = false;
328 | if (currElementIsClosingBracket()) {
329 | if (bracketCount) {
330 | newQuery += '} ';
331 | totalTypes -= 1;
332 | tempCacheObject = {};
333 | bracketCount -= 1;
334 | }
335 |
336 | if (bracketCount === 0) {
337 | isFinalBracket = true;
338 | return isFinalBracket;
339 | }
340 |
341 | index += 1;
342 | }
343 |
344 | return isFinalBracket;
345 | }
346 |
347 |
348 | function createKeyString() {
349 |
350 | if (currElementIsAChar()) {
351 | while (currElementIsntASpace()) {
352 | if (query[index] === '(') {
353 | parseAndHoldVarLocation();
354 | break;
355 | }
356 |
357 | if (query[index] === '}') {
358 | fieldsInCacheOrNot();
359 | break;
360 | }
361 | keyString += query[index];
362 | index += 1;
363 | }
364 | }
365 | }
366 |
367 | function parseAndHoldVarLocation() {
368 | typeMatchedWithVariable = keyString.trim();
369 | variableForFilter = keyString.trim();
370 |
371 | while (query[index] !== ')') {
372 | holdVarString += query[index];
373 | index += 1;
374 | }
375 |
376 | holdVarString += query[index];
377 | index += 1;
378 | }
379 |
380 | function eatWhiteSpace() {
381 | while (query[index] === ' ' || query[index] === '\n') {
382 | index += 1;
383 | }
384 | }
385 | // --------------------------
386 |
387 | // General Helper Functions
388 | function variableExistsAndMatchesCurrentType() {
389 | return variableForFilter && variableForFilter === currentType
390 | }
391 | function tempTypesForCacheDataHasNoLength() {
392 | return !tempTypesForCacheData.length
393 | }
394 | function cacheObjectHasItem() {
395 | return cacheObject[keyString.trim()]
396 | }
397 |
398 | function variableMatchedTypeInTempCache() {
399 | return tempCacheObject[typeMatchedWithVariable.trim()]
400 | }
401 | function bracketCountNotOne() {
402 | return bracketCount !== 1;
403 | }
404 |
405 | function bracketCountIsOne() {
406 | return bracketCount === 1
407 | }
408 |
409 | function currElementIsClosingBracket() {
410 | return query[index] === '}';
411 | }
412 |
413 | function currElementIsntASpace() {
414 | return query[index] !== ' ';
415 | }
416 |
417 | function addElementAndIncrementIndex() {
418 | newQuery += query[index];
419 | index += 1;
420 | }
421 |
422 | function parseOpeningBracket() {
423 | if (currElementIsOpeningBracket()) {
424 | newQuery += query[index] + ' ';
425 | index += 1;
426 | bracketCount += 1;
427 | }
428 | }
429 |
430 | function currElementIsOpeningBracket() {
431 | return query[index] === '{';
432 | }
433 |
434 | function currElementIsntOpeningBracket() {
435 | return query[index] !== '{';
436 | }
437 |
438 | function currElementIsASpace() {
439 | return query[index] === ' '
440 | }
441 |
442 | function currElementIsAChar() {
443 | return query[index].match(charFindRegex)
444 | }
445 |
446 | // Parse Names And Variables
447 | function parseIfNameOrVariable() {
448 |
449 | if (currElementIsAChar()) {
450 | parseName();
451 |
452 | if (currElementIsASpace()) {
453 | index += 1;
454 | } else {
455 | return parseVariable();
456 | }
457 | }
458 | }
459 |
460 | function parseName() {
461 | while (query[index] !== '(' || /\s/.test(query[index])) {
462 | addElementAndIncrementIndex()
463 | }
464 | }
465 |
466 | function parseVariable() {
467 | let inputObjectString = '';
468 | while (currElementIsntOpeningBracket()) {
469 | addElementAndIncrementIndex()
470 | }
471 |
472 | while (query[index] !== '}') {
473 | inputObjectString += query[index];
474 | addElementAndIncrementIndex()
475 | }
476 |
477 | inputObjectString += query[index];
478 | newQuery += query[index];
479 | inputObject = JSON.parse(inputObjectString);
480 |
481 | while (query[index] !== ')') {
482 | addElementAndIncrementIndex()
483 | }
484 |
485 | newQuery += query[index];
486 | index += 2;
487 | }
488 |
489 | function parseTheWordQuery() {
490 | while (query[index] !== ' ') {
491 | addElementAndIncrementIndex()
492 | }
493 | addElementAndIncrementIndex()
494 | }
495 |
496 | function findFirstLetterOrBracket() {
497 | let foundBracketOrQ = 'q';
498 |
499 | while (query[index] !== 'q') {
500 | if (currElementIsOpeningBracket()) {
501 | parseOpeningBracket()
502 | foundBracketOrQ = 'bracket';
503 | break;
504 | }
505 |
506 | index += 1;
507 | }
508 |
509 | return foundBracketOrQ;
510 | }
511 | };
512 |
513 | export default serverFilamentQuery;
514 |
--------------------------------------------------------------------------------
/filament/utils/getSessionStorageKey.js:
--------------------------------------------------------------------------------
1 | const getSessionStorageKey = (query) => {
2 | const res = query
3 | .split('\n')
4 | .filter((part) => part)
5 | .map((part) => part.trim());
6 | const key = res[1].replace(' {', '');
7 |
8 | return key;
9 | };
10 |
11 | export default getSessionStorageKey;
12 |
--------------------------------------------------------------------------------
/filament/utils/index.js:
--------------------------------------------------------------------------------
1 | import mergeTwoArraysById from './mergeTwoArraysById';
2 | import transformQuery from './transformQuery';
3 | import parseKeyInCache from './parseKeyInCache';
4 | import getSessionStorageKey from './getSessionStorageKey';
5 | import uniqueId from './uniqueId';
6 |
7 | export {
8 | mergeTwoArraysById,
9 | transformQuery,
10 | parseKeyInCache,
11 | getSessionStorageKey,
12 | uniqueId,
13 | };
14 |
--------------------------------------------------------------------------------
/filament/utils/mergeTwoArraysById.js:
--------------------------------------------------------------------------------
1 | const mergeTwoArraysById = (dataFromCache, dataFromServer) => {
2 | const mergedData = dataFromCache.map((dataCache) => {
3 | const matchedObj = dataFromServer.find(
4 | (dataServer) => dataServer.id === dataCache.id
5 | );
6 | const newData = { ...dataCache, ...matchedObj };
7 | return newData;
8 | });
9 |
10 | return mergedData;
11 | };
12 |
13 | export default mergeTwoArraysById;
14 |
--------------------------------------------------------------------------------
/filament/utils/parseKeyInCache.js:
--------------------------------------------------------------------------------
1 | function parseKeyInCache(query) {
2 | let keyInCache = '';
3 | let index = 0;
4 | const charFindRegex = /[a-zA-Z]/;
5 | let keyString = '';
6 | let keyWithinCache = 'keyString;';
7 |
8 | skipFirstWord();
9 | eatWhiteSpace();
10 | createKeyString();
11 |
12 | keyWithinCache = keyString;
13 | return keyWithinCache;
14 |
15 | function createKeyString() {
16 | if (query[index].match(charFindRegex)) {
17 | while (query[index] !== ' ') {
18 | if (query[index] === '(') {
19 | parseAndHoldVarLocation();
20 | break;
21 | }
22 |
23 | keyString += query[index];
24 |
25 | index += 1;
26 | }
27 | }
28 | }
29 |
30 | function eatWhiteSpace() {
31 | // query[index] starts out on a space or linebreak
32 | while (query[index] === ' ' || query[index] === '\n') {
33 | index += 1;
34 | }
35 | // next query[index] will be a character
36 | }
37 |
38 | function skipFirstWord() {
39 | // looks for a 'q', which will mean that we found the word 'query'
40 | // if we find a '{' it means we found a query using the different syntax
41 | while (query[index] !== 'q') {
42 | if (query[index] === '{') {
43 | index += 1;
44 | return;
45 | }
46 | index += 1;
47 | }
48 |
49 | // parse the word 'query'
50 | while (query[index] !== ' ') {
51 | index += 1;
52 | }
53 | index += 1;
54 |
55 | if (query[index] === '{') {
56 | index += 1;
57 | }
58 | }
59 | }
60 |
61 | export default parseKeyInCache;
62 |
--------------------------------------------------------------------------------
/filament/utils/transformQuery.js:
--------------------------------------------------------------------------------
1 | const transformQuery = (query) => {
2 | const parts = query.split(' ');
3 | const stack = [];
4 | let indentations = 2;
5 | let result = '';
6 |
7 | parts.forEach((part, idx) => {
8 | if (part === '{') {
9 | stack.push(part);
10 | indentations += 2;
11 | } else if (part === '}') {
12 | stack.pop();
13 | indentations -= 2;
14 | const space = ' '.repeat(indentations);
15 | result += space + part + '\n';
16 | } else {
17 | const space = ' '.repeat(indentations);
18 |
19 | if (part === 'query') result += space + part + ' {' + '\n';
20 | else {
21 | const open = parts[idx + 1] === '{' ? ' {' : '';
22 | result += space + part + open + '\n';
23 | }
24 | }
25 | });
26 |
27 | return result;
28 | };
29 |
30 | export default transformQuery;
31 |
--------------------------------------------------------------------------------
/filament/utils/uniqueId.js:
--------------------------------------------------------------------------------
1 | const uniqueId = () => '_' + Math.random().toString(36).substr(2, 9);
2 |
3 | export default uniqueId;
4 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 | Filament
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/logoBig.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/FilamentQL/d9ba61ff7bab74da0a51da5b281f218990fa26f1/logoBig.jpg
--------------------------------------------------------------------------------
/offline-diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/FilamentQL/d9ba61ff7bab74da0a51da5b281f218990fa26f1/offline-diagram.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "filament",
3 | "version": "1.0.0",
4 | "description": "GraphQL query and caching solution",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "jest",
8 | "client": "webpack-dev-server --hot",
9 | "server": "nodemon server/server.js --exec babel-node",
10 | "json-server": "json-server --watch fake-database.json",
11 | "dev": "npm run client & npm run server & npm run json-server & redis-server"
12 | },
13 | "keywords": [],
14 | "author": "nelson, andrew, chan, duy",
15 | "license": "MIT",
16 | "dependencies": {
17 | "axios": "^0.20.0",
18 | "body-parser": "^1.19.0",
19 | "dotenv": "^8.2.0",
20 | "express": "^4.17.1",
21 | "express-graphql": "^0.11.0",
22 | "filamentql": "^1.0.4",
23 | "graphql": "^15.4.0",
24 | "graphql-import": "^1.0.2",
25 | "graphql-tools": "^6.2.4",
26 | "json-server": "^0.16.2",
27 | "mongoose": "^5.10.11",
28 | "react": "^16.14.0",
29 | "react-dom": "^16.14.0",
30 | "react-router-dom": "^5.2.0",
31 | "redis": "^3.0.2"
32 | },
33 | "devDependencies": {
34 | "@babel/core": "^7.12.3",
35 | "@babel/node": "^7.12.6",
36 | "@babel/plugin-transform-runtime": "^7.12.1",
37 | "@babel/preset-env": "^7.12.1",
38 | "@babel/preset-react": "^7.12.1",
39 | "babel-jest": "^26.6.1",
40 | "babel-loader": "^8.1.0",
41 | "css-loader": "^5.0.0",
42 | "dotenv": "^8.2.0",
43 | "enzyme": "^3.11.0",
44 | "enzyme-adapter-react-16": "^1.15.5",
45 | "jest": "^26.6.1",
46 | "nodemon": "^2.0.5",
47 | "sass": "^1.27.0",
48 | "sass-loader": "^10.0.3",
49 | "style-loader": "^2.0.0",
50 | "supertest": "^6.0.0",
51 | "webpack": "^4.44.2",
52 | "webpack-cli": "^3.3.12",
53 | "webpack-dev-server": "^3.11.0"
54 | },
55 | "repository": {
56 | "type": "git",
57 | "url": "git+https://github.com/neljson/Filament.git"
58 | },
59 | "bugs": {
60 | "url": "https://github.com/neljson/Filament/issues"
61 | },
62 | "homepage": "https://github.com/neljson/Filament#readme"
63 | }
64 |
--------------------------------------------------------------------------------
/placeholderLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/FilamentQL/d9ba61ff7bab74da0a51da5b281f218990fa26f1/placeholderLogo.png
--------------------------------------------------------------------------------
/server-diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/FilamentQL/d9ba61ff7bab74da0a51da5b281f218990fa26f1/server-diagram.png
--------------------------------------------------------------------------------
/server/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/FilamentQL/d9ba61ff7bab74da0a51da5b281f218990fa26f1/server/.DS_Store
--------------------------------------------------------------------------------
/server/__tests__/server.js:
--------------------------------------------------------------------------------
1 | const request = require('supertest');
2 | const express = require('express');
3 | const { graphqlHTTP } = require('express-graphql')
4 | const schema = require('../schema')
5 |
6 | const app = express();
7 |
8 | const server = "http://localhost:8080";
9 |
10 | app.use(
11 | '/graphql',
12 | graphqlHTTP({
13 | schema,
14 | graphiql: true
15 | })
16 | );
17 |
18 | describe('initial server test', () => {
19 | it('should not get status 200', () => {
20 | return request(server)
21 | .post('/graphql')
22 | .send({
23 | query: `
24 | {
25 | todos {
26 |
27 | }
28 | }
29 | `
30 | })
31 | .set('Accept', 'application/incorrect')
32 | .expect('Content-Type', /json/)
33 | .expect(400)
34 | })
35 |
36 | it('should pass the test', () => {
37 | return request(server)
38 | .post('/graphql')
39 | .send({
40 | query: `
41 | {
42 | todos {
43 | id
44 | text
45 | isCompleted
46 | }
47 | }
48 | `
49 | })
50 | .set('Accept', 'application/json')
51 | .expect('Content-Type', /json/)
52 | .expect(200)
53 | })
54 | })
55 |
--------------------------------------------------------------------------------
/server/memberModel.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 | const { Schema } = mongoose;
3 |
4 | const memberSchema = new Schema({
5 | name: {
6 | type: String,
7 | required: true,
8 | },
9 | avatar: {
10 | type: String,
11 | required: true,
12 | },
13 | bio: {
14 | type: String,
15 | required: true,
16 | },
17 | github: {
18 | type: String,
19 | required: true,
20 | },
21 | linkedIn: {
22 | type: String,
23 | required: true,
24 | },
25 | });
26 |
27 | module.exports = mongoose.model("member", memberSchema);
28 |
--------------------------------------------------------------------------------
/server/resolvers.js:
--------------------------------------------------------------------------------
1 | const axios = require("axios");
2 | const Todo = require("./todoModel");
3 | const Member = require("./memberModel");
4 |
5 | const resolvers = {
6 | Query: {
7 | async todos() {
8 | const todos = await Todo.find({});
9 | return todos;
10 | },
11 | async members() {
12 | const members = await Member.find({});
13 | return members;
14 | },
15 | },
16 | Mutation: {
17 | async addTodo(parent, { input }, context) {
18 | try {
19 | const newTodo = {
20 | id: Math.random().toString(),
21 | text: input.text,
22 | isCompleted: false,
23 | difficulty: 99,
24 | };
25 |
26 | const addedTodo = await Todo.create(newTodo);
27 | return addedTodo;
28 | } catch (err) {
29 | return err
30 | }
31 | },
32 | async updateTodo(_, { input }) {
33 | try {
34 | const { id, text } = input;
35 | const updatedTodo = await Todo.findByIdAndUpdate(id, { text }, { new: true })
36 | return updatedTodo
37 | } catch (err) {
38 | return err
39 | }
40 | },
41 | async deleteTodo(_, { input }) {
42 | try {
43 | const { id } = input;
44 | const deletedTodo = await Todo.findByIdAndDelete(id)
45 | return deletedTodo
46 | } catch (err) {
47 | return err
48 | }
49 | },
50 | },
51 | };
52 |
53 | module.exports = resolvers;
54 |
--------------------------------------------------------------------------------
/server/schema.graphql:
--------------------------------------------------------------------------------
1 | type Todo {
2 | id: ID!
3 | text: String!
4 | isCompleted: Boolean!
5 | difficulty: Int!
6 | }
7 |
8 | type Member {
9 | id: ID!
10 | name: String!
11 | avatar: String!
12 | bio: String!
13 | github: String!
14 | linkedIn: String!
15 | }
16 |
17 | type Query {
18 | todos: [Todo!]!
19 | members: [Member!]!
20 | }
21 |
22 | input addTodoInput {
23 | id: ID
24 | text: String!
25 | isCompleted: Boolean
26 | }
27 |
28 | input updateTodoInput {
29 | id: ID!
30 | text: String!
31 | }
32 |
33 | input deleteTodoInput {
34 | id: ID!
35 | }
36 |
37 | type Mutation {
38 | addTodo(input: addTodoInput!): Todo
39 | updateTodo(input: updateTodoInput!): Todo
40 | deleteTodo(input: deleteTodoInput!): Todo
41 | }
42 |
--------------------------------------------------------------------------------
/server/schema.js:
--------------------------------------------------------------------------------
1 | const { loadSchemaSync } = require('@graphql-tools/load');
2 | const { GraphQLFileLoader } = require('@graphql-tools/graphql-file-loader');
3 | const { addResolversToSchema } = require('@graphql-tools/schema');
4 | const { join } = require('path');
5 |
6 | const schema = loadSchemaSync(join(__dirname, 'schema.graphql'), {
7 | loaders: [new GraphQLFileLoader()],
8 | });
9 |
10 | const resolvers = require('./resolvers');
11 |
12 | const schemaWithResolvers = addResolversToSchema({
13 | schema,
14 | resolvers,
15 | });
16 |
17 | module.exports = schemaWithResolvers;
18 |
--------------------------------------------------------------------------------
/server/server.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const { graphqlHTTP } = require('express-graphql');
3 | const redis = require('redis');
4 | require('dotenv').config();
5 |
6 | // Mongo DB Setup
7 | const mongoose = require('mongoose');
8 | const MONGO_URI = process.env.MONGO_URI;
9 | mongoose
10 | .connect(MONGO_URI, {
11 | useNewUrlParser: true,
12 | useUnifiedTopology: true,
13 | dbName: 'Filament',
14 | })
15 | .then(async () => console.log('Connected to Mongo DB'))
16 | .catch((err) => console.log(err));
17 |
18 | // Redis Setup
19 | const client = redis.createClient();
20 | client
21 | .on('error', (err) => console.log('Error ' + err))
22 | .on('connect', () => console.log('Redis client connected'));
23 |
24 | // Filament Setup
25 | const filamentMiddlewareWrapper = require('filamentql/server');
26 | const filamentMiddleware = filamentMiddlewareWrapper(client);
27 |
28 | // Express setup
29 | const app = express();
30 | const PORT = 4000;
31 | const schema = require('./schema');
32 |
33 | app.use(express.json());
34 | app.use('/filament', filamentMiddleware);
35 | app.use(
36 | '/graphql',
37 | graphqlHTTP((req) => ({
38 | schema,
39 | graphiql: true,
40 | context: {
41 | req,
42 | },
43 | }))
44 | );
45 |
46 | app.listen(PORT, () => console.log(`GraphQL server is on port: ${PORT}`));
47 |
--------------------------------------------------------------------------------
/server/todoModel.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 | const { Schema } = mongoose;
3 |
4 | const todoSchema = new Schema({
5 | text: {
6 | type: String,
7 | required: true,
8 | },
9 | isCompleted: {
10 | type: Boolean,
11 | required: true,
12 | default: true,
13 | },
14 | difficulty: {
15 | type: Number,
16 | required: true,
17 | },
18 | });
19 |
20 | module.exports = mongoose.model('todo', todoSchema);
21 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | entry: './client/index.jsx',
5 | output: {
6 | path: path.resolve(__dirname, 'build'),
7 | filename: 'bundle.js',
8 | },
9 | mode: 'development',
10 | module: {
11 | rules: [
12 | {
13 | test: /\.jsx$/,
14 | exclude: /node_modules/,
15 | use: {
16 | loader: 'babel-loader',
17 | options: {
18 | presets: ['@babel/preset-env', '@babel/preset-react'],
19 | plugins: ['@babel/plugin-transform-runtime'],
20 | },
21 | },
22 | },
23 | {
24 | test: /\.s[ac]ss$/,
25 | use: ['style-loader', 'css-loader', 'sass-loader'],
26 | },
27 | ],
28 | },
29 | devServer: {
30 | publicPath: '/build',
31 | proxy: [
32 | {
33 | context: ['/filament', '/graphql'],
34 | target: 'http://localhost:4000',
35 | },
36 | ],
37 | },
38 | resolve: {
39 | extensions: ['.js', '.jsx'],
40 | },
41 | };
42 |
--------------------------------------------------------------------------------