├── .gitignore
├── .prettierrc.json
├── Qache
├── Node.js
├── Qache.js
├── README.md
└── package.json
├── README.md
├── __tests__
└── Qache.test.js
├── package-lock.json
├── package.json
└── qache-app
├── .babelrc
├── .gitignore
├── .prettierrc.json
├── client
├── App.tsx
├── components
│ ├── demo-app
│ │ ├── DemoApp.tsx
│ │ ├── LandingPage.tsx
│ │ ├── LineGraph.tsx
│ │ ├── Navigation.tsx
│ │ ├── ProductDetails.tsx
│ │ ├── ProductDisplay.tsx
│ │ ├── SidebarData.js
│ │ ├── SubMenuData.tsx
│ │ └── images
│ │ │ └── transparentlogowithslogan.png
│ └── home
│ │ ├── Docs.tsx
│ │ ├── Home.tsx
│ │ ├── Introduction.tsx
│ │ ├── Navbar.tsx
│ │ └── Team.tsx
├── images
│ ├── demo.gif
│ ├── navigate.gif
│ ├── preview.gif
│ ├── qache.gif
│ ├── qache.png
│ ├── qacheslogan.png
│ └── storage.jpg
├── index.tsx
└── styles
│ ├── demo-styles
│ ├── LandingPage.scss
│ ├── Navigation.scss
│ ├── ProductDetails.scss
│ ├── ProductDisplay.scss
│ └── SubMenuData.scss
│ ├── home-styles
│ ├── Docs.scss
│ ├── Introduction.scss
│ └── Navbar.scss
│ ├── index.css
│ ├── index.scss
│ └── variables.scss
├── dist
├── bundle.js
├── bundle.js.LICENSE.txt
├── images
│ ├── demo.gif
│ ├── navigate.gif
│ ├── preview.gif
│ ├── qache.gif
│ ├── qache.png
│ └── storage.jpg
├── index.html
└── main.css
├── index.d.ts
├── interfaces.ts
├── package-lock.json
├── package.json
├── postcss.config.js
├── server
├── database
│ └── db.js
├── models
│ ├── CategoryModel.js
│ └── ProductModel.js
├── resolvers
│ └── resolvers.js
├── server.js
└── typeDefs
│ └── schema.js
├── tailwind.config.js
├── tsconfig.json
└── webpack.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | **/node_modules
2 | **/.vscode
3 | **/*.env
4 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "tabWidth": 2,
4 | "semi": true,
5 | "singleQuote": true,
6 | "printWidth": 80,
7 | "jsxSingleQuote": true,
8 | "useTabs": false,
9 | "useEditorConfig": true,
10 | "htmlWhitespaceSensitivity": "css",
11 | "bracketSpacing": true,
12 | "arrowParens": "always"
13 | }
14 |
--------------------------------------------------------------------------------
/Qache/Node.js:
--------------------------------------------------------------------------------
1 | class Node {
2 | constructor(keyRef, value) {
3 | this.keyRef = keyRef;
4 | this.value = value;
5 | this.prev = this.next = null;
6 | this.expires = Infinity;
7 | this.accessCount = 1;
8 | }
9 | }
10 |
11 | module.exports = Node;
12 |
--------------------------------------------------------------------------------
/Qache/Qache.js:
--------------------------------------------------------------------------------
1 | const Node = require('./Node');
2 |
3 | class Qache {
4 | constructor({
5 | timeToLive = Infinity,
6 | maxSize = Infinity,
7 | evictionPolicy = 'LRU',
8 | } = {}) {
9 | //set timeToLive in options or default to 10min
10 | this.TTL = timeToLive; //10 minute default timeToLive
11 | this.maxSize = maxSize; // 5 node default maximum size
12 | this.policyType = evictionPolicy;
13 |
14 | this.content = {}; // STORE OF NODES
15 | this.size = 0; // current size of cache
16 | this.tail = this.head = null; // pointers to head(dequeue)/tail(enqueue) of queue
17 | }
18 |
19 | get(key) {
20 | this.cleanUp(key);
21 | return this._getDataFromQueue(key);
22 | }
23 |
24 | set(key, value) {
25 | if (this.content[key]) {
26 | this.update(key, value);
27 | console.log(
28 | `Updated Qache key "${key}" with value ${value}. In the future, use the "update" method.`
29 | );
30 | } else this._addToQueueAndCache(key, value);
31 | }
32 |
33 | update(key, newValue) {
34 | if (this.content[key]) {
35 | this._refresh(key);
36 | this.content[key].value = newValue;
37 | } else {
38 | this.set(key, newValue);
39 | console.log(
40 | `Set Qache key "${key}" with value ${newValue}. In the future, use the "set" method to adding nodes to the cache.`
41 | );
42 | }
43 | }
44 |
45 | delete(key) {
46 | if (this.content[key]) this._removeFromQueueAndCache(this.content[key]);
47 | }
48 |
49 | //Creates a list, with a unique key identifier. Option to add items to this list on creation.
50 | // It will be strongly advised to only create a list once it's full and complete content is available for storage.
51 | listCreate(listKey, items) {
52 | if (listKey === undefined) {
53 | console.log('Error, listCreate requires a unique cache key');
54 | } else {
55 | //Check if a list exists for this key, if not, create one.
56 | if (this.content[listKey] === undefined) {
57 | console.log('adding to queue and cache');
58 | this._addToQueueAndCache(listKey, []);
59 | }
60 | // for each item given, we push that item into cache, THEN refresh expiration.
61 | items.forEach((item) => this.content[listKey].value.push(item));
62 | }
63 | }
64 | //Check if list exists, if exists, assumed fresh and complete, returns by range or if no range specified, returns all.
65 | listRange(listKey, start = 0, end = Infinity) {
66 | this.cleanUp(listKey);
67 |
68 | if (this.content[listKey] === undefined) return null;
69 |
70 | const { value } = this.content[listKey];
71 | this._refresh(listKey);
72 |
73 | return start === 0 && end >= value.length ? value : value.slice(start, end);
74 | }
75 |
76 | //**Update an item if found, push item if not found.**
77 |
78 | listUpsert(item, filterObj, ...listKeys) {
79 | //Remind user that a key is required for this method
80 | if (listKeys === undefined) {
81 | console.log('Error, listPush requires atleast one unique cache key');
82 | } else {
83 | //Check if a list exists for this key, if not skip
84 | for (const listKey of listKeys) {
85 | if (this.content[listKey] === undefined) {
86 | continue;
87 | }
88 | //We push that item into cache, THEN refresh expiration.
89 | let found = false;
90 | for (const oldItem of this.content[listKey].value) {
91 | for (const key in filterObj) {
92 | if (oldItem[key] === filterObj[key]) {
93 | found = true;
94 | Object.assign(oldItem, item);
95 | }
96 | }
97 | }
98 | if (!found) this.content[listKey].value.push(item);
99 | this.content[listKey].expires = Date.now() + this.TTL;
100 | }
101 | }
102 | }
103 | // Push a newly created item to ONE OR MANY lists
104 | listPush(item, ...listKeys) {
105 | //Remind user that a key is required for this method
106 | if (listKeys === undefined) {
107 | console.log('Error, listPush requires atleast one unique cache key');
108 | } else {
109 | //Check if a list exists for this key, if not, skip
110 | for (const listKey of listKeys) {
111 | if (this.content[listKey] === undefined) {
112 | continue;
113 | }
114 | //We push that item into cache, THEN refresh expiration.
115 | this.content[listKey].value.push(item);
116 | this.content[listKey].expires = Date.now() + this.TTL;
117 |
118 | this._refresh(listKey);
119 | }
120 | }
121 | }
122 |
123 | // Support for Delete Mutations to keep cache fresher longer
124 | // The array of listKeys should be every key the developer wants to remove a specific item from.
125 | // It should be every list the item belongs to, ideally.
126 | // EX. cache.listRemove({name: "Fancy Chair"}, "livingRoomFurniture", "kitchenFurniture"})
127 | // removes all items with name === "Fancy Chair" from cache lists with keys "livingRoomFurniture" and "kitchenFurniture"
128 | listRemoveItem(filterObject, ...listKey) {
129 | // Option to specify if each list only contains the item once.
130 | let unique = false;
131 | if (filterObject.hasOwnProperty('unique')) {
132 | unique = filterObject.unique;
133 | delete filterObject.unique;
134 | }
135 | // Some intuition that if the ID key exists the item must be unique to each list.
136 | if (
137 | filterObject.hasOwnProperty('id') ||
138 | filterObject.hasOwnProperty('ID') ||
139 | filterObject.hasOwnProperty('_id')
140 | ) {
141 | unique = true;
142 | }
143 | //Loops through each listKey
144 | for (const key of listKey) {
145 | // **Cleanup protocol** - remove item if past expiration, if not, refresh expiration.
146 | this.cleanUp(key);
147 | if (this.content[key] !== undefined) {
148 | this.content[key].expires = Date.now() + this.TTL;
149 | } else {
150 | // If key is undefined, skip key.
151 | continue;
152 | }
153 | // **Cleanup protocol**
154 |
155 | //Loops through each list to find the item. using a unique identifier, such as the items id
156 | const currentList = this.content[key].value;
157 | for (const item of currentList) {
158 | let missing = false;
159 | //Loop through filterObject, and if one filter is missing set off flag, and skip to next item in list.
160 | for (let filter in filterObject) {
161 | if (item[filter] !== filterObject[filter]) {
162 | missing = true;
163 | break;
164 | }
165 | }
166 | //if flag was never set off, remove item from list
167 | if (!missing) {
168 | const index = currentList.indexOf(item);
169 | currentList.splice(index, 1);
170 | if (unique) break;
171 | }
172 | }
173 | this._refresh(key);
174 | }
175 | }
176 | //Very similar to listRemoveItem but updates the item instead of deleting it from list
177 | listUpdate(newItem, filterObject, ...listKey) {
178 | // Option to specify if each list only contains the item once.
179 | let unique = false;
180 |
181 | // Some intuition that if the ID key exists the item must be unique to each list.
182 | if (
183 | filterObject.hasOwnProperty('id') ||
184 | filterObject.hasOwnProperty('ID') ||
185 | filterObject.hasOwnProperty('_id')
186 | ) {
187 | unique = true;
188 | }
189 | // If our inuition isn't what the dev wants, they can specify otherwise.
190 | if (filterObject.hasOwnProperty('unique')) {
191 | unique = filterObject.unique;
192 | // delete this key because we don't want to loop over it for item validation
193 | delete filterObject.unique;
194 | }
195 | //Loops through each listKey
196 | for (const key of listKey) {
197 | // **Cleanup protocol** - remove item if past expiration, if not, refresh expiration.
198 | this.cleanUp(key);
199 | if (this.content[key] !== undefined) {
200 | this.content[key].expires = Date.now() + this.TTL;
201 | } else {
202 | // If key is undefined, skip key.
203 | continue;
204 | }
205 | // **Cleanup protocol**
206 | //Loops through each list to find the item. using a unique identifier, such as the items id
207 | const currentList = this.content[key].value;
208 | for (const item of currentList) {
209 | let missing = false;
210 | //Loop through filterObject, and if one filter is missing set off flag, and skip to next item in list.
211 | for (let filter in filterObject) {
212 | if (item[filter] !== filterObject[filter]) {
213 | missing = true;
214 | break;
215 | }
216 | }
217 | //if flag was never set off, update item in list
218 | if (!missing) {
219 | Object.assign(item, newItem);
220 | if (unique) break;
221 | }
222 | }
223 | this._refresh(key);
224 | }
225 | }
226 |
227 | //If list exists, assumed fresh complete, returns filtered results
228 | // FILTEROBJECT - looks like - {username: "xyz", age: 23}
229 | listFetch(listKey, filterObject) {
230 | this.cleanUp(listKey);
231 | // Check if list exists, if not return null.
232 | if (this.content[listKey] === undefined) return null;
233 |
234 | this._refresh(listKey);
235 |
236 | const returnList = [];
237 | // Option to specify if each list only contains the item once.
238 | let unique = false;
239 | // Some intuition that if the ID key exists the item must be unique to each list.
240 | if (
241 | filterObject.hasOwnProperty('id') ||
242 | filterObject.hasOwnProperty('ID') ||
243 | filterObject.hasOwnProperty('_id')
244 | ) {
245 | unique = true;
246 | }
247 | // If our inuition isn't what the dev wants, they can specify otherwise.
248 | if (filterObject.hasOwnProperty('unique')) {
249 | unique = filterObject.unique;
250 | // delete this key because we don't want to loop over it for item validation
251 | delete filterObject.unique;
252 | }
253 | // If list does exist, loop through list and find item by filter Object
254 | for (const item of this.content[listKey].value) {
255 | //create a flag to set off if a filter is not matching
256 | let missing = false;
257 | //Loop through filterObject, and if one filter is missing set off flag, and skip to next item in list.
258 | for (let filter in filterObject) {
259 | if (item[filter] !== filterObject[filter]) {
260 | missing = true;
261 | break;
262 | }
263 | }
264 | //if flag was never set off, add item to filtered list
265 | if (!missing) {
266 | returnList.push(item);
267 | if (unique) break;
268 | }
269 | }
270 | //refresh list expiration since it has been accessed
271 | this.content[listKey].expires = Date.now() + this.TTL;
272 |
273 | //if filtered list is empty, return null
274 | if (returnList.length === 0) return null;
275 |
276 | //if non empty return results
277 | return returnList;
278 | }
279 | // Option to invalidate certain lists, or items and remove them from cache, for certain mutations.
280 | invalidate(...keys) {
281 | //Clears cache if no keys are specified
282 | if (keys.length === 0) {
283 | this.clear();
284 | }
285 | //Clears specific keys and adjusts size property if key exists.
286 | for (let key of keys) {
287 | this.delete(key);
288 | }
289 | }
290 |
291 | /* {UTILITY METHODS} */
292 | //Cleans up stale data
293 | cleanUp(key) {
294 | //Evict a stale key if key is provided
295 | if (key !== undefined && this.content[key] !== undefined) {
296 | if (this.content[key].expires < Date.now()) {
297 | this.delete(key);
298 | }
299 | }
300 | //Option to cleanUp all keys if need arises
301 | else {
302 | for (const key in this.content) {
303 | if (this.content[key].expires < Date.now()) {
304 | this.delete(key);
305 | }
306 | }
307 | }
308 | }
309 | //count amount of keys
310 | size() {
311 | return this.size;
312 | }
313 |
314 | // wipe the cache
315 | clear() {
316 | this.content = {};
317 | this.size = 0;
318 | this.tail = this.head = null;
319 | }
320 |
321 | log() {
322 | console.log(Object.keys(this.content));
323 | console.log(`Size: ${this.size}`);
324 | console.log(`Size is valid: ${this._isSizeValid()}`);
325 | }
326 |
327 | /**
328 | * This function performs the following in order:
329 | * 1. Checks if the node exists in the cache and removes it if found. This behavior enforces that a new node with the same value as an existing node in memory overwrites the existing one and is enqueued to the tail since it is the most recently used data.
330 | * 2. Alternatively checks to see if the cache is full and then deletes the data from the cache and queue.
331 | * 3. Enqueues a new node at the tail of the queue
332 | * @param {string} key
333 | * @param {object} value
334 | */
335 |
336 | _addToQueueAndCache(key, value) {
337 | let nodeInCache = this.content[key];
338 |
339 | if (this.policyType === 'LRU') {
340 | // the node is already in the cache, so we must remove the old one so that our new node is inserted at the tail of the queue.
341 | if (nodeInCache) {
342 | // we only remove from queue and NOT cache since we are just enqueueing this node
343 | this._refresh(key);
344 | }
345 | // when the cache is full, we dequeue the head from the cache/queue
346 | else if (this.size === this.maxSize) {
347 | if (this.maxSize === 0) return;
348 | this._removeFromQueueAndCache(this.head);
349 | }
350 | //key doesn't exist
351 | if (!nodeInCache) {
352 | nodeInCache = new Node(key, value);
353 | nodeInCache.expires = Date.now() + this.TTL;
354 | this.size++;
355 | // enqueue node if it exists, otherwise enqueue new node with value
356 | this._enqueue(nodeInCache);
357 | // assign key to new node
358 | this.content[key] = this.tail;
359 | }
360 | } else if (this.policyType === 'LFU') {
361 | // key exists in cache
362 | if (nodeInCache) {
363 | this._refresh(key, value);
364 | //key doesn't exist, and cache at max size
365 | } else if (this.size === this.maxSize) {
366 | if (this.maxSize === 0) return;
367 | this._removeFromQueueAndCache(this.head);
368 | }
369 | // key doesn't exist
370 | if (!nodeInCache) {
371 | //create new node
372 | nodeInCache = new Node(key, value);
373 | nodeInCache.expires = Date.now() + this.TTL;
374 | this.size++;
375 |
376 | //Place node at head/cold side of queue
377 | this._enqueue(nodeInCache);
378 |
379 | // assign key to new node
380 | this.content[key] = this.head;
381 | }
382 | }
383 | }
384 |
385 | /**
386 | * Move accessed node in cache to the tail of the queue (remove it from queue and then enqueue it)
387 | * @param {object} key
388 | */
389 | _refreshRecent(node) {
390 | this._removeFromQueue(node);
391 | this._enqueue(node);
392 | }
393 |
394 | _refreshFrequent(node) {
395 | this._bubbleSort(node);
396 | }
397 |
398 | _refresh(key) {
399 | const existingNode = this.content[key];
400 |
401 | if (existingNode) {
402 | existingNode.accessCount++;
403 |
404 | if (this.policyType === 'LRU') {
405 | this._refreshRecent(existingNode);
406 | } else if (this.policyType === 'LFU') {
407 | this._refreshFrequent(existingNode);
408 | } else {
409 | throw new Error('Policy type does not exist');
410 | }
411 | }
412 | }
413 |
414 | _bubbleSort(node) {
415 | // 0 node list
416 | if (!node) return;
417 | // 1 node list OR 2+ node list where node is tail
418 | if (node === this.tail) return;
419 |
420 | // 2+ node list
421 | while (node.next && node.next.accessCount < node.accessCount) {
422 | if (node === this.head) {
423 | this.head = node.next;
424 |
425 | //2 node list where node is head
426 | if (!node.next.next) {
427 | this.tail = node;
428 |
429 | node.next.prev = null;
430 | node.next.next = node;
431 |
432 | node.next = null;
433 | node.prev = this.head;
434 | break;
435 | }
436 |
437 | //3+ node list where node is head
438 | const temp = node.next.next;
439 | temp.prev = node;
440 |
441 | node.next.prev = null;
442 | node.next.next = node;
443 |
444 | node.next = temp;
445 | node.prev = this.head;
446 | } else {
447 | //3 node list where node is not head
448 | // A > B > C swap B with C
449 | if (!node.next.next) {
450 | this.tail = node;
451 |
452 | const temp = node.next;
453 |
454 | node.prev.next = temp;
455 |
456 | temp.prev = node.prev;
457 | temp.next = node;
458 |
459 | node.prev = temp;
460 | node.next = null;
461 |
462 | break;
463 | }
464 | //swap with next
465 | // node
466 | // A > B > C > D
467 | //Point A's 'next' to C
468 | node.prev.next = node.next;
469 | //Point D's 'prev' to B
470 | node.next.next.prev = node;
471 |
472 | //Swap C's prev and next pointers
473 | const temp = node.next.next;
474 | node.next.prev = node.prev;
475 | node.next.next = node;
476 |
477 | //Swap B's prev and next pointers
478 | node.prev = node.next;
479 | node.next = temp;
480 | }
481 | node = node.next;
482 | }
483 | }
484 |
485 | /**
486 | * Add node to the tail of the DLL (enqueue it)
487 | * When we call enqueue, we assume we are enqueing a new node or existing node that is "floating" without pointing to any other nodes
488 | * @param {object} key
489 | */
490 | _enqueue(node) {
491 | // insert new node at tail of the linked list (queue)
492 | if (this.policyType === 'LRU') {
493 | if (this.tail) {
494 | node.prev = this.tail;
495 | this.tail.next = node;
496 | this.tail = node;
497 | } else this.tail = this.head = node;
498 | // queue is empty. point head & tail ➡ new Node
499 | } else if (this.policyType === 'LFU') {
500 | if (this.head) {
501 | node.next = this.head;
502 | this.head.prev = node;
503 | this.head = node;
504 | } else this.tail = this.head = node;
505 | }
506 | }
507 | /**
508 | * Removes a node from the queue and deletes the corresponding data from the cache
509 | * @param {object} key
510 | */
511 |
512 | _removeFromQueueAndCache(node) {
513 | delete this.content[node.keyRef];
514 | this._removeFromQueue(node);
515 | this.size--;
516 | }
517 |
518 | /**
519 | * Removes a node from the queue by detaching itself from its neighbors and connecting those neighbors' pointers.
520 | * @param {Node} node
521 | */
522 | _removeFromQueue(node) {
523 | // if this node is the only node in the queue
524 | if (!node.next && !node.prev) {
525 | this.head = this.tail = null;
526 | }
527 | // if node is at the tail of the queue
528 | else if (!node.next) {
529 | node.prev.next = null;
530 | this.tail = node.prev;
531 | }
532 | // if node is at the head of the queue
533 | else if (!node.prev) {
534 | node.next.prev = null;
535 | this.head = node.next;
536 | }
537 | // node is between two nodes, so connect neighboring pointers
538 | else {
539 | node.next.prev = node.prev;
540 | node.prev.next = node.next;
541 | }
542 | // remove node's pointers to former neighbors in queue
543 | node.prev = node.next = null;
544 | }
545 |
546 | /**
547 | * Refresh a node in the queue and return its value
548 | * @param {Node} node
549 | * @returns
550 | */
551 | _getDataFromQueue(key) {
552 | const nodeInCache = this.content[key];
553 |
554 | if (!nodeInCache) {
555 | console.log(`There is no key: ${key} in the cache.`);
556 | return null;
557 | }
558 |
559 | this._refresh(key);
560 |
561 | return nodeInCache.value;
562 | }
563 |
564 | /**
565 | * Used for testing
566 | * @returns {Boolean}
567 | */
568 | _isSizeValid() {
569 | return this.size === Object.keys(this.content).length;
570 | }
571 | }
572 |
573 | module.exports = Qache;
574 |
--------------------------------------------------------------------------------
/Qache/README.md:
--------------------------------------------------------------------------------
1 | # Qache
2 |
3 | ## What is Qache?
4 | Qache is a utility class for handling server side caching of MongoDB queries made with GraphQL.
5 |
6 | ## Installation
7 | Using npm:
8 | ```shell
9 | $ npm i qache
10 | ```
11 | In Node.js:
12 |
13 | ```js
14 | // Load the Qache class
15 | var Qache = require('qache');
16 |
17 | // Instantiate a cache
18 | var cache = new Qache();
19 | ```
20 | ## Qache Properties
21 | - `content`
22 | - `size`
23 | - `maxSize`
24 | - `TTL`
25 |
26 | ## Qache Methods
27 | - `get(key)`
28 | - `set(key, value)`
29 | - `listCreate(listKey, ...item)`
30 | - `listRange(listKey, start, end)`
31 | - `listPush(item, ...listKeys)`
32 | - `listRemoveItem(filterObject, ...listKey)`
33 | - `listUpdate(filterObject, newItem, ...listKey)`
34 | - `listFetch(listKey,filterObject)`
35 | - `invalidate(...keys)`
36 |
37 | ## Support
38 | Tested in Node.js 16.13.0
39 |
40 | [Automated unit tests](https://github.com/oslabs-beta/Qache/tree/dev/__tests__) are available.
41 |
42 | See the complete [package source](https://github.com/oslabs-beta/Qache) for more details, as well as a fullstack demo application.
43 |
--------------------------------------------------------------------------------
/Qache/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "qache",
3 | "version": "1.0.5",
4 | "description": "Qache is a utility class for handling server side caching of API queries made with any database.",
5 | "main": "Qache.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/oslabs-beta/Qache.git"
12 | },
13 | "keywords": [
14 | "cache",
15 | "qache",
16 | "graphql",
17 | "graphql-cache",
18 | "simple",
19 | "cache",
20 | "oslabs",
21 | "os-labs"
22 | ],
23 | "author": "Qache",
24 | "license": "MIT",
25 | "bugs": {
26 | "url": "https://github.com/oslabs-beta/Qache/issues"
27 | },
28 | "homepage": "https://github.com/oslabs-beta/Qache#readme"
29 | }
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Qache
4 |
5 | ## What is Qache?
6 | Qache is a modular utility class for handling server side caching of Data.
7 |
8 | Accelerated by [OS Labs](https://github.com/open-source-labs) and developed by [Nader Almogazy](https://github.com/nader12334), [Steven Du](https://github.com/stebed), [Leo Crossman](https://github.com/leocrossman), and [Evan Preedy](https://github.com/ep1815)
9 |
10 | ### Features
11 | - Server-side caching implemented in one line: ```const qache = new Qache({});```
12 | - Easy-to-use, modular caching methods, to get started... ```qache.set(key, value)``` then ```qache.get(key)```
13 | - Full Suite of list methods, allowing users to create, read, update, and delete cache data on a per page, per category, or site-wide level.
14 | - Lazy Invalidation! Optionally reset your cache after certain events if you can't figure out a way to maintain cache validity. Simply ```qache.invalidate()```
15 | - Get diagnostics on your Cache, and see all it's contents! ```qache.log()```
16 | - Cache Eviction Policies:
17 | - timeToLive: Qache tracks when each node stored was last accessed. By default data has an infinite timeToLive, to change this, pass into your options object `{timeToLive: NumberInMilliseconds}`
18 | - maxSize: Qache tracks how many keys/nodes exist, and upon reaching it's max, will begin to evict older data in order to add new data. This property is always true, and defaults to 5 nodes. To change this property, pass into your options object ```{maxSize: Number}```
19 | - LRU(default): Upon reaching max size, Qache will evict the Least Recently Accessed Node in cache
20 | - LFU: Upon reaching max size, Qache will evict the Least Frequently Used Node in Cache, determined by tracking access counts of each node. To switch to this policy, pass into your options object ```{evictionPolicy: "LFU"}```
21 |
22 | ### Contribute to Qache
23 | All code changes happen through Github Pull Requests and we actively welcome them. To submit your pull request, follow the steps below:
24 | ## Pull Requests
25 | 1. Fork the repo and create your branch from `master`.
26 | 2. If you've added code that should be tested, add tests.
27 | 3. Be sure to comment on any code added.
28 | 5. Ensure the test suite passes.
29 | 6. Make sure your code lints.
30 | 7. Issue a Pull Request!
31 |
32 | ## Setting up the Dev Server, Demo, and Mongo DB
33 | 1. Clone this repo.
34 | 2. `cd qache-app`
35 | 3. Run `npm install`
36 | 4. `npm start`
37 |
38 | ## Coding Style
39 | 2 spaces for indentation
40 | 80 character line length
41 |
42 | A note on using the dev server: you must access the `/graphql` route on port 8080.
43 |
--------------------------------------------------------------------------------
/__tests__/Qache.test.js:
--------------------------------------------------------------------------------
1 | const Cache = require('../Qache/Qache');
2 | const Node = require('../Qache/Node');
3 |
4 | const testUsers = [
5 | {
6 | username: 'nader12334',
7 | firstName: 'nader',
8 | lastName: 'almogazy',
9 | age: 27,
10 | },
11 | {
12 | username: 'leocrossman',
13 | firstName: 'leo',
14 | lastName: 'crossman',
15 | age: 23,
16 | },
17 | {
18 | username: 'ep1815',
19 | firstName: 'evan',
20 | lastName: 'preedy',
21 | age: 23,
22 | },
23 | {
24 | username: 'stebed',
25 | firstName: 'steven',
26 | lastName: 'du',
27 | age: Number.MAX_VALUE,
28 | },
29 | ];
30 |
31 | describe('Qache Tests', () => {
32 | describe('Node', () => {
33 | let node;
34 | beforeEach(() => {
35 | node = new Node('testKey', { testData: 'testVal' });
36 | });
37 | it('should have properties: keyRef, value, prev, next, expires, accessCount', () => {
38 | expect(node.keyRef).toBeDefined();
39 | expect(node.value).toBeDefined();
40 | expect(node.prev).toBeDefined();
41 | expect(node.next).toBeDefined();
42 | expect(node.expires).toBeDefined();
43 | expect(node.accessCount).toBeDefined();
44 | });
45 | });
46 |
47 | describe('Qache', () => {
48 | let cache, users, userNode;
49 | // this beforeEach will change soonish... will be replaced with whatever's relevant or just removed.
50 | beforeEach(() => {
51 | users = [...testUsers];
52 | users2 = testUsers.map((user) => user.username + 2);
53 | users3 = testUsers.map((user) => user.username + 3);
54 | users4 = testUsers.map((user) => user.username + 4);
55 | userNode = new Node('users', users);
56 | userNode2 = new Node('users2', users2);
57 | userNode3 = new Node('users3', users3);
58 | userNode4 = new Node('users4', users4);
59 | cache = new Cache();
60 | });
61 |
62 | it('should have properties: TTL, maxSize, content, size, policyType, head, tail', () => {
63 | expect(cache.TTL).toBeDefined();
64 | expect(cache.maxSize).toBeDefined();
65 | expect(cache.content).toBeDefined();
66 | expect(cache.size).toBeDefined();
67 | expect(cache.policyType).toBeDefined();
68 | expect(cache.head).toBeDefined();
69 | expect(cache.tail).toBeDefined();
70 | });
71 |
72 | it('should have initial property values: head = tail = null, size = 0, content = {}, maxSize = options.maxSize, policyType = options.evictionPolicy', () => {
73 | expect(cache.head).toBe(null);
74 | expect(cache.tail).toBe(null);
75 | expect(cache.size).toBe(0);
76 | expect(cache.content).toEqual({});
77 | expect(cache.policyType).toEqual('LRU');
78 | });
79 |
80 | describe('set()', () => {
81 | it(`should be a method on the 'Qache' class`, () => {
82 | expect(cache.set).toBeDefined();
83 | expect(typeof cache.set).toBe('function');
84 | });
85 |
86 | it('should take in a data type and store a refrence to the value in the cache as a node', () => {
87 | const testKeys = [
88 | 'string',
89 | 'number',
90 | 'boolean',
91 | 'array',
92 | 'object',
93 | 'null',
94 | 'NaN',
95 | ].map((type) => type + 'Key');
96 | const testTypes = [
97 | 'testString',
98 | 7,
99 | true,
100 | [1, 2, 3],
101 | { a: 'asdf' },
102 | null,
103 | NaN,
104 | ];
105 | for (let i = 0; i < testTypes.length; i++) {
106 | const key = testKeys[i];
107 | const typeVal = testTypes[i];
108 | cache.set(key, typeVal);
109 | expect(cache.content[key]).toBeInstanceOf(Node);
110 | expect(cache.content[key].value).toBe(typeVal);
111 | cache = new Cache();
112 | }
113 | });
114 |
115 | it('should take in a cache key/value, add the node to the queue, and add the node to the cache at the key when cache is empty', () => {
116 | expect(cache.head).toBe(null);
117 | expect(cache.tail).toBe(null);
118 | const key = 'users';
119 | cache.set(key, users);
120 | let node = cache.content[key];
121 | expect(node).toBeInstanceOf(Node);
122 | expect(node).toEqual(userNode);
123 | expect(node.keyRef).toBe(key);
124 | expect(node.value).toBe(users);
125 | expect(node.prev).toBe(null);
126 | expect(node.next).toBe(null);
127 | expect(node.accessCount).toBe(1);
128 | expect(cache.head).toBe(node);
129 | expect(cache.tail).toBe(node);
130 | });
131 |
132 | it('should increment cache size when size < maxSize', () => {
133 | expect(cache.size).toBe(0);
134 | cache.set('users', users);
135 | expect(cache.size).toBe(1);
136 | cache.set('users2', users2);
137 | expect(cache.size).toBe(2);
138 | });
139 |
140 | it('should not alter cache size when size equals maxSize', () => {
141 | cache.maxSize = 0;
142 | cache.set('users', users);
143 | expect(cache.size).toBe(0);
144 | cache = new Cache();
145 | cache.maxSize = 3;
146 | cache.set('users', users);
147 | expect(cache.size).toBe(1);
148 | cache.set('users2', users2);
149 | expect(cache.size).toBe(2);
150 | cache.set('users3', users3);
151 | expect(cache.size).toBe(3);
152 | cache.set('users4', users4);
153 | expect(cache.size).toBe(3);
154 | expect(cache.size).toEqual(Object.keys(cache.content).length);
155 | });
156 | });
157 |
158 | describe('get()', () => {
159 | it(`should be a method on the 'Qache' class`, () => {
160 | expect(cache.get).toBeDefined();
161 | expect(typeof cache.get).toBe('function');
162 | });
163 |
164 | it('should get a value from a key in the cache when there is one element in the cache', () => {
165 | cache.set('users', users);
166 | expect(cache.get('users')).toEqual(userNode.value);
167 | });
168 | it('should get a value from a key in the cache when there are two elements in the cache', () => {
169 | cache.set('users', users);
170 | cache.set('users2', users2);
171 | expect(cache.get('users2')).toEqual(userNode2.value);
172 | });
173 | it('should get a value from a key in the cache when there are 3+ elements in the cache', () => {
174 | cache.set('users', users);
175 | cache.set('users2', users2);
176 | cache.set('users3', users3);
177 | expect(cache.get('users3')).toEqual(userNode3.value);
178 | });
179 |
180 | it('should get the reference to the value in the node and not a copy of the value', () => {
181 | cache.content['users'] = userNode;
182 | expect(cache.get('users')).toBe(userNode.value);
183 | });
184 |
185 | it('should increment accessCount on the cache node', () => {
186 | userNode.accessCount = 0;
187 | cache.content['users'] = userNode;
188 | expect(cache.get('users')).toBe(users);
189 | expect(userNode.accessCount).toBe(1);
190 | expect(cache.get('users')).toBe(users);
191 | expect(userNode.accessCount).toBe(2);
192 | });
193 | });
194 |
195 | describe('delete()', () => {
196 | beforeEach(() => {
197 | users = [...testUsers];
198 | users2 = testUsers.map((user) => user.username + 2);
199 | users3 = testUsers.map((user) => user.username + 3);
200 | users4 = testUsers.map((user) => user.username + 4);
201 | userNode = new Node('users', users);
202 | userNode2 = new Node('users2', users2);
203 | userNode3 = new Node('users3', users3);
204 | userNode4 = new Node('users4', users4);
205 | cache = new Cache();
206 | });
207 |
208 | it(`should be a method on the 'Qache' class`, () => {
209 | expect(cache.delete).toBeDefined();
210 | expect(typeof cache.delete).toBe('function');
211 | });
212 |
213 | it('should take in a cache key and remove the corresponding node from the cache', () => {
214 | cache.set('users', users);
215 | cache.delete('users');
216 | expect(cache.get('users')).toBe(null);
217 | expect(cache.content['users']).toBe(undefined);
218 | });
219 |
220 | it('should take in a cache key and remove the corresponding node from the queue', () => {
221 | cache.set('users', users);
222 | cache.set('users2', users2);
223 | cache.set('users3', users3);
224 | cache.set('users4', users4);
225 | cache.delete('users2');
226 | let curr = cache.head;
227 | while (curr) {
228 | expect(curr).not.toEqual(userNode2);
229 | curr = curr.next;
230 | }
231 | });
232 |
233 | it('should remove a node from the cache and set head/tail to null when the node was the only one in cache before deletion', () => {
234 | cache.set('users', users);
235 | cache.delete('users');
236 | expect(cache.head).toBe(null);
237 | expect(cache.tail).toBe(null);
238 | });
239 |
240 | it('should assign the next node to the head of the linked list when the node at the head is deleted', () => {
241 | cache.set('users', users);
242 | cache.set('users2', users2);
243 | cache.set('users3', users3);
244 | cache.set('users4', users4);
245 | cache.delete('users');
246 | userNode2.prev = null;
247 | userNode2.next = userNode3;
248 | userNode3.prev = userNode2;
249 | userNode3.next = userNode4;
250 | userNode4.prev = userNode3;
251 | expect(cache.head).toEqual(userNode2);
252 | });
253 | it('should assign the previous node to the tail of the linked list when the node at the tail is deleted', () => {
254 | cache.set('users', users);
255 | cache.set('users2', users2);
256 | cache.set('users3', users3);
257 | cache.set('users4', users4);
258 | cache.delete('users4');
259 | userNode.next = userNode2;
260 | userNode2.prev = userNode;
261 | userNode2.next = userNode3;
262 | userNode3.prev = userNode2;
263 | userNode3.next = null;
264 | expect(cache.tail).toEqual(userNode3);
265 | });
266 | });
267 |
268 | describe('invalidate()', () => {
269 | beforeEach(() => {
270 | users = [...testUsers];
271 | users2 = testUsers.map((user) => user.username + 2);
272 | users3 = testUsers.map((user) => user.username + 3);
273 | users4 = testUsers.map((user) => user.username + 4);
274 | userNode = new Node('users', users);
275 | userNode2 = new Node('users2', users2);
276 | userNode3 = new Node('users3', users3);
277 | userNode4 = new Node('users4', users4);
278 | cache = new Cache();
279 | });
280 |
281 | it(`should be a method on the 'Qache' class`, () => {
282 | expect(cache.invalidate).toBeDefined();
283 | expect(typeof cache.invalidate).toBe('function');
284 | });
285 |
286 | it('should clear the entire cache if no arguments are passed in', () => {
287 | cache.set('users', users);
288 | cache.set('users2', users2);
289 | cache.set('users3', users3);
290 | cache.set('users4', users4);
291 | cache.invalidate();
292 | expect(cache.size).toBe(0);
293 | expect(cache.head).toBe(null);
294 | expect(cache.tail).toBe(null);
295 | expect(Object.keys(cache.content).length).toBe(0);
296 | });
297 |
298 | it('should remove a single item from the cache when it is the only item in the cache', () => {
299 | cache.set('users', users);
300 | cache.invalidate('users');
301 | expect(cache.content['users']).toBe(undefined);
302 | });
303 |
304 | it('should remove a single item from the cache when there are two items in the cache and the item is at the head, as well as reassign the head node to the next node in the queue', () => {
305 | cache.set('users', users);
306 | cache.set('users2', users2);
307 | cache.invalidate('users');
308 | expect(cache.content['users']).toBe(undefined);
309 | expect(cache.head).toBe(cache.tail);
310 | });
311 | it('should remove a single item from the cache when there are two items in the cache and the item is at the tail, as well as reassign the tail node to the previous node in the queue', () => {
312 | cache.set('users', users);
313 | cache.set('users2', users2);
314 | cache.invalidate('users2');
315 | expect(cache.content['users2']).toBe(undefined);
316 | expect(cache.tail).toBe(cache.head);
317 | });
318 | it('should remove a single item from the cache when there are 3+ items in the cache', () => {
319 | cache.set('users', users);
320 | cache.set('users2', users2);
321 | cache.set('users3', users3);
322 | cache.invalidate('users2');
323 | expect(cache.content['users2']).toBe(undefined);
324 | });
325 | it('should remove all keys passed in from the cache', () => {
326 | const userArr = ['users2', 'users3', 'users'];
327 | cache.set('users', users);
328 | cache.set('users2', users2);
329 | cache.set('users3', users3);
330 | cache.invalidate(...userArr);
331 | for (const user of userArr) {
332 | expect(cache.content[user]).toBe(undefined);
333 | }
334 | });
335 | });
336 |
337 | describe('cleanUp()', () => {
338 | beforeEach(() => {
339 | users = [...testUsers];
340 | users2 = testUsers.map((user) => user.username + 2);
341 | users3 = testUsers.map((user) => user.username + 3);
342 | users4 = testUsers.map((user) => user.username + 4);
343 | userNode = new Node('users', users);
344 | userNode2 = new Node('users2', users2);
345 | userNode3 = new Node('users3', users3);
346 | userNode4 = new Node('users4', users4);
347 | cache = new Cache();
348 | });
349 |
350 | it(`should be a method on the 'Qache' class`, () => {
351 | expect(cache.invalidate).toBeDefined();
352 | expect(typeof cache.invalidate).toBe('function');
353 | });
354 |
355 | it('should evict a provided key if the key exceeds its TTL', () => {
356 | cache.set('users', users);
357 | cache.set('users2', users2);
358 | cache.set('users3', users3);
359 | cache.set('users4', users4);
360 | cache.content['users'].expires = Date.now() - 100;
361 | cache.cleanUp('users');
362 | expect(cache.content['users']).toBe(undefined);
363 | });
364 |
365 | it('should not evict a provided key if the key does not exceed its TTL', () => {
366 | cache.set('users', users);
367 | cache.set('users2', users2);
368 | cache.set('users3', users3);
369 | cache.set('users4', users4);
370 | cache.content['users'].expires = Date.now() + 100;
371 | cache.cleanUp('users');
372 | expect(cache.content['users']).not.toBe(undefined);
373 | });
374 |
375 | it('should evict all provided keys when all the keys exceed their respective TTL', () => {
376 | cache.set('users', users);
377 | cache.set('users2', users2);
378 | cache.set('users3', users3);
379 | cache.set('users4', users4);
380 | for (const user of ['users', 'users2', 'users3', 'users4']) {
381 | cache.content[user].expires = Date.now() - 100;
382 | cache.cleanUp(user);
383 | expect(cache.content[user]).toBe(undefined);
384 | }
385 | });
386 |
387 | it('should evict some provided keys on successive calls when some of the keys exceed their respective TTL', () => {
388 | const users = ['users', 'users2', 'users3', 'users4'];
389 | cache.set('users', users);
390 | cache.set('users2', users2);
391 | cache.set('users3', users3);
392 | cache.set('users4', users4);
393 | for (let i = 0; i < users.length; i++) {
394 | const user = users[i];
395 | cache.content[user].expires =
396 | i % 2 === 0 ? Date.now() - 100 : Date.now() + 100;
397 | cache.cleanUp(user);
398 | i % 2 === 0
399 | ? expect(cache.content[user]).toBe(undefined)
400 | : expect(cache.content[user]).not.toBe(undefined);
401 | }
402 | });
403 |
404 | it('should evict all keys in the cache whose expiration time has passed and no key is passed in', () => {
405 | const users = ['users', 'users2', 'users3', 'users4'];
406 | cache.set('users', users);
407 | cache.set('users2', users2);
408 | cache.set('users3', users3);
409 | cache.set('users4', users4);
410 | console.log(cache.content);
411 | users.forEach((user) => {
412 | cache.content[user].expires = Date.now() - 100;
413 | });
414 | cache.cleanUp();
415 | for (const user of users) {
416 | expect(cache.content[user]).toBe(undefined);
417 | }
418 | });
419 | });
420 |
421 | describe('LRU Eviction Policy Tests:', () => {
422 | describe('set()', () => {
423 | beforeEach(() => {
424 | users = [...testUsers];
425 | users2 = testUsers.map((user) => user.username + 2);
426 | users3 = testUsers.map((user) => user.username + 3);
427 | users4 = testUsers.map((user) => user.username + 4);
428 | userNode = new Node('users', users);
429 | userNode2 = new Node('users2', users2);
430 | userNode3 = new Node('users3', users3);
431 | userNode4 = new Node('users4', users4);
432 | cache = new Cache();
433 | });
434 |
435 | it('should add a node to the queue and cache when there is one element in the cache and update the cache tail to point to the added node', () => {
436 | cache.set('users', users);
437 | cache.set('users2', users2);
438 | userNode.next = userNode2;
439 | userNode2.prev = userNode;
440 | expect(cache.head).toEqual(userNode);
441 | expect(cache.tail).toEqual(userNode2);
442 | expect(cache.tail).toBeInstanceOf(Node);
443 | });
444 | it('should add a node to the queue and cache when there are two elements in the cache', () => {
445 | cache.set('users', users);
446 | cache.set('users2', users2);
447 | cache.set('users3', users3);
448 | userNode.next = userNode2;
449 | userNode2.prev = userNode;
450 | userNode2.next = userNode3;
451 | userNode3.prev = userNode2;
452 | expect(cache.head).toEqual(userNode);
453 | expect(cache.tail).toEqual(userNode3);
454 | expect(cache.head.next).toEqual(userNode2);
455 | expect(cache.tail.prev).toEqual(userNode2);
456 | expect(cache.head.next.next).toBe(cache.tail);
457 | expect(cache.tail.prev.prev).toBe(cache.head);
458 | expect(cache.content['users2']).toEqual(userNode2);
459 | });
460 | it('should add a node to the queue and cache when there are 3+ elements in the cache', () => {
461 | cache.set('users', users);
462 | cache.set('users2', users2);
463 | cache.set('users3', users3);
464 | cache.set('users4', users4);
465 | userNode.next = userNode2;
466 | userNode2.prev = userNode;
467 | userNode2.next = userNode3;
468 | userNode3.prev = userNode2;
469 | userNode3.next = userNode4;
470 | userNode4.prev = userNode3;
471 | expect(cache.head).toEqual(userNode);
472 | expect(cache.head.next).toEqual(userNode2);
473 | expect(cache.head.next.next).toEqual(userNode3);
474 | expect(cache.head.next.next.next).toEqual(cache.tail);
475 | expect(cache.tail).toEqual(userNode4);
476 | expect(cache.tail.prev).toEqual(userNode3);
477 | expect(cache.tail.prev.prev).toEqual(userNode2);
478 | expect(cache.tail.prev.prev.prev).toEqual(userNode);
479 | expect(cache.content['users']).toEqual(userNode);
480 | expect(cache.content['users2']).toEqual(userNode2);
481 | expect(cache.content['users3']).toEqual(userNode3);
482 | expect(cache.content['users4']).toEqual(userNode4);
483 | });
484 |
485 | it('should add a node to the tail and remove + reassign the head when cache size equals maxSize', () => {
486 | cache.maxSize = 3;
487 | cache.set('users', users);
488 | cache.set('users2', users2);
489 | cache.set('users3', users3);
490 | cache.set('users4', users4);
491 | userNode2.prev = null;
492 | userNode2.next = userNode3;
493 | userNode3.prev = userNode2;
494 | userNode3.next = userNode4;
495 | userNode4.prev = userNode3;
496 | expect(cache.head).toEqual(userNode2);
497 | expect(cache.head.next).toEqual(userNode3);
498 | expect(cache.head.next.next).toBe(cache.tail);
499 | expect(cache.tail).toEqual(userNode4);
500 | expect(cache.tail.prev).toEqual(userNode3);
501 | expect(cache.tail.prev.prev).toBe(cache.head);
502 | });
503 | });
504 |
505 | describe('get()', () => {
506 | beforeEach(() => {
507 | users = [...testUsers];
508 | users2 = testUsers.map((user) => user.username + 2);
509 | users3 = testUsers.map((user) => user.username + 3);
510 | users4 = testUsers.map((user) => user.username + 4);
511 | userNode = new Node('users', users);
512 | userNode2 = new Node('users2', users2);
513 | userNode3 = new Node('users3', users3);
514 | userNode4 = new Node('users4', users4);
515 | const nodes = [userNode, userNode2, userNode3, userNode4];
516 | nodes.forEach((node) => {
517 | node.accessCount++;
518 | });
519 | cache = new Cache();
520 | });
521 |
522 | it('should refresh node position in the queue by moving it to the tail', () => {
523 | cache.set('users', users);
524 | cache.get('users');
525 | expect(cache.tail).toBe(cache.content['users']);
526 | expect(cache.tail).toEqual(userNode);
527 | });
528 | });
529 |
530 | describe('update()', () => {
531 | beforeEach(() => {
532 | users = [...testUsers];
533 | users2 = testUsers.map((user) => user.username + 2);
534 | users3 = testUsers.map((user) => user.username + 3);
535 | users4 = testUsers.map((user) => user.username + 4);
536 | userNode = new Node('users', users);
537 | userNode2 = new Node('users2', users2);
538 | userNode3 = new Node('users3', users3);
539 | userNode4 = new Node('users4', users4);
540 | cache = new Cache();
541 | });
542 |
543 | it('should move an existing node to the tail of the cache when a new value is passed into update with an existing key', () => {
544 | cache.set('users', users);
545 | cache.set('users2', users2);
546 | cache.update('users', users3);
547 | userNode3.keyRef = 'users';
548 | userNode3.accessCount = 2;
549 | userNode2.prev = null;
550 | userNode2.next = userNode3;
551 | userNode3.prev = userNode2;
552 | userNode3.next = null;
553 | expect(cache.size).toBe(2);
554 | expect(cache.head).toEqual(userNode2);
555 | expect(cache.tail).toBe(cache.head.next);
556 | });
557 | });
558 | });
559 | describe('LFU Eviction Policy Tests:', () => {
560 | describe('set()', () => {
561 | beforeEach(() => {
562 | users = [...testUsers];
563 | users2 = testUsers.map((user) => user.username + 2);
564 | users3 = testUsers.map((user) => user.username + 3);
565 | users4 = testUsers.map((user) => user.username + 4);
566 | userNode = new Node('users', users);
567 | userNode2 = new Node('users2', users2);
568 | userNode3 = new Node('users3', users3);
569 | userNode4 = new Node('users4', users4);
570 | cache = new Cache();
571 | });
572 | it('should evict the node with the lowest access count', () => {});
573 | });
574 | });
575 | });
576 | });
577 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "qache",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "devDependencies": {
7 | "jest": "^27.4.7"
8 | },
9 | "scripts": {
10 | "deploy": "git push heroku `git subtree split --prefix qache-app deployment`:main --force",
11 | "test": "jest --watchAll"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "git+https://github.com/oslabs-beta/Qache.git"
16 | },
17 | "author": "",
18 | "license": "MIT",
19 | "bugs": {
20 | "url": "https://github.com/oslabs-beta/Qache/issues"
21 | },
22 | "homepage": "https://github.com/oslabs-beta/Qache#readme"
23 | }
24 |
--------------------------------------------------------------------------------
/qache-app/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@babel/preset-env",
5 | {
6 | "modules": false
7 | }
8 | ],
9 | "@babel/preset-react"
10 | ],
11 | "plugins": ["react-require"]
12 | }
13 |
--------------------------------------------------------------------------------
/qache-app/.gitignore:
--------------------------------------------------------------------------------
1 | .cache/
2 | coverage/
3 | **/node_modules
4 | *.log
5 | **/.vscode
6 |
7 | #.env files contain passwords/api keys
8 | **/*.env
9 |
10 | # OS generated files
11 | .DS_Store
12 | .DS_Store?
13 | ._*
14 | .Spotlight-V100
15 | .Trashes
16 | ehthumbs.db
17 | Thumbs.db
18 |
--------------------------------------------------------------------------------
/qache-app/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "tabWidth": 2,
4 | "semi": true,
5 | "singleQuote": true,
6 | "printWidth": 80,
7 | "jsxSingleQuote": true,
8 | "useTabs": false,
9 | "useEditorConfig": true,
10 | "htmlWhitespaceSensitivity": "css",
11 | "bracketSpacing": true,
12 | "arrowParens": "always"
13 | }
14 |
--------------------------------------------------------------------------------
/qache-app/client/App.tsx:
--------------------------------------------------------------------------------
1 | import { Routes, Route } from 'react-router-dom';
2 | import Home from './components/home/Home';
3 | import DemoApp from './components/demo-app/DemoApp';
4 | import Docs from './components/home/Docs'
5 |
6 | const App = () => {
7 | return (
8 | <>
9 |
10 | } />
11 | } />
12 | } />
13 |
14 | >
15 | );
16 | };
17 |
18 | export default App;
19 |
--------------------------------------------------------------------------------
/qache-app/client/components/demo-app/DemoApp.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { Routes, Route } from 'react-router-dom';
3 | import { Metric } from '../../../interfaces';
4 | import Navigation from './Navigation';
5 | import LandingPage from './LandingPage';
6 | import ProductDisplay from './ProductDisplay';
7 | import '../../styles/demo-styles/Navigation.scss';
8 |
9 | const DemoApp = () => {
10 | const [sidebar, setSidebar] = useState(false);
11 | const [refresh, setRefresh] = useState(false);
12 | const hideSidebar = () => setSidebar(false);
13 | const [metrics, setMetrics] = useState({
14 | Bedroom: {
15 | labels: [],
16 | data: [],
17 | },
18 | Mattresses: {
19 | labels: [],
20 | data: [],
21 | },
22 | Furniture: {
23 | labels: [],
24 | data: [],
25 | },
26 | Storage: {
27 | labels: [],
28 | data: [],
29 | },
30 | 'Living Room': {
31 | labels: [],
32 | data: [],
33 | },
34 | Kitchen: {
35 | labels: [],
36 | data: [],
37 | },
38 | Bathroom: {
39 | labels: [],
40 | data: [],
41 | },
42 | Appliances: {
43 | labels: [],
44 | data: [],
45 | },
46 | Couches: {
47 | labels: [],
48 | data: [],
49 | },
50 | Deals: {
51 | labels: [],
52 | data: [],
53 | },
54 | });
55 |
56 | return (
57 | <>
58 | {
62 | hideSidebar();
63 | }
64 | : undefined
65 | }
66 | className={sidebar ? 'sidebar-overlay' : ''}
67 | />
68 |
71 |
72 | } />
73 |
84 | }
85 | />
86 |
97 | }
98 | />
99 |
110 | }
111 | />
112 |
123 | }
124 | />
125 |
136 | }
137 | />
138 |
149 | }
150 | />
151 |
162 | }
163 | />
164 |
175 | }
176 | />
177 |
188 | }
189 | />
190 |
201 | }
202 | />
203 |
204 | >
205 | );
206 | };
207 |
208 | export default DemoApp;
209 |
--------------------------------------------------------------------------------
/qache-app/client/components/demo-app/LandingPage.tsx:
--------------------------------------------------------------------------------
1 | import '../../styles/demo-styles/LandingPage.scss';
2 | import LineGraph from './LineGraph';
3 | import storage from '../../images/storage.jpg';
4 |
5 | const LandingPage = () => {
6 | return (
7 | <>
8 |
9 |
10 |
11 |
12 |
13 |
Find the right products for your dream room using our demo!
14 |
15 |
16 |
17 |
18 |
19 | Experience our caching solution when searching for products!
20 |
21 |
29 |
30 |
31 |
32 | >
33 | );
34 | };
35 |
36 | export default LandingPage;
37 |
--------------------------------------------------------------------------------
/qache-app/client/components/demo-app/LineGraph.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Chart as ChartJS,
3 | LineElement,
4 | PointElement,
5 | CategoryScale,
6 | LinearScale,
7 | Title,
8 | Tooltip,
9 | Legend,
10 | } from 'chart.js';
11 |
12 | import { Line } from 'react-chartjs-2';
13 |
14 | import { Metric, Dataset } from '../../../interfaces';
15 |
16 | ChartJS.register(
17 | CategoryScale,
18 | LinearScale,
19 | LineElement,
20 | PointElement,
21 | Title,
22 | Tooltip,
23 | Legend
24 | );
25 |
26 | const LineGraph = ({
27 | metrics,
28 | width,
29 | height,
30 | }: {
31 | metrics: Metric;
32 | width: number;
33 | height: number;
34 | }) => {
35 | const state: {
36 | labels: string[] | undefined;
37 | datasets: Dataset[];
38 | } = {
39 | labels: [],
40 | datasets: [
41 | {
42 | label: ' ms',
43 | fill: false,
44 | lineTension: 0.35,
45 | backgroundColor: 'rgba(75,192,192,1)',
46 | borderColor: 'rgba(0,0,0,1)',
47 | borderWidth: 2,
48 | data: [],
49 | },
50 | ],
51 | };
52 |
53 | const options = {
54 | responsive: true,
55 | plugins: {
56 | legend: {
57 | display: false,
58 | },
59 | },
60 | };
61 |
62 | state.labels = metrics.labels;
63 | state.datasets[0].data = metrics.data;
64 |
65 | return (
66 | <>
67 |
68 | >
69 | );
70 | };
71 |
72 | export default LineGraph;
73 |
--------------------------------------------------------------------------------
/qache-app/client/components/demo-app/Navigation.tsx:
--------------------------------------------------------------------------------
1 | import axios, { AxiosResponse } from 'axios';
2 | import { useState } from 'react';
3 | import '../../styles/demo-styles/Navigation.scss';
4 | import { useLocation } from 'react-router-dom';
5 | import { HashLink as Link } from 'react-router-hash-link';
6 | import { FaBars, FaSearch } from 'react-icons/fa';
7 | import { AiOutlineClose } from 'react-icons/ai';
8 | import { IoMdRefresh } from 'react-icons/io';
9 | import { SidebarData } from './SidebarData';
10 | import { IconContext } from 'react-icons';
11 | import SubMenu from './SubMenuData';
12 |
13 | const Navigation = ({ props }: { props: any }) => {
14 | const {
15 | refresh,
16 | setRefresh,
17 | sidebar,
18 | setSidebar,
19 | hideSidebar,
20 | }: {
21 | refresh: boolean;
22 | setRefresh: React.Dispatch
>;
23 | sidebar: boolean;
24 | setSidebar: React.Dispatch>;
25 | hideSidebar: () => void;
26 | } = props;
27 |
28 | const [productMenu, setProductMenu] = useState(false);
29 | const [roomMenu, setRoomMenu] = useState(false);
30 |
31 | const showSidebar = () => setSidebar(true);
32 |
33 | const showSubMenu = (title: String) => {
34 | if (title === 'Products') setProductMenu(true);
35 | if (title === 'Rooms') setRoomMenu(true);
36 | };
37 |
38 | let location = useLocation();
39 | const invalidateCache = () => {
40 | const body ={query: `
41 | {
42 | invalidate
43 | }
44 | `}
45 | const uri = process.env.NODE_ENV === 'development' ? 'http://localhost:3000/graphql': '/graphql'
46 | axios.post(uri, body)
47 | }
48 |
49 | return (
50 | <>
51 |
52 |
53 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | {SidebarData.map((item: any, index: number) => {
107 | return (
108 |
118 | );
119 | })}
120 |
121 |
122 |
123 |
124 | >
125 | );
126 | };
127 |
128 | export default Navigation;
129 |
--------------------------------------------------------------------------------
/qache-app/client/components/demo-app/ProductDetails.tsx:
--------------------------------------------------------------------------------
1 | import { Product } from '../../../interfaces';
2 | import '../../styles/demo-styles/ProductDetails.scss';
3 | import axios, { AxiosResponse } from 'axios'
4 |
5 | const ProductDetails = ({ productData }: { productData: Product[] }) => {
6 |
7 | /* Add to cart button: when clicked, sends product.id to cart
8 | which should add that product to the cart for checkout */
9 | // const addToCart = (product: { inCart: boolean; }) => {
10 | // product.inCart = true;
11 | // }
12 |
13 | // /* Remove from cart button: when clicked, sends product.id to cart
14 | // which should remove that product from the cart */
15 | // const removeFromCart = (product: { inCart: boolean; }) => {
16 | // product.inCart = false;
17 | // }
18 | return (
19 |
20 | {productData.map((product, key) => (
21 |
22 | {product.onSale ? (
23 | <>
24 |
25 | 50% OFF
26 |
27 |
28 | >
29 | ) : (
30 | <>
31 |
32 | >
33 | )}
34 |
35 |
{product.name}
36 | {product.onSale ? (
37 |
38 | $
39 |
40 | {product.price.toFixed(2)}
41 |
42 |
43 | $
44 |
45 |
46 | {(Math.floor((product.price / 2) * 100) / 100).toFixed(2)}
47 |
48 |
49 |
50 | ) : (
51 |
52 | $
53 | {product.price.toFixed(2)}
54 |
55 | )}
56 | {product.quantity ? (
57 |
{product.quantity} in stock
58 | ) : (
59 |
Out of stock
60 | )}
61 |
{product.description}
62 | {/*
{
63 | let body = {
64 | query: `mutation {
65 | updateProduct (id: ${product.id}, inCart: true)
66 | }`
67 | }
68 | axios
69 | .post('http://localhost:3000/graphql', body)
70 | .then(({data}: AxiosResponse) => {
71 | })
72 | }}>Add to cart
73 |
{product.inCart = false}}>Remove from cart */}
74 |
75 |
76 | ))}
77 |
78 | );
79 | };
80 |
81 | export default ProductDetails;
82 |
--------------------------------------------------------------------------------
/qache-app/client/components/demo-app/ProductDisplay.tsx:
--------------------------------------------------------------------------------
1 | import axios, { AxiosResponse } from 'axios';
2 | import { useState, useEffect } from 'react';
3 | import { Product, Metric } from '../../../interfaces';
4 | import ProductDetails from './ProductDetails';
5 | import LineGraph from './LineGraph';
6 | import '../../styles/demo-styles/ProductDisplay.scss';
7 |
8 | const ProductDisplay = ({ props }: { props: any }) => {
9 | const {
10 | category,
11 | setMetrics,
12 | metrics,
13 | refresh,
14 | }: {
15 | category: string;
16 | setMetrics: React.Dispatch>;
17 | metrics: Metric;
18 | refresh: boolean;
19 | setRefresh: React.Dispatch>;
20 | } = props;
21 | const [productData, setProductData] = useState([]);
22 | const [speed, setSpeed] = useState([]);
23 |
24 | let body = {
25 | query: `
26 | {
27 | getProductsBy(category: "${category}") {
28 | name
29 | description
30 | imageUrl
31 | quantity
32 | price
33 | onSale
34 | category
35 | }
36 | }`,
37 | };
38 |
39 | useEffect(() => {
40 | if (category === 'Deals') {
41 | body = {
42 | query: `{
43 | filterProductsBy(filter: {
44 | onSale: true
45 | }) {
46 | name
47 | description
48 | imageUrl
49 | quantity
50 | price
51 | onSale
52 | }
53 | }`,
54 | };
55 | }
56 | const t1 = Date.now(); // time before axios post starts
57 | const uri = process.env.NODE_ENV === 'development' ? 'http://localhost:3000/graphql': '/graphql'
58 | axios
59 | .post(uri, body)
60 | .then(({ data }: AxiosResponse) => {
61 | const t2 = Date.now();
62 | setSpeed([...speed, t2 - t1]);
63 | if (data.data.getProductsBy) setProductData(data.data.getProductsBy);
64 | else if (data.data.filterProductsBy)
65 | setProductData(data.data.filterProductsBy);
66 | });
67 | }, [category, refresh]);
68 |
69 | useEffect(() => {
70 | if (speed.length) {
71 | const newMetrics = { ...metrics };
72 | let prevLabel =
73 | newMetrics[category].labels[newMetrics[category].labels.length - 1];
74 | if (!prevLabel) prevLabel = '0';
75 | newMetrics[category].labels.push(String(Number(prevLabel) + 1));
76 | newMetrics[category].data.push(speed[speed.length - 1]);
77 | setMetrics(newMetrics);
78 | }
79 | }, [speed]);
80 |
81 | return (
82 |
83 |
84 |
85 | Server Latency
86 | Fetches
87 | Cache Speed for {category}
88 |
89 |
90 |
91 | This Chart represents the latency to the server, where the
92 | content for this page was fetched from.
93 |
94 |
95 | When the server needs to receive data from the database, these fetches
96 | can take very long times.
97 |
98 |
99 | Feel free to click the refresh button at the top right, and experience
100 | the speeds our caching solution provides.
101 |
102 |
103 | Our library allows caching data per page, category, single
104 | pieces of information, whatever you need!
105 |
106 |
107 | In addition we allow support for mutations including
108 | create, delete, and update - where only the
109 | relevant data in the cache is updated, immediately!
110 |
111 |
112 |
113 |
114 | {productData ? (
115 |
116 | ) : (
117 |
No items found
118 | )}
119 |
120 | );
121 | };
122 |
123 | export default ProductDisplay;
124 |
--------------------------------------------------------------------------------
/qache-app/client/components/demo-app/SidebarData.js:
--------------------------------------------------------------------------------
1 | import { FaChair, FaToilet, FaCouch, FaBed } from 'react-icons/fa';
2 | import { AiFillHome } from 'react-icons/ai';
3 | import { IoMdFlash } from 'react-icons/io';
4 | import { RiArrowUpSFill, RiArrowDownSFill } from 'react-icons/ri';
5 | import { BiCabinet } from 'react-icons/bi';
6 | import { GiKnifeFork, GiSofa } from 'react-icons/gi';
7 | import { MdKitchen } from 'react-icons/md';
8 | import { FiMonitor } from 'react-icons/fi';
9 |
10 | export const SidebarData = [
11 | {
12 | title: 'Home',
13 | path: '/demo-app',
14 | icon: ,
15 | cName: 'nav-text',
16 | },
17 | {
18 | title: 'Products',
19 | path: '#',
20 | icon: ,
21 | iconClosed: ,
22 | iconOpened: ,
23 | subNav: [
24 | {
25 | title: 'Appliances',
26 | path: 'products/appliances',
27 | icon: ,
28 | },
29 | {
30 | title: 'Couches',
31 | path: 'products/couches',
32 | icon: ,
33 | },
34 | {
35 | title: 'Furniture',
36 | path: 'products/furnitures',
37 | icon: ,
38 | },
39 | {
40 | title: 'Mattresses',
41 | path: 'products/mattresses',
42 | icon: ,
43 | },
44 | {
45 | title: 'Storage',
46 | path: 'products/storage',
47 | icon: ,
48 | },
49 | ],
50 | cName: 'nav-text',
51 | },
52 | {
53 | title: 'Rooms',
54 | path: '#',
55 | icon: ,
56 | iconClosed: ,
57 | iconOpened: ,
58 | subNav: [
59 | {
60 | title: 'Bathroom',
61 | path: 'rooms/bathroom',
62 | icon: ,
63 | },
64 | {
65 | title: 'Bedroom',
66 | path: 'rooms/bedroom',
67 | icon: ,
68 | },
69 | {
70 | title: 'Kitchen',
71 | path: 'rooms/kitchen',
72 | icon: ,
73 | },
74 | {
75 | title: 'Living Room',
76 | path: 'rooms/living-room',
77 | icon: ,
78 | },
79 | ],
80 | cName: 'nav-text',
81 | },
82 | {
83 | title: 'Deals',
84 | path: 'deals',
85 | icon: ,
86 | cName: 'nav-text',
87 | },
88 | ];
89 |
--------------------------------------------------------------------------------
/qache-app/client/components/demo-app/SubMenuData.tsx:
--------------------------------------------------------------------------------
1 | import '../../styles/demo-styles/SubMenuData.scss';
2 | import { useState, useEffect } from 'react';
3 | import { Link } from 'react-router-dom';
4 | import { Item } from '../../../interfaces';
5 |
6 | const SubMenu = ({
7 | item,
8 | sidebar,
9 | productMenu,
10 | setProductMenu,
11 | roomMenu,
12 | setRoomMenu,
13 | hideSidebar,
14 | }: {
15 | item: Item;
16 | sidebar: boolean;
17 | productMenu: boolean;
18 | setProductMenu: React.Dispatch>;
19 | roomMenu: boolean;
20 | setRoomMenu: React.Dispatch>;
21 | hideSidebar: () => void;
22 | }) => {
23 | const [subnav, setSubnav] = useState(false);
24 |
25 | const showSubNav = () => setSubnav(!subnav);
26 |
27 | useEffect(() => {
28 | setSubnav(false);
29 | setProductMenu(false);
30 | setRoomMenu(false);
31 | }, [sidebar]);
32 |
33 | useEffect(() => {
34 | if (item.title === 'Products') setSubnav(true);
35 | }, [productMenu]);
36 |
37 | useEffect(() => {
38 | if (item.title === 'Rooms') setSubnav(true);
39 | }, [roomMenu]);
40 |
41 | return (
42 |
43 |
48 |
49 | {item.icon}
50 | {item.title}
51 |
52 |
53 | {item.subNav && subnav
54 | ? item.iconOpened
55 | : item.subNav
56 | ? item.iconClosed
57 | : null}
58 |
59 |
60 |
61 | {item.subNav
62 | ? subnav &&
63 | item.subNav.map((item: Item, index: number) => {
64 | return (
65 |
71 |
{item.icon}
72 |
{item.title}
73 |
74 | );
75 | })
76 | : null}
77 |
78 | );
79 | };
80 |
81 | export default SubMenu;
82 |
--------------------------------------------------------------------------------
/qache-app/client/components/demo-app/images/transparentlogowithslogan.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Qache/dd306d760354bd24b40f1c821ad170ef9e290271/qache-app/client/components/demo-app/images/transparentlogowithslogan.png
--------------------------------------------------------------------------------
/qache-app/client/components/home/Docs.tsx:
--------------------------------------------------------------------------------
1 | import { HashLink as Link } from 'react-router-hash-link';
2 | import Navbar from './Navbar';
3 | import '../../styles/home-styles/Docs.scss'
4 |
5 | const scrollWithOffset = (el:any) => {
6 | const yCoordinate = el.getBoundingClientRect().top + window.pageYOffset;
7 | const yOffset = -120;
8 | window.scrollTo({ top: yCoordinate + yOffset, behavior: 'smooth' });
9 | }
10 |
11 |
12 | const Docs = () => {
13 | return (
14 | <>
15 |
16 |
17 | {/* SIDE NAVIGATION BAR */}
18 |
19 |
Quick Start
20 |
21 | scrollWithOffset(el)}>Install
22 | scrollWithOffset(el)}>Instantiate
23 | scrollWithOffset(el)}>Set Your First Key
24 | scrollWithOffset(el)}>Get Your Data
25 |
26 |
Commands
27 |
28 | scrollWithOffset(el)}>get
29 | scrollWithOffset(el)}>set
30 | scrollWithOffset(el)}>update
31 | scrollWithOffset(el)}>delete
32 | scrollWithOffset(el)}>listCreate
33 | scrollWithOffset(el)}>listRange
34 | scrollWithOffset(el)}>invalidate
35 | scrollWithOffset(el)}>listPush
36 | scrollWithOffset(el)}>listFetch
37 | scrollWithOffset(el)}>listUpdate
38 | scrollWithOffset(el)}>listUpsert
39 | scrollWithOffset(el)}>listRemoveItem
40 |
41 |
scrollWithOffset(el)}>
FAQ
42 | {/* CONTENT SECTION */}
43 |
44 |
45 |
46 |
Quick Start
47 |
48 |
49 |
Installation
50 |
Installation is a very simple process. Start with a quick npm install qache
51 |
52 | npm install qache
53 |
54 |
This will add qache to your servers dependencies. Then, choose a home for your cache, somewhere where it can be easily referenced. It is recommended you keep it in the same file that you make your database calls.
55 |
56 |
57 |
58 |
Instantiate
59 |
Once you have picked a file to set up your cache, the process is simple.
60 |
61 | const
Qache = require
('qache'
) //require in qache
62 | const
cache = new
Qache
() //This line will create a default instance of our Qache
63 |
64 |
65 | Awesome! With just those two lines, every time you run your server, you have access to powerful caching capabilities.
66 | Now you have some decisions to make: in particular, some settings to apply to your cache.
67 |
68 |
69 |
70 | const
policies = {'{'}
71 | timeToLive: 1000
*
60
*
10
//time is expected in ms, this line represents 10min
72 | maxSize: 100
73 | evictionPolicy: "LFU"
74 | {'}'}
75 | const
cache = new
Qache
(policies)// Pass your policies into the constructor like so
76 |
77 |
78 |
79 | All of these fields are optional, but important to consider.
80 | The eviction policy options are LRU (Least Recently Used - this is selected by default) and LFU (Least Frequently Used).
81 | maxSize and timeToLive are infinte by default. If you have a large amount of data in your database and can't cache everything, it is recommended that you put some limitations on either maxSize or timeToLive.
82 |
83 |
84 |
85 |
Set Your First Key
86 |
87 | To start caching data all we have to do is set our first key!
88 | Keep in mind, we need to catch the data after the database response, but before the return.
89 |
90 |
91 |
92 | getProductById
: async
function
(args){'{'}
93 | const
data =
await
Product.findOne
(args.id); //Your standard DB query
94 | cache.set
(args.id, data); //add this new data to cache
95 | return
data;
96 | {'}'}
97 |
98 |
99 |
Congrats! You just added your first piece of data to cache... but something is missing.
100 |
101 |
102 |
Get Your Data
103 |
What would be the point of adding data to cache, without using it.
104 |
105 |
106 | getProductById
: async
function
(args){'{'}
107 | const
cacheResponse =
cache.get
(args.id); //Check the cache under the unique key id
108 | if
(cacheResponse) return
cacheResponse; // If the cache has what we're looking for, we return it.
109 | const
data =
await
Product.findOne
(args.id); //Your standard DB query
110 | cache.set
(args.id, data); //add this new data to cache
111 | return
data;
112 | {'}'}
113 |
114 |
115 |
116 | Now that's what we're talking about! In 5 lines, we check our cache, and set the data to cache if it wasn't found the first time.
117 | Below you can find a complete list of the commands, and be sure to consider maintaining the validity of your data.
118 | There are commands for every mutation!
119 |
120 |
121 |
122 |
123 |
124 |
Commands
125 |
126 |
get(key)
127 |
Time Complexity: O(1)
128 |
Used to retrieve data stored under a predetermined key. See "Set" below
129 |
130 |
131 |
set(key, value)
132 |
Time Complexity: O(1)
133 |
Intended for storing singleton data, i.e. data with a unique identifier. This function has many uses, such as storing data by id, or by a username, so it can easily save you many trips to a database to retrieve those pieces of data.
134 |
135 |
136 |
update(key, value)
137 |
Time Complexity: O(1)
138 |
Used to update data of a key that already exists
139 |
140 |
141 |
delete(key)
142 |
Time Complexity: O(1)
143 |
Removes a key that should no longer be in cache, such as if an item was deleted out of the database, and the user wants the stored data in cache to stay fresh.
144 |
145 |
146 |
invalidate(...keys)
147 |
Time Complexity: O(n) where n = number of keys requiring deletion
148 |
invalidate, or "lazy invalidation". This command can take in one or more keys, and delete the data, and the nodes associated with each key. If you run into an event that leads to a complicated situation when trying to keep data fresh and valid, consider invalidating the cache after such an event. We call this method lazy, because it is to be used as a last resort. We believe our methods can keep most sets of data fresh in cache for a long time.
149 |
150 |
151 |
listCreate(listKey, ...item)
152 |
Time Complexity: O(1)
153 |
Intended to store data in a systematic way. By page, by category, or by anything else that can identify a group. After you have retrieved a certain dataset, store it under a key that identifies that group, to retrieve it faster later.
154 |
155 |
156 |
listRange(listKey, start, end)
157 |
Time Complexity: O(1) if no range specified, otherwise O(n) where n is the length of the list
158 |
Used to retrieve a previously stored list. You can return an entire list by just passing in the list's unique key, or you can get a range, to return e.g. only the first 10, or the last 20.
159 |
160 |
161 |
listFetch(listKey, filterObject)
162 |
Time Complexity: O(n) where n is the length of the list
163 |
Used similarly to listRange, this function takes a filterObject. Pass in the key of whatever you want to filter by, and the value you're looking for, e.g. {'{'}category: "couch"{'}'} to retrieve all results that match that description.
164 |
165 |
166 |
listPush(item, ...listKeys)
167 |
Time Complexity: O(1)
168 |
This function can be used to add new items to an existing list. For example, if we just added a new listing and don't want to invalidate our cache, we can just listPush the new listing to the existing list.
169 |
170 |
171 |
listUpdate(updatedItem, filterObject, ...listKey)
172 |
Time Complexity: O(n) where n is the length of the list
173 |
Used to update the value of items currently in a list. Similar to the previous function, if we want to maintain freshness of our cache, we can use this function to update each list that a given item belonged to. This method takes in a filterObject, which can be used with a unique identifier to find the item in need of updating.
174 |
175 |
176 |
listUpsert(item, filterObject, ...listKeys)
177 |
Time Complexity: O(n) where n is the length of the list
178 |
This function is dual-purpose. Similar to both listPush and listUpdate, this method will scan one or multiple lists for an item. Then, when the item is found, the function updates it, and if the item is not found, the item is added to the given list(s). Very useful in tricky situations, when you don't know if an item has been added in your cache.
179 |
180 |
181 |
listRemoveItem(filterObject, ...listKey)
182 |
Time Complexity: O(n) where n is the length of the list
183 |
As the name suggests, this function removes an item from one or several lists in your cache. It is useful when an item has been deleted from the database, and you want to reflect that instantly in your cache.
184 |
185 |
186 |
187 |
188 |
FAQ
189 |
190 |
How is Qache different from other key-value stores?
191 |
192 | Qache's main draws are its ease of use and gentle learning curve. But behind that, lies a powerful and modular suite of methods to handle complex situations.
193 | Everything is handled in the same server being serviced by the cache. This has several important consequences:
194 |
Qache horizontally scales in tandem with servers. Each server AUTOMATICALLY has its own instance of qache! Keep in mind however that you will need to adopt an approach to handle cache updates, such as employing a service worker process.
195 |
Qache requires very little upkeep after it's initial set up.
196 |
In most cases Qache fits perfectly into an ACID compliant system, and can be integrated into existing systems with ease.
197 |
198 |
199 |
200 |
Will a Qache ever be a bottleneck for my server?
201 |
202 | It is nearly impossible for Qache to be the bottleneck of your server. We set out to break apart the bottlenecks that come with high site traffic, or large amounts of database queries, even more specifically GraphQL queries, which are known to sometimes make multiple DB queries in a single request.
203 | We accomplished that task, and surpassed our expectations.
204 | To be more specific, every Qache read operation can be performed in constant time, or if neccesary, e.g. for filtered results, linear time. This means Qache can get through millions of requests without ever getting bogged down.
205 |
206 |
207 |
208 |
Is there a way to control how much memory is used?
209 |
210 | We have built in a few powerful settings that allow for you to use up as much or as little memory as you want by setting a max key limit or a short expiration date.
211 | If those settings don't suffice, you can take further steps to store your data in stringified form, and parse it when you receive it.
212 |
213 |
214 |
215 |
What's a good strategy for keeping data in cache accurate?
216 |
217 | It varies from case to case, but the greatest rule of thumb is this: if a request is making an update, it should also update the cache. And the same goes for deleting and creating. Whatever database action is occurring, mirror it with a cache action.
218 | If the idea of an inaccurate cache scares you, cache.invalidate() after every mutation will ease your worries!
219 |
220 |
221 |
222 |
How is Qache pronounced?
223 |
224 | kwaash :{')'}
225 |
226 |
227 |
228 |
229 |
230 |
231 | >
232 | );
233 | };
234 |
235 | export default Docs;
236 |
--------------------------------------------------------------------------------
/qache-app/client/components/home/Home.tsx:
--------------------------------------------------------------------------------
1 | import { Routes, Route } from 'react-router-dom';
2 | import Navbar from './Navbar';
3 | import Introduction from './Introduction';
4 |
5 | const Home = () => {
6 | return (
7 | <>
8 |
9 |
10 | } />
11 |
12 | >
13 | )
14 | };
15 |
16 | export default Home;
--------------------------------------------------------------------------------
/qache-app/client/components/home/Introduction.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from 'react-router-dom';
2 | import '../../styles/home-styles/Introduction.scss';
3 | import { MdOutlineArrowForwardIos } from 'react-icons/md';
4 | import { AiOutlineArrowRight } from 'react-icons/ai';
5 | import LineGraph from '../demo-app/LineGraph';
6 | import Team from './Team';
7 | import preview from '../../images/preview.gif';
8 | import demo from '../../images/demo.gif';
9 | import navigate from '../../images/navigate.gif';
10 | import qache from '../../images/qache.gif';
11 | import { motion } from 'framer-motion';
12 | import { useInView } from 'react-intersection-observer';
13 |
14 | const Introduction = () => {
15 | const [ref1, inView1] = useInView({
16 | triggerOnce: true,
17 | });
18 | const [ref2, inView2] = useInView({
19 | triggerOnce: true,
20 | });
21 | const [ref3, inView3] = useInView({
22 | triggerOnce: true,
23 | });
24 | const [ref4, inView4] = useInView({
25 | triggerOnce: true,
26 | });
27 |
28 | const copyToClipboard = () => {
29 | const range = document.createRange();
30 | range.selectNode(document.getElementsByClassName('copy-button')[0]);
31 | window.getSelection()?.removeAllRanges();
32 | window.getSelection()?.addRange(range);
33 | document.execCommand('copy');
34 | window.getSelection()?.removeAllRanges();
35 | };
36 |
37 | return (
38 | <>
39 |
64 |
65 |
77 |
78 |
79 |
88 |
89 |
94 |
99 |
100 |
101 |
102 |
103 |
104 |
112 |
113 |
114 |
115 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
Qache Your Data
130 |
131 | Qache is a modular utility class for handling server side caching of
132 | data. It works with any database and API architecture including
133 | GraphQL and RESTful APIs.
134 |
135 |
136 |
137 |
145 |
146 |
147 |
148 |
149 |
150 |
Demo our Qache Tool
151 |
152 |
159 |
165 | Preview
166 |
167 | We built a Demo application to see in real time how our caching
168 | solution would work on a mock database we created with MongoDB
169 | and GraphQL. Behind the scenes, we've implemented our Qache
170 | methods to handle query performance, optimizing it with server
171 | side caching.
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
186 |
192 | Browsing Through
193 |
194 | In our Demo application, you can browse through the navigation
195 | bar and sidebar, allowing you to go through our products, rooms,
196 | and deals.
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
211 |
217 | Latency Graph
218 |
219 | Our Demo application shows a line graph for each of the pages.
220 | The landing page has an example line graph of what will be shown
221 | when you visit any of the pages. The line graph shows the speed
222 | in miliseconds on the y-axis and the number of fetches/queries
223 | to the database and our server side cache on the x-axis.
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
238 |
244 | Qache Tool
245 |
246 | The first point on the line graph describes how fast our query
247 | takes to fetch the data from the database and returning it back
248 | to the client which displays all the products on the page.
249 | Behind the scenes, our qache tool stashes that response data
250 | into the cache. Above the graph is a refresh button that allows
251 | you to refetch the data. It creates a new data point in the line
252 | graph every time you click on it, but instead of fetching the
253 | data from the database again, it checks if the data lies in the
254 | server side cache first, and if it is, it will return the data
255 | from the cache, cutting the time needed to go to the database.
256 |
257 |
258 |
259 |
260 |
261 |
262 | >
263 | );
264 | };
265 |
266 | export default Introduction;
267 |
--------------------------------------------------------------------------------
/qache-app/client/components/home/Navbar.tsx:
--------------------------------------------------------------------------------
1 | import '../../styles/home-styles/Navbar.scss';
2 | import { HashLink as Link } from 'react-router-hash-link';
3 | import { FaGithub, FaLinkedin } from 'react-icons/fa';
4 | import { IconContext } from 'react-icons';
5 | import qache from '../../images/qache.png';
6 |
7 | const scrollWithOffset = (el:any) => {
8 | const yCoordinate = el.getBoundingClientRect().top + window.pageYOffset;
9 | const yOffset = -120;
10 | window.scrollTo({ top: yCoordinate + yOffset, behavior: 'smooth' });
11 | }
12 |
13 | const Navbar = () => {
14 | return (
15 | <>
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | Demo App
28 |
29 |
30 |
31 |
32 | Docs
33 |
34 |
35 |
36 | scrollWithOffset(el)} className='nav-link'>
37 | Meet The Team
38 |
39 |
40 |
41 |
42 |
66 |
67 |
68 | >
69 | );
70 | };
71 |
72 | export default Navbar;
73 |
--------------------------------------------------------------------------------
/qache-app/client/components/home/Team.tsx:
--------------------------------------------------------------------------------
1 | import { FaGithub, FaLinkedin } from 'react-icons/fa';
2 |
3 | const shuffleArray = (array: {}[]) => {
4 | const outputArray: {}[] = [];
5 | const used: any = new Set();
6 | while (outputArray.length !== array.length) {
7 | const rand: number = Math.floor(Math.random() * array.length);
8 | if (!used.has(array[rand])) {
9 | outputArray.push(array[rand]);
10 | used.add(array[rand]);
11 | } else continue;
12 | }
13 | return outputArray;
14 | };
15 |
16 | const unshuffledPeople = [
17 | {
18 | name: 'Nader Almogazy',
19 | role: 'Full-Stack Engineer',
20 | imageUrl:
21 | 'https://media-exp1.licdn.com/dms/image/C4E03AQF8fJfVlCNFPg/profile-displayphoto-shrink_800_800/0/1639494320094?e=1649289600&v=beta&t=9tnZvGoJuLvmwoWfCPmmJCTGHCiP6wy06sbLKskn4k8',
22 | githubUrl: 'https://github.com/nader12334',
23 | linkedinUrl: 'https://www.linkedin.com/in/naderalmogazy/',
24 | },
25 | {
26 | name: 'Steven Du',
27 | role: 'Full-Stack Engineer',
28 | imageUrl:
29 | 'https://media-exp1.licdn.com/dms/image/C5603AQFDqqzbMsHLkw/profile-displayphoto-shrink_800_800/0/1614478246591?e=1649289600&v=beta&t=rF9o-U-6HfdMOjy_qSy5iqperqrRcU0s9N47fvOam3c',
30 | githubUrl: 'https://github.com/stebed',
31 | linkedinUrl: 'https://www.linkedin.com/in/stevendu/',
32 | },
33 | {
34 | name: 'Evan Preedy',
35 | role: 'Full-Stack Engineer',
36 | imageUrl:
37 | 'https://media-exp1.licdn.com/dms/image/C4D03AQHJYHhjminKOQ/profile-displayphoto-shrink_800_800/0/1583715406957?e=1649289600&v=beta&t=Xe-g-MbDEGdm9EQA0Wsx0LBqrxMDpysZjMxAKXixZKY',
38 | githubUrl: 'https://github.com/ep1815',
39 | linkedinUrl: 'https://www.linkedin.com/in/evan-preedy/',
40 | },
41 | {
42 | name: 'Leo Crossman',
43 | role: 'Full-Stack Engineer',
44 | imageUrl:
45 | 'https://media-exp1.licdn.com/dms/image/C5603AQGv8VLWelgLgA/profile-displayphoto-shrink_800_800/0/1540586637094?e=1649289600&v=beta&t=0fynsTHefEKnH-6JtLjh0Djy5-cQXmX6E_QtzptnO2w',
46 | githubUrl: 'https://github.com/leocrossman',
47 | linkedinUrl: 'https://www.linkedin.com/in/leocrossman',
48 | },
49 | ];
50 |
51 | const people: {}[] = shuffleArray(unshuffledPeople);
52 |
53 | const Team = () => {
54 | return (
55 |
56 |
57 |
58 |
59 |
60 | Meet our team
61 |
62 |
63 | Connect with our engineers!
64 |
65 |
66 |
70 | {people.map((person: any) => (
71 |
75 |
76 |
81 |
82 |
83 |
{person.name}
84 |
{person.role}
85 |
86 |
87 |
125 |
126 |
127 |
128 | ))}
129 |
130 |
131 |
132 |
133 | );
134 | };
135 |
136 | export default Team;
137 |
--------------------------------------------------------------------------------
/qache-app/client/images/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Qache/dd306d760354bd24b40f1c821ad170ef9e290271/qache-app/client/images/demo.gif
--------------------------------------------------------------------------------
/qache-app/client/images/navigate.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Qache/dd306d760354bd24b40f1c821ad170ef9e290271/qache-app/client/images/navigate.gif
--------------------------------------------------------------------------------
/qache-app/client/images/preview.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Qache/dd306d760354bd24b40f1c821ad170ef9e290271/qache-app/client/images/preview.gif
--------------------------------------------------------------------------------
/qache-app/client/images/qache.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Qache/dd306d760354bd24b40f1c821ad170ef9e290271/qache-app/client/images/qache.gif
--------------------------------------------------------------------------------
/qache-app/client/images/qache.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Qache/dd306d760354bd24b40f1c821ad170ef9e290271/qache-app/client/images/qache.png
--------------------------------------------------------------------------------
/qache-app/client/images/qacheslogan.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Qache/dd306d760354bd24b40f1c821ad170ef9e290271/qache-app/client/images/qacheslogan.png
--------------------------------------------------------------------------------
/qache-app/client/images/storage.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Qache/dd306d760354bd24b40f1c821ad170ef9e290271/qache-app/client/images/storage.jpg
--------------------------------------------------------------------------------
/qache-app/client/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as ReactDOM from 'react-dom';
3 | import { BrowserRouter } from 'react-router-dom';
4 | import App from './App';
5 | import './styles/index.css';
6 | import './styles/index.scss';
7 |
8 | // import { store } from './Redux/store';
9 | // import { Provider } from 'react-redux';
10 |
11 | ReactDOM.render(
12 | //
13 |
14 |
15 |
16 |
17 | ,
18 | // ,
19 | document.getElementById('app')
20 | );
21 |
--------------------------------------------------------------------------------
/qache-app/client/styles/demo-styles/LandingPage.scss:
--------------------------------------------------------------------------------
1 | .landingPage-container {
2 | display: flex;
3 | flex-direction: column;
4 | h1{
5 | text-align: center;
6 | }
7 | .section-container {
8 | display: flex;
9 | justify-content: space-evenly;
10 | flex-direction: row;
11 | }
12 | .imageContainer{
13 | display: flex;
14 | position: relative;
15 | justify-content: center;
16 | align-items: center;
17 | overflow: hidden;
18 | width: 50%;
19 | }
20 | img {
21 | object-fit: cover;
22 | flex-shrink: 0;
23 | height: 80%;
24 | }
25 |
26 | .img-caption {
27 | display: flex;
28 | justify-content: flex-start;
29 | align-items: center;
30 | position: absolute;
31 | overflow: hidden;
32 | top: 75%;
33 | height: 5rem;
34 | backdrop-filter: blur(25px);
35 | h2 {
36 | font-weight: 300;
37 | }
38 | }
39 |
40 | .graph-container {
41 | display: flex;
42 | flex-direction: column;
43 | align-items: stretch;
44 | h2 {
45 | margin: 0;
46 | font-size: 2rem;
47 | text-align: center;
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/qache-app/client/styles/demo-styles/Navigation.scss:
--------------------------------------------------------------------------------
1 | @import '../variables.scss';
2 |
3 | #nav-container {
4 | display: flex;
5 | justify-content: flex-start;
6 | align-items: center;
7 | height: 10%;
8 | background-color: $white;
9 | white-space: nowrap;
10 | .navbar {
11 | display: flex;
12 | justify-content: space-evenly;
13 | align-items: center;
14 | height: 100%;
15 | width: 100%;
16 | height: 80px;
17 | padding-top: 1%;
18 | padding-left: 5%;
19 | padding-right: 5%;
20 | background-color: $offWhite;
21 | border-bottom: solid 1px #dfdfdf;
22 |
23 | .menu-bars {
24 | display: flex;
25 | justify-content: space-between;
26 | margin-left: 0;
27 | font-size: 2rem;
28 | background: none;
29 | }
30 |
31 | a {
32 | font-size: 1.5rem;
33 | font-weight: bold;
34 | text-decoration: none;
35 | color: black;
36 | cursor: pointer;
37 |
38 | &:hover,
39 | &:focus {
40 | text-decoration: underline;
41 | }
42 | }
43 |
44 | .search {
45 | width: 40%;
46 | display: flex;
47 | position: relative;
48 | input {
49 | width: 100%;
50 | padding: 10px;
51 | padding-left: 1.75rem;
52 | font-size: large;
53 | background-color: $offWhite;
54 | }
55 | .icon {
56 | position: absolute;
57 | top: 14px;
58 | left: 7px;
59 | }
60 | }
61 |
62 | .not-active {
63 | visibility: hidden;
64 | }
65 |
66 | .active {
67 | visibility: visible;
68 | background-color: transparent;
69 | border: none;
70 | cursor: pointer;
71 | font-size: xx-large;
72 |
73 | &:active {
74 | transform: translateY(1.5px);
75 | background: none;
76 | background-color: transparent;
77 | }
78 |
79 | &:active .tooltip {
80 | box-shadow: none;
81 | }
82 |
83 | .tooltip {
84 | padding: 10px;
85 | position: absolute;
86 | width: auto;
87 | white-space: nowrap;
88 | word-wrap: no-wrap;
89 | box-shadow: 1px 1px 20px #aaa;
90 | border-radius: 5px;
91 | background-color: #fff;
92 | top: 85px;
93 | left: 90.75%;
94 | transform: translate(-50%);
95 | transform-style: preserve-3d;
96 | z-index: 200;
97 | font-size: small;
98 | display: none;
99 |
100 | &:after {
101 | content: '';
102 | position: absolute;
103 | display: block;
104 | width: 10px;
105 | height: 10px;
106 | transform-origin: 50% 50%;
107 | transform: rotate(45deg) translateX(-50%);
108 | background-color: #fff;
109 | left: 50%;
110 | top: -1px;
111 | z-index: 400;
112 | }
113 | }
114 |
115 | &:hover .tooltip {
116 | display: block;
117 | }
118 |
119 | .spinner:hover {
120 | animation: spin infinite 1s linear;
121 |
122 | @keyframes spin {
123 | from {
124 | transform: rotate(0deg);
125 | }
126 | to {
127 | transform: rotate(360deg);
128 | }
129 | }
130 | }
131 | }
132 | }
133 |
134 | .nav-menu {
135 | display: flex;
136 | justify-content: center;
137 | position: fixed;
138 | width: 250px;
139 | height: 300vh;
140 | top: 0;
141 | left: -100%;
142 | transition: 750ms;
143 | background-color: $offWhite;
144 | overflow-y: scroll !important;
145 | }
146 |
147 | .nav-menu.active {
148 | padding-left: 1.5%;
149 | left: 0;
150 | z-index: 10;
151 | transition: 250ms;
152 | }
153 |
154 | ::-webkit-scrollbar {
155 | width: 0px;
156 | background: transparent; /* make scrollbar transparent */
157 | }
158 |
159 | .nav-menu-items {
160 | width: 100%;
161 |
162 | .navbar-toggle {
163 | display: flex;
164 | justify-content: flex-start;
165 | align-items: center;
166 | height: 5rem;
167 | margin-left: 1rem;
168 | font-size: large;
169 | background-color: $offWhite;
170 | cursor: pointer;
171 | }
172 | }
173 | }
174 |
175 | // dim the background when the sidebar is open
176 | .sidebar-overlay::after {
177 | content: '';
178 | position: fixed;
179 | width: 100%;
180 | height: 100%;
181 | background-color: rgba(0, 0, 0, 0.5);
182 | left: 0;
183 | top: 0;
184 | z-index: 10;
185 | }
186 |
--------------------------------------------------------------------------------
/qache-app/client/styles/demo-styles/ProductDetails.scss:
--------------------------------------------------------------------------------
1 | .products-wrap {
2 | display: flex;
3 | flex-wrap: wrap;
4 | width: 100%;
5 | z-index: -1;
6 |
7 | .product-container {
8 | img {
9 | opacity: 0.85;
10 | }
11 | display: flex;
12 | flex-direction: column;
13 | align-items: center;
14 | width: 21%;
15 | padding: 2%;
16 | border-radius: 3px;
17 | background-color: rgba(255, 255, 255, 0.85);
18 | border-top: 1px solid lightgrey;
19 |
20 | .sale {
21 | background: transparent;
22 | height: 3rem;
23 | width: 3rem;
24 | position: relative;
25 | left: 50%;
26 | display: flex;
27 | align-items: center;
28 | justify-content: center;
29 | text-align: center;
30 | transform: rotate(20deg);
31 | animation: beat 1s ease infinite alternate;
32 | overflow: hidden;
33 | z-index: 1;
34 | }
35 | .sale:before {
36 | content: '';
37 | position: absolute;
38 | top: 0;
39 | left: 0;
40 | height: 3rem;
41 | width: 3rem;
42 | background: green;
43 | color: white;
44 | transform: rotate(135deg);
45 | z-index: -1;
46 | }
47 | @keyframes beat {
48 | from {
49 | transform: rotate(20deg) scale(1);
50 | }
51 | to {
52 | transform: rotate(20deg) scale(1.1);
53 | }
54 | }
55 |
56 | .product-info {
57 | display: flex;
58 | width: 100%;
59 | flex-direction: column;
60 |
61 | strong {
62 | color: red;
63 |
64 | .onSale {
65 | text-decoration: line-through;
66 | }
67 | }
68 | .newPrice {
69 | color: green;
70 | }
71 |
72 | .noSale {
73 | color: black;
74 | }
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/qache-app/client/styles/demo-styles/ProductDisplay.scss:
--------------------------------------------------------------------------------
1 |
2 |
3 | .productDisplay-container {
4 | display: flex;
5 | justify-content: center;
6 | align-items: center;
7 | flex-direction: column;
8 | padding-left: 5%;
9 | padding-right: 5%;
10 |
11 | h1 {
12 | width: 100%;
13 | margin: 20px;
14 | padding-left: 200px;
15 | padding-bottom: 20px;
16 | }
17 |
18 | .cache-line {
19 | display: flex;
20 | width: 100%;
21 | justify-content: space-evenly;
22 |
23 | .talkingPoints{
24 | width: 20%;
25 | margin-top: 20px;
26 | font-size: 1vw;
27 | padding: 1.25%;
28 | }
29 | .lineGraphContainer{
30 | position: relative;
31 | width: 80%;
32 | margin-top: 20px;
33 | font-size: 1vw;
34 | canvas{
35 | width: 100%;
36 | height: 100%;
37 | padding: 5%;
38 | }
39 | .title{
40 | position: absolute;
41 | left: 48%;
42 | top: 5%;
43 | }
44 | .yLabel{
45 | position: absolute;
46 | top: 50%;
47 | left: -3%;
48 | transform: rotate(270deg);
49 | }
50 | .xLabel{
51 | position: absolute;
52 | top: 90%;
53 | left: 50%;
54 | }
55 | }
56 |
57 | .image-container {
58 | display: flex;
59 | justify-content: center;
60 | align-items: center;
61 | overflow-y: hidden;
62 | border-radius: 3px;
63 |
64 | img {
65 | object-fit: cover;
66 | width: 400px;
67 | height: 400px;
68 | }
69 | }
70 | }
71 |
72 | img {
73 | width: 100%;
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/qache-app/client/styles/demo-styles/SubMenuData.scss:
--------------------------------------------------------------------------------
1 | .sidebarLink {
2 | display: flex;
3 | justify-content: start;
4 | align-items: center;
5 | height: 5rem;
6 | padding: 0.5rem 0 0.5rem 1rem;
7 | text-decoration: none;
8 | border-radius: 0.25rem;
9 | font-size: x-large;
10 | color: black;
11 |
12 | svg {
13 | display: inline-block;
14 | position: relative;
15 | vertical-align: baseline;
16 | top: 0.25rem;
17 | }
18 | span {
19 | display: inline-block;
20 | margin-left: 1rem;
21 | &:hover {
22 | text-decoration: underline;
23 | }
24 | }
25 | }
26 |
27 | .dropdownLink {
28 | height: 60px;
29 | padding-left: 3rem;
30 | display: flex;
31 | align-items: center;
32 | text-decoration: none;
33 | border-radius: 0.25rem;
34 | font-size: large;
35 | color: black;
36 |
37 | &:hover {
38 | text-decoration: underline;
39 | }
40 |
41 | .subIcon {
42 | display: flex;
43 | align-items: center;
44 | padding-right: 1rem;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/qache-app/client/styles/home-styles/Docs.scss:
--------------------------------------------------------------------------------
1 | @import '../variables.scss';
2 |
3 | body {
4 | .code-block{
5 | margin: 10px 30px;
6 | padding: 10px;
7 | width: 80%;
8 | min-height: 100px;
9 | color: rgb(223, 223, 223);
10 | display: inline-block;
11 | font-size: small;
12 | background-color: rgb(19, 19, 19);
13 | border-radius: 5px;
14 | border: 1px solid gray
15 | }
16 | .gr{
17 | color: rgba(138, 138, 138, 0.616)
18 | }
19 | .g{
20 | color: rgb(101, 223, 101)
21 | }
22 | .r{
23 | color: rgb(241, 54, 54)
24 | }
25 | .b{
26 | color: rgb(91, 189, 228)
27 | }
28 | .y{
29 | color: rgb(230, 228, 119)
30 | }
31 | .p{
32 | color: rgb(176, 111, 236);
33 | }
34 | #docs {
35 | display: flex;
36 | width: 100%;
37 | background-color: $softBlack;
38 | color: $white;
39 |
40 | strong {
41 | color: $cyan;
42 | margin: 10px 10px;
43 | }
44 |
45 | .title {
46 | font-size: x-large;
47 | color: $offerWhite;
48 | font-weight: 600;
49 | margin-left: 20px;
50 | padding-bottom: 5px;
51 | border-bottom: 1px solid gray;
52 | }
53 | .time {
54 | color: $cyan;
55 | font-size: large;
56 | margin: 5px 20px;
57 | }
58 | .body {
59 | font-size: large;
60 | margin-left: 20px;
61 | }
62 |
63 | #docs-nav {
64 | padding: 10px;
65 | font-size: larger;
66 | position: fixed;
67 | top: 130px;
68 | width: 200px;
69 | height: 100vh;
70 | border-right: 1px solid gray;
71 | li {
72 | margin-left: 30px;
73 | color: $offerWhite;
74 | }
75 | }
76 |
77 | #docs-content {
78 | display: flex;
79 | width: auto;
80 | padding-top: 130px;
81 | margin-left: 230px;
82 | flex-direction: column;
83 | font-size: x-large;
84 | strong{
85 | font-size: xx-large;
86 | margin-left: -10px;
87 | }
88 | .section {
89 | background-color: $offBlack;
90 | margin: 0px 5px 10px 5px;
91 | width: 90%;
92 | border-radius: 10px;
93 | padding: 10px 0px 20px 0px;
94 |
95 | .title {
96 | margin: 0;
97 | padding-left: 20px;
98 | color: $offWhite;
99 | }
100 |
101 | .time {
102 | margin-bottom: 10px;
103 | }
104 | .body {
105 | margin-top: 10px;
106 | color: $offerWhite;
107 | }
108 | }
109 | }
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/qache-app/client/styles/home-styles/Introduction.scss:
--------------------------------------------------------------------------------
1 | @import '../variables.scss';
2 |
3 | .introduction-container {
4 | display: flex;
5 | height: 40%;
6 | justify-content: space-around;
7 | align-items: center;
8 | background-color: $softBlack;
9 | padding: 2%;
10 | padding-top: 120px;
11 |
12 | .header-texts {
13 | display: flex;
14 | flex-direction: column;
15 | align-items: center;
16 | color: $white;
17 |
18 | h1 {
19 | font-size: 3.5rem;
20 | font-weight: bold;
21 | margin-bottom: 2%;
22 | }
23 |
24 | p {
25 | font-weight: 200;
26 | font-size: 1.5rem;
27 | letter-spacing: 0.05rem;
28 | }
29 |
30 | h1,
31 | p {
32 | margin: 0;
33 | }
34 |
35 | p,
36 | .buttons-container {
37 | display: flex;
38 | justify-content: space-between;
39 | padding-top: 2%;
40 | padding-bottom: 2%;
41 | }
42 |
43 | strong {
44 | color: $cyan;
45 | }
46 |
47 | .buttons-container {
48 | display: flex;
49 | align-items: center;
50 | width: 50%;
51 |
52 | .copy-button {
53 | display: flex;
54 | background-color: $cyan;
55 | padding: 20px 25px;
56 | border-radius: 5px;
57 | transition: $quick;
58 | cursor: pointer;
59 |
60 | .forward-arrow {
61 | display: inline-block;
62 | }
63 |
64 | code {
65 | display: inline-block;
66 | color: $black;
67 | font-weight: 600;
68 | }
69 |
70 | &:hover {
71 | background-color: $white;
72 | }
73 |
74 | &:active {
75 | transform: scale(0.95);
76 | }
77 | }
78 |
79 | /* Curl Top Right */
80 | .hvr-curl-top-right {
81 | -webkit-transform: perspective(1px) translateZ(0);
82 | transform: perspective(1px) translateZ(0);
83 | box-shadow: 0 0 1px rgba(0, 0, 0, 0);
84 | }
85 |
86 | .hvr-curl-top-right::before {
87 | pointer-events: none;
88 | position: absolute;
89 | content: '';
90 | height: 0;
91 | width: 0;
92 | top: 0;
93 | right: 0;
94 | background: white;
95 | border-radius: 5px;
96 | /* IE9 */
97 | background: linear-gradient(
98 | 225deg,
99 | white 45%,
100 | #aaa 50%,
101 | #ccc 56%,
102 | white 80%
103 | );
104 | box-shadow: -1px 1px 1px rgba(0, 0, 0, 0.4);
105 | -webkit-transition-duration: 0.3s;
106 | transition-duration: 0.3s;
107 | -webkit-transition-property: width, height;
108 | transition-property: width, height;
109 | }
110 |
111 | .hvr-curl-top-right:hover::before,
112 | .hvr-curl-top-right:focus::before,
113 | .hvr-curl-top-right:active::before {
114 | width: 25px;
115 | height: 25px;
116 | border-radius: 5px;
117 | }
118 | }
119 | }
120 |
121 | .blue-button,
122 | a {
123 | display: inline-block;
124 | color: $cyan;
125 | text-decoration: none;
126 | vertical-align: bottom;
127 | cursor: pointer;
128 |
129 | &:hover {
130 | color: $white;
131 | }
132 |
133 | &:active {
134 | transform: scale(0.95);
135 | }
136 | }
137 | }
138 |
139 | .version-banner {
140 | display: block;
141 | text-align: center;
142 | background-color: $black;
143 | color: $white;
144 | height: 5%;
145 | padding: 0.5%;
146 |
147 | a {
148 | color: $cyan;
149 | text-decoration: underline;
150 |
151 | &:hover {
152 | color: $white;
153 | }
154 | }
155 | }
156 |
157 | .overview-container {
158 | display: flex;
159 | max-width: 100vw;
160 | max-height: 45%;
161 | background-color: $offWhite;
162 |
163 | .bg {
164 | position: absolute;
165 | z-index: 1;
166 | width: 100%;
167 | max-height: 45%;
168 | filter: blur(10px);
169 | }
170 |
171 | .overview-texts,
172 | .overview-graph {
173 | display: flex;
174 | flex-direction: column;
175 | max-width: 50%;
176 | width: 50%;
177 | padding: 3.5% 5%;
178 | }
179 |
180 | .overview-texts {
181 | justify-content: center;
182 | align-items: flex-start;
183 |
184 | h3 {
185 | font-size: 2rem;
186 | font-weight: 300;
187 | margin-bottom: 2%;
188 | }
189 |
190 | p {
191 | font-size: 1.25rem;
192 | width: 75%;
193 | margin-top: 1%;
194 | margin-bottom: 1%;
195 | }
196 | }
197 |
198 | .overview-graph {
199 | background-color: $offWhite;
200 | backdrop-filter: blur(10px);
201 |
202 | canvas {
203 | backdrop-filter: blur(10px);
204 | background-color: rgba(255, 255, 255, 0.5);
205 | border-radius: 5px;
206 | }
207 | }
208 | }
209 |
210 | .features-container {
211 | display: flex;
212 | flex-direction: column;
213 | align-items: center;
214 | justify-content: center;
215 | padding: 1%;
216 | background-color: $offWhite;
217 |
218 | .demo-feature-container {
219 | display: flex;
220 | flex-direction: column;
221 | color: $black;
222 | background-color: #f5f5f4;
223 | width: 75%;
224 | padding: 3%;
225 | box-shadow: rgba(149, 157, 165, 0.2) 0px 8px 24px;
226 |
227 | h1 {
228 | font-size: 2.5rem;
229 | font-weight: 600;
230 | margin-top: 0;
231 | margin-bottom: 5%;
232 | text-align: center;
233 | }
234 |
235 | hr {
236 | border: 1px solid $zinc;
237 | }
238 |
239 | .demo-container {
240 | display: flex;
241 | padding: 1%;
242 |
243 | img {
244 | max-width: 50%;
245 | margin-right: 2.5%;
246 | }
247 |
248 | .text {
249 | width: 100%;
250 | }
251 |
252 | h3 {
253 | font-size: 1.75rem;
254 | font-weight: 400;
255 | margin-bottom: 1%;
256 | }
257 | }
258 | }
259 | }
260 |
--------------------------------------------------------------------------------
/qache-app/client/styles/home-styles/Navbar.scss:
--------------------------------------------------------------------------------
1 | @import '../variables.scss';
2 |
3 | .navbar-container {
4 | display: flex;
5 | position: fixed;
6 | justify-content: space-between;
7 | width: 100%;
8 | height: 120px;
9 | padding-right: 1%;
10 | padding-left: 1%;
11 | z-index: 1;
12 | top: 0;
13 | background-color: $black;
14 | white-space: nowrap;
15 |
16 | .image-container {
17 | display: flex;
18 | width: 200px;
19 | margin-right: 10;
20 | img {
21 | height: 100%;
22 | object-fit: contain;
23 | box-sizing: border-box;
24 | vertical-align: middle;
25 | }
26 | }
27 |
28 | .left-nav,
29 | .right-nav {
30 | display: flex;
31 |
32 |
33 | .nav-list {
34 | display: flex;
35 | align-items: center;
36 | margin-left: auto;
37 |
38 | li{
39 | margin: 0 10px;
40 | }
41 |
42 | .nav-link {
43 | text-decoration: none;
44 | font-size: large;
45 | font-weight: 550;
46 | color: $white;
47 | display: block;
48 | position: relative;
49 | padding: 0.5rem 0;
50 | margin-left: 2rem;
51 |
52 | &::after {
53 | content: '';
54 | position: absolute;
55 | bottom: 0;
56 | left: 0;
57 | width: 100%;
58 | height: 0.1rem;
59 | background-color: $cyan;
60 | transition: opacity $quick, transform $quick;
61 | opacity: 1;
62 | transform: scale(0);
63 | transform-origin: center;
64 | }
65 |
66 | &:hover, &:hover::after, &:focus::after {
67 | color: $cyan;
68 | transform: scale(1);
69 | }
70 | }
71 | }
72 |
73 | .nav-list:nth-child(1) {
74 | justify-content: flex-end;
75 |
76 | .nav-icons {
77 | height: 100%;
78 |
79 | path:hover, svg:hover {
80 | fill: $cyan;
81 | transition: fill $quick ease-in;
82 | }
83 | }
84 | }
85 | }
86 | .left-nav{
87 | margin-right: auto;
88 | }
89 | .right-nav{
90 | margin-right: auto;
91 | margin-right: 3%;
92 | .nav-list {
93 | display: flex;
94 | align-items: center;
95 | margin-left: auto;
96 |
97 | svg{
98 | height: 100%;
99 | }
100 | }
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/qache-app/client/styles/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 |
3 | @tailwind components;
4 |
5 | @tailwind utilities;
--------------------------------------------------------------------------------
/qache-app/client/styles/index.scss:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+Display&display=swap');
2 |
3 | html {
4 | scroll-behavior: smooth;
5 | }
6 |
7 | body {
8 | box-sizing: border-box;
9 | margin: 0px;
10 | padding: 0px;
11 | font-family: 'Noto Sans Display', sans-serif;
12 | -webkit-font-smoothing: antialiased;
13 | -moz-osx-font-smoothing: grayscale;
14 | height: 100vh;
15 | width: 100vw;
16 | overflow-x: hidden;
17 | }
18 |
19 | canvas {
20 | background-color: #fafafa;
21 | z-index: 1;
22 | }
23 |
24 | #app {
25 | height: 100%;
26 | }
27 |
--------------------------------------------------------------------------------
/qache-app/client/styles/variables.scss:
--------------------------------------------------------------------------------
1 | $cyan: #21f5ee;
2 | $bluegreen: #009c97;
3 | $black: #111111;
4 | $softBlack: #18181b;
5 | $offBlack: #27272a;
6 | $lightSoftBlack: rgb(31, 41, 55);
7 | $white: #ffffff;
8 | $offWhite: #fafafa;
9 | $offerWhite: #e0e0e0;
10 | $zinc: #e4e4e7;
11 | $quick: 300ms;
--------------------------------------------------------------------------------
/qache-app/dist/bundle.js.LICENSE.txt:
--------------------------------------------------------------------------------
1 | /*
2 | object-assign
3 | (c) Sindre Sorhus
4 | @license MIT
5 | */
6 |
7 | /*!
8 | * Chart.js v3.7.0
9 | * https://www.chartjs.org
10 | * (c) 2021 Chart.js Contributors
11 | * Released under the MIT License
12 | */
13 |
14 | /*! *****************************************************************************
15 | Copyright (c) Microsoft Corporation.
16 |
17 | Permission to use, copy, modify, and/or distribute this software for any
18 | purpose with or without fee is hereby granted.
19 |
20 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
21 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
22 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
23 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
24 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
25 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
26 | PERFORMANCE OF THIS SOFTWARE.
27 | ***************************************************************************** */
28 |
29 | /**
30 | * React Router DOM v6.2.1
31 | *
32 | * Copyright (c) Remix Software Inc.
33 | *
34 | * This source code is licensed under the MIT license found in the
35 | * LICENSE.md file in the root directory of this source tree.
36 | *
37 | * @license MIT
38 | */
39 |
40 | /**
41 | * React Router v6.2.1
42 | *
43 | * Copyright (c) Remix Software Inc.
44 | *
45 | * This source code is licensed under the MIT license found in the
46 | * LICENSE.md file in the root directory of this source tree.
47 | *
48 | * @license MIT
49 | */
50 |
51 | /** @license React v0.20.2
52 | * scheduler.production.min.js
53 | *
54 | * Copyright (c) Facebook, Inc. and its affiliates.
55 | *
56 | * This source code is licensed under the MIT license found in the
57 | * LICENSE file in the root directory of this source tree.
58 | */
59 |
60 | /** @license React v17.0.2
61 | * react-dom.production.min.js
62 | *
63 | * Copyright (c) Facebook, Inc. and its affiliates.
64 | *
65 | * This source code is licensed under the MIT license found in the
66 | * LICENSE file in the root directory of this source tree.
67 | */
68 |
69 | /** @license React v17.0.2
70 | * react-jsx-runtime.production.min.js
71 | *
72 | * Copyright (c) Facebook, Inc. and its affiliates.
73 | *
74 | * This source code is licensed under the MIT license found in the
75 | * LICENSE file in the root directory of this source tree.
76 | */
77 |
78 | /** @license React v17.0.2
79 | * react.production.min.js
80 | *
81 | * Copyright (c) Facebook, Inc. and its affiliates.
82 | *
83 | * This source code is licensed under the MIT license found in the
84 | * LICENSE file in the root directory of this source tree.
85 | */
86 |
--------------------------------------------------------------------------------
/qache-app/dist/images/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Qache/dd306d760354bd24b40f1c821ad170ef9e290271/qache-app/dist/images/demo.gif
--------------------------------------------------------------------------------
/qache-app/dist/images/navigate.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Qache/dd306d760354bd24b40f1c821ad170ef9e290271/qache-app/dist/images/navigate.gif
--------------------------------------------------------------------------------
/qache-app/dist/images/preview.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Qache/dd306d760354bd24b40f1c821ad170ef9e290271/qache-app/dist/images/preview.gif
--------------------------------------------------------------------------------
/qache-app/dist/images/qache.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Qache/dd306d760354bd24b40f1c821ad170ef9e290271/qache-app/dist/images/qache.gif
--------------------------------------------------------------------------------
/qache-app/dist/images/qache.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Qache/dd306d760354bd24b40f1c821ad170ef9e290271/qache-app/dist/images/qache.png
--------------------------------------------------------------------------------
/qache-app/dist/images/storage.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Qache/dd306d760354bd24b40f1c821ad170ef9e290271/qache-app/dist/images/storage.jpg
--------------------------------------------------------------------------------
/qache-app/dist/index.html:
--------------------------------------------------------------------------------
1 | Qache
--------------------------------------------------------------------------------
/qache-app/dist/main.css:
--------------------------------------------------------------------------------
1 | @import url(https://fonts.googleapis.com/css2?family=Noto+Sans+Display&display=swap);
2 | .navbar-container{display:flex;position:fixed;justify-content:space-between;width:100%;height:120px;padding-right:1%;padding-left:1%;z-index:1;top:0;background-color:#111;white-space:nowrap}.navbar-container .image-container{display:flex;width:200px;margin-right:10}.navbar-container .image-container img{height:100%;object-fit:contain;box-sizing:border-box;vertical-align:middle}.navbar-container .left-nav,.navbar-container .right-nav{display:flex}.navbar-container .left-nav .nav-list,.navbar-container .right-nav .nav-list{display:flex;align-items:center;margin-left:auto}.navbar-container .left-nav .nav-list li,.navbar-container .right-nav .nav-list li{margin:0 10px}.navbar-container .left-nav .nav-list .nav-link,.navbar-container .right-nav .nav-list .nav-link{text-decoration:none;font-size:large;font-weight:550;color:#fff;display:block;position:relative;padding:0.5rem 0;margin-left:2rem}.navbar-container .left-nav .nav-list .nav-link::after,.navbar-container .right-nav .nav-list .nav-link::after{content:'';position:absolute;bottom:0;left:0;width:100%;height:0.1rem;background-color:#21f5ee;transition:opacity 300ms,transform 300ms;opacity:1;transform:scale(0);transform-origin:center}.navbar-container .left-nav .nav-list .nav-link:hover,.navbar-container .left-nav .nav-list .nav-link:hover::after,.navbar-container .left-nav .nav-list .nav-link:focus::after,.navbar-container .right-nav .nav-list .nav-link:hover,.navbar-container .right-nav .nav-list .nav-link:hover::after,.navbar-container .right-nav .nav-list .nav-link:focus::after{color:#21f5ee;transform:scale(1)}.navbar-container .left-nav .nav-list:nth-child(1),.navbar-container .right-nav .nav-list:nth-child(1){justify-content:flex-end}.navbar-container .left-nav .nav-list:nth-child(1) .nav-icons,.navbar-container .right-nav .nav-list:nth-child(1) .nav-icons{height:100%}.navbar-container .left-nav .nav-list:nth-child(1) .nav-icons path:hover,.navbar-container .left-nav .nav-list:nth-child(1) .nav-icons svg:hover,.navbar-container .right-nav .nav-list:nth-child(1) .nav-icons path:hover,.navbar-container .right-nav .nav-list:nth-child(1) .nav-icons svg:hover{fill:#21f5ee;transition:fill 300ms ease-in}.navbar-container .left-nav{margin-right:auto}.navbar-container .right-nav{margin-right:auto;margin-right:3%}.navbar-container .right-nav .nav-list{display:flex;align-items:center;margin-left:auto}.navbar-container .right-nav .nav-list svg{height:100%}
3 |
4 | .introduction-container{display:flex;height:40%;justify-content:space-around;align-items:center;background-color:#18181b;padding:2%;padding-top:120px}.introduction-container .header-texts{display:flex;flex-direction:column;align-items:center;color:#fff}.introduction-container .header-texts h1{font-size:3.5rem;font-weight:bold;margin-bottom:2%}.introduction-container .header-texts p{font-weight:200;font-size:1.5rem;letter-spacing:0.05rem}.introduction-container .header-texts h1,.introduction-container .header-texts p{margin:0}.introduction-container .header-texts p,.introduction-container .header-texts .buttons-container{display:flex;justify-content:space-between;padding-top:2%;padding-bottom:2%}.introduction-container .header-texts strong{color:#21f5ee}.introduction-container .header-texts .buttons-container{display:flex;align-items:center;width:50%}.introduction-container .header-texts .buttons-container .copy-button{display:flex;background-color:#21f5ee;padding:20px 25px;border-radius:5px;transition:300ms;cursor:pointer}.introduction-container .header-texts .buttons-container .copy-button .forward-arrow{display:inline-block}.introduction-container .header-texts .buttons-container .copy-button code{display:inline-block;color:#111;font-weight:600}.introduction-container .header-texts .buttons-container .copy-button:hover{background-color:#fff}.introduction-container .header-texts .buttons-container .copy-button:active{transform:scale(0.95)}.introduction-container .header-texts .buttons-container .hvr-curl-top-right{-webkit-transform:perspective(1px) translateZ(0);transform:perspective(1px) translateZ(0);box-shadow:0 0 1px rgba(0,0,0,0)}.introduction-container .header-texts .buttons-container .hvr-curl-top-right::before{pointer-events:none;position:absolute;content:'';height:0;width:0;top:0;right:0;background:white;border-radius:5px;background:linear-gradient(225deg, white 45%, #aaa 50%, #ccc 56%, white 80%);box-shadow:-1px 1px 1px rgba(0,0,0,0.4);-webkit-transition-duration:0.3s;transition-duration:0.3s;-webkit-transition-property:width, height;transition-property:width, height}.introduction-container .header-texts .buttons-container .hvr-curl-top-right:hover::before,.introduction-container .header-texts .buttons-container .hvr-curl-top-right:focus::before,.introduction-container .header-texts .buttons-container .hvr-curl-top-right:active::before{width:25px;height:25px;border-radius:5px}.introduction-container .blue-button,.introduction-container a{display:inline-block;color:#21f5ee;text-decoration:none;vertical-align:bottom;cursor:pointer}.introduction-container .blue-button:hover,.introduction-container a:hover{color:#fff}.introduction-container .blue-button:active,.introduction-container a:active{transform:scale(0.95)}.version-banner{display:block;text-align:center;background-color:#111;color:#fff;height:5%;padding:0.5%}.version-banner a{color:#21f5ee;text-decoration:underline}.version-banner a:hover{color:#fff}.overview-container{display:flex;max-width:100vw;max-height:45%;background-color:#fafafa}.overview-container .bg{position:absolute;z-index:1;width:100%;max-height:45%;filter:blur(10px)}.overview-container .overview-texts,.overview-container .overview-graph{display:flex;flex-direction:column;max-width:50%;width:50%;padding:3.5% 5%}.overview-container .overview-texts{justify-content:center;align-items:flex-start}.overview-container .overview-texts h3{font-size:2rem;font-weight:300;margin-bottom:2%}.overview-container .overview-texts p{font-size:1.25rem;width:75%;margin-top:1%;margin-bottom:1%}.overview-container .overview-graph{background-color:#fafafa;backdrop-filter:blur(10px)}.overview-container .overview-graph canvas{backdrop-filter:blur(10px);background-color:rgba(255,255,255,0.5);border-radius:5px}.features-container{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:1%;background-color:#fafafa}.features-container .demo-feature-container{display:flex;flex-direction:column;color:#111;background-color:#f5f5f4;width:75%;padding:3%;box-shadow:rgba(149,157,165,0.2) 0px 8px 24px}.features-container .demo-feature-container h1{font-size:2.5rem;font-weight:600;margin-top:0;margin-bottom:5%;text-align:center}.features-container .demo-feature-container hr{border:1px solid #e4e4e7}.features-container .demo-feature-container .demo-container{display:flex;padding:1%}.features-container .demo-feature-container .demo-container img{max-width:50%;margin-right:2.5%}.features-container .demo-feature-container .demo-container .text{width:100%}.features-container .demo-feature-container .demo-container h3{font-size:1.75rem;font-weight:400;margin-bottom:1%}
5 |
6 | #nav-container{display:flex;justify-content:flex-start;align-items:center;height:10%;background-color:#fff;white-space:nowrap}#nav-container .navbar{display:flex;justify-content:space-evenly;align-items:center;height:100%;width:100%;height:80px;padding-top:1%;padding-left:5%;padding-right:5%;background-color:#fafafa;border-bottom:solid 1px #dfdfdf}#nav-container .navbar .menu-bars{display:flex;justify-content:space-between;margin-left:0;font-size:2rem;background:none}#nav-container .navbar a{font-size:1.5rem;font-weight:bold;text-decoration:none;color:black;cursor:pointer}#nav-container .navbar a:hover,#nav-container .navbar a:focus{text-decoration:underline}#nav-container .navbar .search{width:40%;display:flex;position:relative}#nav-container .navbar .search input{width:100%;padding:10px;padding-left:1.75rem;font-size:large;background-color:#fafafa}#nav-container .navbar .search .icon{position:absolute;top:14px;left:7px}#nav-container .navbar .not-active{visibility:hidden}#nav-container .navbar .active{visibility:visible;background-color:transparent;border:none;cursor:pointer;font-size:xx-large}#nav-container .navbar .active:active{transform:translateY(1.5px);background:none;background-color:transparent}#nav-container .navbar .active:active .tooltip{box-shadow:none}#nav-container .navbar .active .tooltip{padding:10px;position:absolute;width:auto;white-space:nowrap;word-wrap:no-wrap;box-shadow:1px 1px 20px #aaa;border-radius:5px;background-color:#fff;top:85px;left:90.75%;transform:translate(-50%);transform-style:preserve-3d;z-index:200;font-size:small;display:none}#nav-container .navbar .active .tooltip:after{content:'';position:absolute;display:block;width:10px;height:10px;transform-origin:50% 50%;transform:rotate(45deg) translateX(-50%);background-color:#fff;left:50%;top:-1px;z-index:400}#nav-container .navbar .active:hover .tooltip{display:block}#nav-container .navbar .active .spinner:hover{animation:spin infinite 1s linear}@keyframes spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}#nav-container .nav-menu{display:flex;justify-content:center;position:fixed;width:250px;height:300vh;top:0;left:-100%;transition:750ms;background-color:#fafafa;overflow-y:scroll !important}#nav-container .nav-menu.active{padding-left:1.5%;left:0;z-index:10;transition:250ms}#nav-container ::-webkit-scrollbar{width:0px;background:transparent}#nav-container .nav-menu-items{width:100%}#nav-container .nav-menu-items .navbar-toggle{display:flex;justify-content:flex-start;align-items:center;height:5rem;margin-left:1rem;font-size:large;background-color:#fafafa;cursor:pointer}.sidebar-overlay::after{content:'';position:fixed;width:100%;height:100%;background-color:rgba(0,0,0,0.5);left:0;top:0;z-index:10}
7 |
8 | .sidebarLink{display:flex;justify-content:start;align-items:center;height:5rem;padding:0.5rem 0 0.5rem 1rem;text-decoration:none;border-radius:0.25rem;font-size:x-large;color:black}.sidebarLink svg{display:inline-block;position:relative;vertical-align:baseline;top:0.25rem}.sidebarLink span{display:inline-block;margin-left:1rem}.sidebarLink span:hover{text-decoration:underline}.dropdownLink{height:60px;padding-left:3rem;display:flex;align-items:center;text-decoration:none;border-radius:0.25rem;font-size:large;color:black}.dropdownLink:hover{text-decoration:underline}.dropdownLink .subIcon{display:flex;align-items:center;padding-right:1rem}
9 |
10 | .landingPage-container{display:flex;flex-direction:column}.landingPage-container h1{text-align:center}.landingPage-container .section-container{display:flex;justify-content:space-evenly;flex-direction:row}.landingPage-container .imageContainer{display:flex;position:relative;justify-content:center;align-items:center;overflow:hidden;width:50%}.landingPage-container img{object-fit:cover;flex-shrink:0;height:80%}.landingPage-container .img-caption{display:flex;justify-content:flex-start;align-items:center;position:absolute;overflow:hidden;top:75%;height:5rem;backdrop-filter:blur(25px)}.landingPage-container .img-caption h2{font-weight:300}.landingPage-container .graph-container{display:flex;flex-direction:column;align-items:stretch}.landingPage-container .graph-container h2{margin:0;font-size:2rem;text-align:center}
11 |
12 | .products-wrap{display:flex;flex-wrap:wrap;width:100%;z-index:-1}.products-wrap .product-container{display:flex;flex-direction:column;align-items:center;width:21%;padding:2%;border-radius:3px;background-color:rgba(255,255,255,0.85);border-top:1px solid lightgrey}.products-wrap .product-container img{opacity:0.85}.products-wrap .product-container .sale{background:transparent;height:3rem;width:3rem;position:relative;left:50%;display:flex;align-items:center;justify-content:center;text-align:center;transform:rotate(20deg);animation:beat 1s ease infinite alternate;overflow:hidden;z-index:1}.products-wrap .product-container .sale:before{content:'';position:absolute;top:0;left:0;height:3rem;width:3rem;background:green;color:white;transform:rotate(135deg);z-index:-1}@keyframes beat{from{transform:rotate(20deg) scale(1)}to{transform:rotate(20deg) scale(1.1)}}.products-wrap .product-container .product-info{display:flex;width:100%;flex-direction:column}.products-wrap .product-container .product-info strong{color:red}.products-wrap .product-container .product-info strong .onSale{text-decoration:line-through}.products-wrap .product-container .product-info .newPrice{color:green}.products-wrap .product-container .product-info .noSale{color:black}
13 |
14 | .productDisplay-container{display:flex;justify-content:center;align-items:center;flex-direction:column;padding-left:5%;padding-right:5%}.productDisplay-container h1{width:100%;margin:20px;padding-left:200px;padding-bottom:20px}.productDisplay-container .cache-line{display:flex;width:100%;justify-content:space-evenly}.productDisplay-container .cache-line .talkingPoints{width:20%;margin-top:20px;font-size:1vw;padding:1.25%}.productDisplay-container .cache-line .lineGraphContainer{position:relative;width:80%;margin-top:20px;font-size:1vw}.productDisplay-container .cache-line .lineGraphContainer canvas{width:100%;height:100%;padding:5%}.productDisplay-container .cache-line .lineGraphContainer .title{position:absolute;left:48%;top:5%}.productDisplay-container .cache-line .lineGraphContainer .yLabel{position:absolute;top:50%;left:-3%;transform:rotate(270deg)}.productDisplay-container .cache-line .lineGraphContainer .xLabel{position:absolute;top:90%;left:50%}.productDisplay-container .cache-line .image-container{display:flex;justify-content:center;align-items:center;overflow-y:hidden;border-radius:3px}.productDisplay-container .cache-line .image-container img{object-fit:cover;width:400px;height:400px}.productDisplay-container img{width:100%}
15 |
16 | body .code-block{margin:10px 30px;padding:10px;width:80%;min-height:100px;color:#dfdfdf;display:inline-block;font-size:small;background-color:#131313;border-radius:5px;border:1px solid gray}body .gr{color:rgba(138,138,138,0.616)}body .g{color:#65df65}body .r{color:#f13636}body .b{color:#5bbde4}body .y{color:#e6e477}body .p{color:#b06fec}body #docs{display:flex;width:100%;background-color:#18181b;color:#fff}body #docs strong{color:#21f5ee;margin:10px 10px}body #docs .title{font-size:x-large;color:#e0e0e0;font-weight:600;margin-left:20px;padding-bottom:5px;border-bottom:1px solid gray}body #docs .time{color:#21f5ee;font-size:large;margin:5px 20px}body #docs .body{font-size:large;margin-left:20px}body #docs #docs-nav{padding:10px;font-size:larger;position:fixed;top:130px;width:200px;height:100vh;border-right:1px solid gray}body #docs #docs-nav li{margin-left:30px;color:#e0e0e0}body #docs #docs-content{display:flex;width:auto;padding-top:130px;margin-left:230px;flex-direction:column;font-size:x-large}body #docs #docs-content strong{font-size:xx-large;margin-left:-10px}body #docs #docs-content .section{background-color:#27272a;margin:0px 5px 10px 5px;width:90%;border-radius:10px;padding:10px 0px 20px 0px}body #docs #docs-content .section .title{margin:0;padding-left:20px;color:#fafafa}body #docs #docs-content .section .time{margin-bottom:10px}body #docs #docs-content .section .body{margin-top:10px;color:#e0e0e0}
17 |
18 | /*
19 | ! tailwindcss v3.0.18 | MIT License | https://tailwindcss.com
20 | *//*
21 | 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
22 | 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
23 | */
24 |
25 | *,
26 | ::before,
27 | ::after {
28 | box-sizing: border-box; /* 1 */
29 | border-width: 0; /* 2 */
30 | border-style: solid; /* 2 */
31 | border-color: #e5e7eb; /* 2 */
32 | }
33 |
34 | ::before,
35 | ::after {
36 | --tw-content: '';
37 | }
38 |
39 | /*
40 | 1. Use a consistent sensible line-height in all browsers.
41 | 2. Prevent adjustments of font size after orientation changes in iOS.
42 | 3. Use a more readable tab size.
43 | 4. Use the user's configured `sans` font-family by default.
44 | */
45 |
46 | html {
47 | line-height: 1.5; /* 1 */
48 | -webkit-text-size-adjust: 100%; /* 2 */
49 | -moz-tab-size: 4; /* 3 */
50 | -o-tab-size: 4;
51 | tab-size: 4; /* 3 */
52 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; /* 4 */
53 | }
54 |
55 | /*
56 | 1. Remove the margin in all browsers.
57 | 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
58 | */
59 |
60 | body {
61 | margin: 0; /* 1 */
62 | line-height: inherit; /* 2 */
63 | }
64 |
65 | /*
66 | 1. Add the correct height in Firefox.
67 | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
68 | 3. Ensure horizontal rules are visible by default.
69 | */
70 |
71 | hr {
72 | height: 0; /* 1 */
73 | color: inherit; /* 2 */
74 | border-top-width: 1px; /* 3 */
75 | }
76 |
77 | /*
78 | Add the correct text decoration in Chrome, Edge, and Safari.
79 | */
80 |
81 | abbr:where([title]) {
82 | -webkit-text-decoration: underline dotted;
83 | text-decoration: underline dotted;
84 | }
85 |
86 | /*
87 | Remove the default font size and weight for headings.
88 | */
89 |
90 | h1,
91 | h2,
92 | h3,
93 | h4,
94 | h5,
95 | h6 {
96 | font-size: inherit;
97 | font-weight: inherit;
98 | }
99 |
100 | /*
101 | Reset links to optimize for opt-in styling instead of opt-out.
102 | */
103 |
104 | a {
105 | color: inherit;
106 | text-decoration: inherit;
107 | }
108 |
109 | /*
110 | Add the correct font weight in Edge and Safari.
111 | */
112 |
113 | b,
114 | strong {
115 | font-weight: bolder;
116 | }
117 |
118 | /*
119 | 1. Use the user's configured `mono` font family by default.
120 | 2. Correct the odd `em` font sizing in all browsers.
121 | */
122 |
123 | code,
124 | kbd,
125 | samp,
126 | pre {
127 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; /* 1 */
128 | font-size: 1em; /* 2 */
129 | }
130 |
131 | /*
132 | Add the correct font size in all browsers.
133 | */
134 |
135 | small {
136 | font-size: 80%;
137 | }
138 |
139 | /*
140 | Prevent `sub` and `sup` elements from affecting the line height in all browsers.
141 | */
142 |
143 | sub,
144 | sup {
145 | font-size: 75%;
146 | line-height: 0;
147 | position: relative;
148 | vertical-align: baseline;
149 | }
150 |
151 | sub {
152 | bottom: -0.25em;
153 | }
154 |
155 | sup {
156 | top: -0.5em;
157 | }
158 |
159 | /*
160 | 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
161 | 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
162 | 3. Remove gaps between table borders by default.
163 | */
164 |
165 | table {
166 | text-indent: 0; /* 1 */
167 | border-color: inherit; /* 2 */
168 | border-collapse: collapse; /* 3 */
169 | }
170 |
171 | /*
172 | 1. Change the font styles in all browsers.
173 | 2. Remove the margin in Firefox and Safari.
174 | 3. Remove default padding in all browsers.
175 | */
176 |
177 | button,
178 | input,
179 | optgroup,
180 | select,
181 | textarea {
182 | font-family: inherit; /* 1 */
183 | font-size: 100%; /* 1 */
184 | line-height: inherit; /* 1 */
185 | color: inherit; /* 1 */
186 | margin: 0; /* 2 */
187 | padding: 0; /* 3 */
188 | }
189 |
190 | /*
191 | Remove the inheritance of text transform in Edge and Firefox.
192 | */
193 |
194 | button,
195 | select {
196 | text-transform: none;
197 | }
198 |
199 | /*
200 | 1. Correct the inability to style clickable types in iOS and Safari.
201 | 2. Remove default button styles.
202 | */
203 |
204 | button,
205 | [type='button'],
206 | [type='reset'],
207 | [type='submit'] {
208 | -webkit-appearance: button; /* 1 */
209 | background-color: transparent; /* 2 */
210 | background-image: none; /* 2 */
211 | }
212 |
213 | /*
214 | Use the modern Firefox focus style for all focusable elements.
215 | */
216 |
217 | :-moz-focusring {
218 | outline: auto;
219 | }
220 |
221 | /*
222 | Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
223 | */
224 |
225 | :-moz-ui-invalid {
226 | box-shadow: none;
227 | }
228 |
229 | /*
230 | Add the correct vertical alignment in Chrome and Firefox.
231 | */
232 |
233 | progress {
234 | vertical-align: baseline;
235 | }
236 |
237 | /*
238 | Correct the cursor style of increment and decrement buttons in Safari.
239 | */
240 |
241 | ::-webkit-inner-spin-button,
242 | ::-webkit-outer-spin-button {
243 | height: auto;
244 | }
245 |
246 | /*
247 | 1. Correct the odd appearance in Chrome and Safari.
248 | 2. Correct the outline style in Safari.
249 | */
250 |
251 | [type='search'] {
252 | -webkit-appearance: textfield; /* 1 */
253 | outline-offset: -2px; /* 2 */
254 | }
255 |
256 | /*
257 | Remove the inner padding in Chrome and Safari on macOS.
258 | */
259 |
260 | ::-webkit-search-decoration {
261 | -webkit-appearance: none;
262 | }
263 |
264 | /*
265 | 1. Correct the inability to style clickable types in iOS and Safari.
266 | 2. Change font properties to `inherit` in Safari.
267 | */
268 |
269 | ::-webkit-file-upload-button {
270 | -webkit-appearance: button; /* 1 */
271 | font: inherit; /* 2 */
272 | }
273 |
274 | /*
275 | Add the correct display in Chrome and Safari.
276 | */
277 |
278 | summary {
279 | display: list-item;
280 | }
281 |
282 | /*
283 | Removes the default spacing and border for appropriate elements.
284 | */
285 |
286 | blockquote,
287 | dl,
288 | dd,
289 | h1,
290 | h2,
291 | h3,
292 | h4,
293 | h5,
294 | h6,
295 | hr,
296 | figure,
297 | p,
298 | pre {
299 | margin: 0;
300 | }
301 |
302 | fieldset {
303 | margin: 0;
304 | padding: 0;
305 | }
306 |
307 | legend {
308 | padding: 0;
309 | }
310 |
311 | ol,
312 | ul,
313 | menu {
314 | list-style: none;
315 | margin: 0;
316 | padding: 0;
317 | }
318 |
319 | /*
320 | Prevent resizing textareas horizontally by default.
321 | */
322 |
323 | textarea {
324 | resize: vertical;
325 | }
326 |
327 | /*
328 | 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
329 | 2. Set the default placeholder color to the user's configured gray 400 color.
330 | */
331 |
332 | input::-moz-placeholder, textarea::-moz-placeholder {
333 | opacity: 1; /* 1 */
334 | color: #9ca3af; /* 2 */
335 | }
336 |
337 | input:-ms-input-placeholder, textarea:-ms-input-placeholder {
338 | opacity: 1; /* 1 */
339 | color: #9ca3af; /* 2 */
340 | }
341 |
342 | input::placeholder,
343 | textarea::placeholder {
344 | opacity: 1; /* 1 */
345 | color: #9ca3af; /* 2 */
346 | }
347 |
348 | /*
349 | Set the default cursor for buttons.
350 | */
351 |
352 | button,
353 | [role="button"] {
354 | cursor: pointer;
355 | }
356 |
357 | /*
358 | Make sure disabled buttons don't get the pointer cursor.
359 | */
360 | :disabled {
361 | cursor: default;
362 | }
363 |
364 | /*
365 | 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
366 | 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
367 | This can trigger a poorly considered lint error in some tools but is included by design.
368 | */
369 |
370 | img,
371 | svg,
372 | video,
373 | canvas,
374 | audio,
375 | iframe,
376 | embed,
377 | object {
378 | display: block; /* 1 */
379 | vertical-align: middle; /* 2 */
380 | }
381 |
382 | /*
383 | Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
384 | */
385 |
386 | img,
387 | video {
388 | max-width: 100%;
389 | height: auto;
390 | }
391 |
392 | /*
393 | Ensure the default browser behavior of the `hidden` attribute.
394 | */
395 |
396 | [hidden] {
397 | display: none;
398 | }
399 |
400 | *, ::before, ::after {
401 | --tw-translate-x: 0;
402 | --tw-translate-y: 0;
403 | --tw-rotate: 0;
404 | --tw-skew-x: 0;
405 | --tw-skew-y: 0;
406 | --tw-scale-x: 1;
407 | --tw-scale-y: 1;
408 | --tw-pan-x: ;
409 | --tw-pan-y: ;
410 | --tw-pinch-zoom: ;
411 | --tw-scroll-snap-strictness: proximity;
412 | --tw-ordinal: ;
413 | --tw-slashed-zero: ;
414 | --tw-numeric-figure: ;
415 | --tw-numeric-spacing: ;
416 | --tw-numeric-fraction: ;
417 | --tw-ring-inset: ;
418 | --tw-ring-offset-width: 0px;
419 | --tw-ring-offset-color: #fff;
420 | --tw-ring-color: rgb(59 130 246 / 0.5);
421 | --tw-ring-offset-shadow: 0 0 #0000;
422 | --tw-ring-shadow: 0 0 #0000;
423 | --tw-shadow: 0 0 #0000;
424 | --tw-shadow-colored: 0 0 #0000;
425 | --tw-blur: ;
426 | --tw-brightness: ;
427 | --tw-contrast: ;
428 | --tw-grayscale: ;
429 | --tw-hue-rotate: ;
430 | --tw-invert: ;
431 | --tw-saturate: ;
432 | --tw-sepia: ;
433 | --tw-drop-shadow: ;
434 | --tw-backdrop-blur: ;
435 | --tw-backdrop-brightness: ;
436 | --tw-backdrop-contrast: ;
437 | --tw-backdrop-grayscale: ;
438 | --tw-backdrop-hue-rotate: ;
439 | --tw-backdrop-invert: ;
440 | --tw-backdrop-opacity: ;
441 | --tw-backdrop-saturate: ;
442 | --tw-backdrop-sepia: ;
443 | }
444 |
445 | .sr-only {
446 | position: absolute;
447 | width: 1px;
448 | height: 1px;
449 | padding: 0;
450 | margin: -1px;
451 | overflow: hidden;
452 | clip: rect(0, 0, 0, 0);
453 | white-space: nowrap;
454 | border-width: 0;
455 | }
456 |
457 | .mx-auto {
458 | margin-left: auto;
459 | margin-right: auto;
460 | }
461 |
462 | .flex {
463 | display: flex;
464 | }
465 |
466 | .hidden {
467 | display: none;
468 | }
469 |
470 | .h-40 {
471 | height: 10rem;
472 | }
473 |
474 | .h-10 {
475 | height: 2.5rem;
476 | }
477 |
478 | .h-11 {
479 | height: 2.75rem;
480 | }
481 |
482 | .w-40 {
483 | width: 10rem;
484 | }
485 |
486 | .w-10 {
487 | width: 2.5rem;
488 | }
489 |
490 | .w-11 {
491 | width: 2.75rem;
492 | }
493 |
494 | .max-w-7xl {
495 | max-width: 80rem;
496 | }
497 |
498 | .justify-center {
499 | justify-content: center;
500 | }
501 |
502 | .space-y-2 > :not([hidden]) ~ :not([hidden]) {
503 | --tw-space-y-reverse: 0;
504 | margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse)));
505 | margin-bottom: calc(0.5rem * var(--tw-space-y-reverse));
506 | }
507 |
508 | .space-y-5 > :not([hidden]) ~ :not([hidden]) {
509 | --tw-space-y-reverse: 0;
510 | margin-top: calc(1.25rem * calc(1 - var(--tw-space-y-reverse)));
511 | margin-bottom: calc(1.25rem * var(--tw-space-y-reverse));
512 | }
513 |
514 | .space-y-6 > :not([hidden]) ~ :not([hidden]) {
515 | --tw-space-y-reverse: 0;
516 | margin-top: calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));
517 | margin-bottom: calc(1.5rem * var(--tw-space-y-reverse));
518 | }
519 |
520 | .space-y-1 > :not([hidden]) ~ :not([hidden]) {
521 | --tw-space-y-reverse: 0;
522 | margin-top: calc(0.25rem * calc(1 - var(--tw-space-y-reverse)));
523 | margin-bottom: calc(0.25rem * var(--tw-space-y-reverse));
524 | }
525 |
526 | .space-x-5 > :not([hidden]) ~ :not([hidden]) {
527 | --tw-space-x-reverse: 0;
528 | margin-right: calc(1.25rem * var(--tw-space-x-reverse));
529 | margin-left: calc(1.25rem * calc(1 - var(--tw-space-x-reverse)));
530 | }
531 |
532 | .rounded-lg {
533 | border-radius: 0.5rem;
534 | }
535 |
536 | .rounded-full {
537 | border-radius: 9999px;
538 | }
539 |
540 | .bg-zinc-900 {
541 | --tw-bg-opacity: 1;
542 | background-color: rgb(24 24 27 / var(--tw-bg-opacity));
543 | }
544 |
545 | .bg-zinc-800 {
546 | --tw-bg-opacity: 1;
547 | background-color: rgb(39 39 42 / var(--tw-bg-opacity));
548 | }
549 |
550 | .px-4 {
551 | padding-left: 1rem;
552 | padding-right: 1rem;
553 | }
554 |
555 | .py-10 {
556 | padding-top: 2.5rem;
557 | padding-bottom: 2.5rem;
558 | }
559 |
560 | .px-6 {
561 | padding-left: 1.5rem;
562 | padding-right: 1.5rem;
563 | }
564 |
565 | .text-center {
566 | text-align: center;
567 | }
568 |
569 | .text-3xl {
570 | font-size: 1.875rem;
571 | line-height: 2.25rem;
572 | }
573 |
574 | .text-xl {
575 | font-size: 1.25rem;
576 | line-height: 1.75rem;
577 | }
578 |
579 | .text-lg {
580 | font-size: 1.125rem;
581 | line-height: 1.75rem;
582 | }
583 |
584 | .font-extrabold {
585 | font-weight: 800;
586 | }
587 |
588 | .font-medium {
589 | font-weight: 500;
590 | }
591 |
592 | .leading-6 {
593 | line-height: 1.5rem;
594 | }
595 |
596 | .tracking-tight {
597 | letter-spacing: -0.025em;
598 | }
599 |
600 | .text-white {
601 | --tw-text-opacity: 1;
602 | color: rgb(255 255 255 / var(--tw-text-opacity));
603 | }
604 |
605 | .text-gray-300 {
606 | --tw-text-opacity: 1;
607 | color: rgb(209 213 219 / var(--tw-text-opacity));
608 | }
609 |
610 | .text-cyan-500 {
611 | --tw-text-opacity: 1;
612 | color: rgb(6 182 212 / var(--tw-text-opacity));
613 | }
614 |
615 | .text-gray-400 {
616 | --tw-text-opacity: 1;
617 | color: rgb(156 163 175 / var(--tw-text-opacity));
618 | }
619 |
620 | .filter {
621 | filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
622 | }
623 |
624 | .transition {
625 | transition-property: color, background-color, border-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-text-decoration-color, -webkit-backdrop-filter;
626 | transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
627 | transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-text-decoration-color, -webkit-backdrop-filter;
628 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
629 | transition-duration: 150ms;
630 | }
631 |
632 | .hover\:scale-110:hover {
633 | --tw-scale-x: 1.1;
634 | --tw-scale-y: 1.1;
635 | transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
636 | }
637 |
638 | .hover\:text-gray-300:hover {
639 | --tw-text-opacity: 1;
640 | color: rgb(209 213 219 / var(--tw-text-opacity));
641 | }
642 |
643 | @media (min-width: 640px) {
644 |
645 | .sm\:grid {
646 | display: grid;
647 | }
648 |
649 | .sm\:grid-cols-2 {
650 | grid-template-columns: repeat(2, minmax(0, 1fr));
651 | }
652 |
653 | .sm\:gap-6 {
654 | gap: 1.5rem;
655 | }
656 |
657 | .sm\:space-y-4 > :not([hidden]) ~ :not([hidden]) {
658 | --tw-space-y-reverse: 0;
659 | margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse)));
660 | margin-bottom: calc(1rem * var(--tw-space-y-reverse));
661 | }
662 |
663 | .sm\:space-y-0 > :not([hidden]) ~ :not([hidden]) {
664 | --tw-space-y-reverse: 0;
665 | margin-top: calc(0px * calc(1 - var(--tw-space-y-reverse)));
666 | margin-bottom: calc(0px * var(--tw-space-y-reverse));
667 | }
668 |
669 | .sm\:px-6 {
670 | padding-left: 1.5rem;
671 | padding-right: 1.5rem;
672 | }
673 |
674 | .sm\:text-4xl {
675 | font-size: 2.25rem;
676 | line-height: 2.5rem;
677 | }
678 | }
679 |
680 | @media (min-width: 768px) {
681 |
682 | .md\:max-w-xl {
683 | max-width: 36rem;
684 | }
685 | }
686 |
687 | @media (min-width: 1024px) {
688 |
689 | .lg\:max-w-3xl {
690 | max-width: 48rem;
691 | }
692 |
693 | .lg\:grid-cols-2 {
694 | grid-template-columns: repeat(2, minmax(0, 1fr));
695 | }
696 |
697 | .lg\:gap-8 {
698 | gap: 2rem;
699 | }
700 |
701 | .lg\:px-8 {
702 | padding-left: 2rem;
703 | padding-right: 2rem;
704 | }
705 |
706 | .lg\:py-4 {
707 | padding-top: 1rem;
708 | padding-bottom: 1rem;
709 | }
710 | }
711 |
712 | @media (min-width: 1280px) {
713 |
714 | .xl\:flex {
715 | display: flex;
716 | }
717 |
718 | .xl\:h-56 {
719 | height: 14rem;
720 | }
721 |
722 | .xl\:w-56 {
723 | width: 14rem;
724 | }
725 |
726 | .xl\:max-w-none {
727 | max-width: none;
728 | }
729 |
730 | .xl\:items-center {
731 | align-items: center;
732 | }
733 |
734 | .xl\:justify-between {
735 | justify-content: space-between;
736 | }
737 |
738 | .xl\:space-y-10 > :not([hidden]) ~ :not([hidden]) {
739 | --tw-space-y-reverse: 0;
740 | margin-top: calc(2.5rem * calc(1 - var(--tw-space-y-reverse)));
741 | margin-bottom: calc(2.5rem * var(--tw-space-y-reverse));
742 | }
743 |
744 | .xl\:px-10 {
745 | padding-left: 2.5rem;
746 | padding-right: 2.5rem;
747 | }
748 |
749 | .xl\:text-left {
750 | text-align: left;
751 | }
752 | }
753 | html{scroll-behavior:smooth}body{box-sizing:border-box;margin:0px;padding:0px;font-family:'Noto Sans Display', sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;height:100vh;width:100vw;overflow-x:hidden}canvas{background-color:#fafafa;z-index:1}#app{height:100%}
754 |
755 |
--------------------------------------------------------------------------------
/qache-app/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.png';
2 | declare module '*.jpg';
3 | declare module '*.gif';
--------------------------------------------------------------------------------
/qache-app/interfaces.ts:
--------------------------------------------------------------------------------
1 | import { Component } from 'react';
2 |
3 | export interface Product {
4 | id?: string;
5 | name: string;
6 | description: string;
7 | imageUrl: string;
8 | quantity: number;
9 | price: number;
10 | onSale: boolean;
11 | category: string;
12 | inCart: boolean;
13 | }
14 |
15 | export interface Metric {
16 | labels?: string[];
17 | data?: number[];
18 |
19 | Bedroom?: {
20 | labels: string[];
21 | data: number[];
22 | };
23 | Mattresses?: {
24 | labels: string[];
25 | data: number[];
26 | };
27 | Furniture?: {
28 | labels: string[];
29 | data: number[];
30 | };
31 | Storage?: {
32 | labels: string[];
33 | data: number[];
34 | };
35 | 'Living Room'?: {
36 | labels: string[];
37 | data: number[];
38 | };
39 | Kitchen?: {
40 | labels: string[];
41 | data: number[];
42 | };
43 | Bathroom?: {
44 | labels: string[];
45 | data: number[];
46 | };
47 | Appliances?: {
48 | labels: string[];
49 | data: number[];
50 | };
51 | Couches?: {
52 | labels: string[];
53 | data: number[];
54 | };
55 | Deals?: {
56 | labels: string[];
57 | data: number[];
58 | };
59 | Cart?: {
60 | labels: string[];
61 | data: number[];
62 | }
63 | }
64 |
65 | export interface Item {
66 | title: string;
67 | path: string;
68 | icon: Component;
69 | cName?: string;
70 | iconClosed?: Component;
71 | iconOpened?: Component;
72 | subNav?: Item[] | undefined;
73 | }
74 |
75 | export interface Dataset {
76 | label: string;
77 | fill: boolean;
78 | lineTension: number;
79 | backgroundColor: string;
80 | borderColor: string;
81 | borderWidth: number;
82 | data: number[] | undefined;
83 | }
84 |
--------------------------------------------------------------------------------
/qache-app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "demo-app",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "jest",
8 | "start": "npm run server",
9 | "start:dev": "concurrently --kill-others-on-fail \"npm run server:dev\" \"npm run client\"",
10 | "server": "cross-env NODE_ENV=production node server/server.js",
11 | "server:dev": "cross-env NODE_ENV=development nodemon server/server.js",
12 | "client": "cross-env NODE_ENV=development webpack serve --mode development",
13 | "build:dev": "webpack --mode development",
14 | "build:prod": "NODE_ENV=production webpack --mode production",
15 | "build:css": "npx tailwindcss -i ./client/styles/index.css -o ./dist/main.css --watcmain.css --watch",
16 | "deploy": "cd .. && npm run deploy"
17 | },
18 | "engines": {
19 | "node": ">=16.0.0 <=17.4.0",
20 | "npm": "8.3.1"
21 | },
22 | "keywords": [],
23 | "author": "",
24 | "license": "ISC",
25 | "dependencies": {
26 | "@babel/core": "^7.16.10",
27 | "@babel/plugin-transform-react-jsx": "^7.16.7",
28 | "@babel/preset-env": "^7.16.11",
29 | "@babel/preset-react": "^7.16.7",
30 | "@heroicons/react": "^1.0.5",
31 | "@observablehq/plot": "^0.4.0",
32 | "@types/react": "^17.0.38",
33 | "@types/react-dom": "^17.0.11",
34 | "@types/react-router-hash-link": "^2.4.5",
35 | "add": "^2.0.6",
36 | "autoprefixer": "^10.4.2",
37 | "axios": "^0.25.0",
38 | "babel-jest": "^27.4.6",
39 | "babel-loader": "^8.2.3",
40 | "babel-plugin-react-require": "^3.1.3",
41 | "chart.js": "^3.7.0",
42 | "concurrently": "^7.0.0",
43 | "cors": "^2.8.5",
44 | "cross-env": "^7.0.3",
45 | "css-loader": "^6.5.1",
46 | "dotenv": "^14.2.0",
47 | "express": "^4.17.2",
48 | "express-graphql": "^0.12.0",
49 | "file-loader": "^6.2.0",
50 | "framer-motion": "^6.2.4",
51 | "html-webpack-plugin": "^5.5.0",
52 | "jest": "^27.4.7",
53 | "mini-css-extract-plugin": "^2.5.3",
54 | "mongoose": "^6.2.1",
55 | "node-sass": "^7.0.1",
56 | "nodemon": "^2.0.15",
57 | "postcss-loader": "^6.2.1",
58 | "prettier": "^2.5.1",
59 | "qache": "^1.0.5",
60 | "react": "^17.0.2",
61 | "react-chartjs-2": "^4.0.1",
62 | "react-dom": "^17.0.2",
63 | "react-icons": "^4.3.1",
64 | "react-intersection-observer": "^8.33.1",
65 | "react-router-dom": "^6.2.1",
66 | "react-router-hash-link": "^2.4.3",
67 | "sass-loader": "^12.4.0",
68 | "style-loader": "^3.3.1",
69 | "tailwindcss": "^3.0.18",
70 | "ts-loader": "^9.2.6",
71 | "typescript": "^4.5.4",
72 | "url-loader": "^4.1.1",
73 | "webpack": "^5.66.0",
74 | "webpack-bundle-analyzer": "^4.5.0",
75 | "webpack-cli": "^4.9.1",
76 | "webpack-dev-server": "^4.7.3"
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/qache-app/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [
3 | require('tailwindcss'),
4 | require('autoprefixer')
5 | ]
6 | };
--------------------------------------------------------------------------------
/qache-app/server/database/db.js:
--------------------------------------------------------------------------------
1 | const products = []
2 |
3 | const categories = []
4 |
5 | module.exports = products, categories;
--------------------------------------------------------------------------------
/qache-app/server/models/CategoryModel.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 | const Schema = mongoose.Schema;
3 |
4 | const categorySchema = new Schema({
5 | name: { type: String, required: true, unique: true },
6 | products: [{ type: Schema.Types.ObjectId, ref: 'product'}],
7 | });
8 |
9 | const Category = mongoose.model('category', categorySchema);
10 |
11 | module.exports = Category;
12 |
--------------------------------------------------------------------------------
/qache-app/server/models/ProductModel.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 | const Schema = mongoose.Schema;
3 |
4 | const productSchema = new Schema({
5 | name: { type: String, required: true },
6 | description: { type: String, required: true },
7 | imageUrl: { type: String, required: true },
8 | quantity: { type: Number, required: true },
9 | price: { type: Number, required: true },
10 | onSale: { type: Boolean, required: true },
11 | inCart: {type: Boolean, required: true},
12 | category: [{ type: Schema.Types.ObjectId, ref: 'category', required: true }],
13 | });
14 |
15 | const Product = mongoose.model('product', productSchema);
16 |
17 | module.exports = Product;
18 |
--------------------------------------------------------------------------------
/qache-app/server/resolvers/resolvers.js:
--------------------------------------------------------------------------------
1 | let Qache;
2 | if (process.env.NODE_ENV === 'development') {
3 | Qache = require('../../../Qache/Qache');
4 | } else if (process.env.NODE_ENV === 'production') {
5 | Qache = require('qache');
6 | }
7 | const Product = require('../models/ProductModel');
8 | const Category = require('../models/CategoryModel');
9 |
10 | const cache = new Qache({ evictionPolicy: 'LFU' });
11 |
12 | module.exports = {
13 | // clear cache
14 | invalidate: () => {
15 | try {
16 | cache.invalidate();
17 | console.log('Cache invalidated!');
18 | return true;
19 | } catch (error) {
20 | console.log(error);
21 | return false;
22 | }
23 | },
24 | // creates new product and adds it to DB
25 | addProduct: async (args, parent, info) => {
26 | const {
27 | name,
28 | description,
29 | imageUrl,
30 | quantity,
31 | price,
32 | onSale,
33 | category,
34 | inCart,
35 | } = args.product;
36 |
37 | // creates new product in DB
38 | const newProduct = await Product.create({
39 | name,
40 | description,
41 | imageUrl,
42 | quantity,
43 | price,
44 | onSale,
45 | category,
46 | inCart,
47 | });
48 |
49 | newProduct.category.forEach(async (id) => {
50 | const path = await Category.findById(id);
51 | cache.listPush(newProduct, category.name);
52 | path.products.push(newProduct._id);
53 | await path.save();
54 | });
55 | return newProduct;
56 | },
57 |
58 | // creates new category and adds it to DB
59 | addCategory: async (args, parent, info) => {
60 | const { name, products } = args.category;
61 | const newCategory = await Category.create({ name, products });
62 | return newCategory;
63 | },
64 |
65 | // returns all existing products in DB
66 | getAllProducts: async (args, parent, info) => {
67 | const data = await Product.find().populate('category');
68 | return data;
69 | },
70 |
71 | // returns all existing products in DB that are in given category
72 | getProductsBy: async (args) => {
73 | const t1 = Date.now();
74 | const { category } = args;
75 | const cacheRes = cache.listRange(category); // checks if the category of products exist in cache first
76 | if (cacheRes) {
77 | const t2 = Date.now();
78 | console.log(t2 - t1, 'ms');
79 | console.log('This response came from the CACHE.');
80 | return cacheRes;
81 | } // if exists, returns the array of products
82 | const dbRes = await Category.findOne({ name: category }).populate(
83 | 'products'
84 | );
85 | // *** Our website and our database are hosted in the same AWS warehouse.
86 | // *** In order to properly illustrate benefits of our library for the majority of use cases, we sourced average ping time from https://wondernetwork.com/pings/New%20York
87 | // ** medium-high latency simulation = average ping from NYC -> Dallas 58ms (East -> Central) - averages taken from our actual database pings 8.5ms (AWS US-East-1 for both server and database)
88 | // ** medium-high latency simulation - 50 ms
89 | await new Promise((resolve, reject) => {
90 | setTimeout(() => {
91 | resolve('medium-high latency simulation');
92 | }, 50);
93 | });
94 |
95 | console.log('This response came from the DATABASE');
96 | const t3 = Date.now();
97 | cache.listCreate(category, dbRes.products); // sets products array into cache under the name of category
98 | console.log(t3 - t1, 'ms');
99 | return dbRes.products;
100 | },
101 |
102 | // returns all existing categories in DB
103 | getCategories: async (args, parent, info) => {
104 | const data = await Category.find().populate('products');
105 | return data;
106 | },
107 |
108 | // returns existing category in DB with corresponding ID
109 | getCategoryBy: async (args, parent, info) => {
110 | const data = await Category.findOne({ id: args.id }).populate('products');
111 | return data;
112 | },
113 |
114 | // deletes existing Product in DB with corresponding ID
115 | deleteProduct: async (args, parent, info) => {
116 | await Product.deleteOne({ _id: args.id });
117 | return;
118 | },
119 |
120 | // deletes existing Category in DB with corresponding ID
121 | deleteCategory: async (args, parent, info) => {
122 | await Category.deleteOne({ _id: args.id });
123 | return;
124 | },
125 |
126 | // patches existing Product in DB with corresponding ID, replacing chosen field(s) with inputted info
127 | updateProduct: async (args, parent, info) => {
128 | let { id } = args.product;
129 | const updatedProduct = await Product.findOneAndUpdate(
130 | { _id: id },
131 | args.product,
132 | {
133 | new: true,
134 | }
135 | ).populate('category');
136 |
137 | if (updatedProduct.onSale)
138 | cache.listUpsert(updatedProduct, { id }, 'onSale');
139 | else cache.listRemoveItem({ id }, 'onSale');
140 |
141 | const categoryNames = [];
142 | updatedProduct.category.forEach((obj) => categoryNames.push(obj.name));
143 | cache.listUpdate({ id }, args.product, ...categoryNames);
144 | return updatedProduct;
145 | },
146 |
147 | // patches existing Category in DB with corresponding ID, replacing chosen field(s) with inputted info
148 | updateCategory: async (args, parent, info) => {
149 | let { id, name, products } = args.category;
150 | let updatedCategory = await Category.findById(id);
151 | if (name) updatedCategory.name = name;
152 | if (products) updatedCategory.products = products;
153 | await Category.findOneAndUpdate({ _id: id }, updatedCategory, {
154 | new: true,
155 | });
156 | return updatedCategory;
157 | },
158 |
159 | // filters existing Products based on onSale/inCart field
160 | filterProductsBy: async (args, parent, info) => {
161 | const { onSale } = args.filter;
162 | const cacheRes = cache.listRange('onSale');
163 | if (cacheRes) {
164 | return cacheRes;
165 | }
166 | const filteredProducts = await Product.find({ onSale });
167 |
168 | // *** Our website and our database are hosted in the same AWS warehouse.
169 | // *** In order to properly illustrate benefits of our library for the majority of use cases, we sourced average ping time from https://wondernetwork.com/pings/New%20York
170 | // ** medium-high latency simulation = average ping from NYC -> Dallas 58ms (East -> Central) - averages taken from our actual database pings 8.5ms (AWS US-East-1 for both server and database)
171 | // ** medium-high latency simulation - 50 ms
172 | await new Promise((resolve, reject) => {
173 | setTimeout(() => {
174 | resolve('medium-high latency simulation');
175 | }, 50);
176 | });
177 |
178 | cache.listCreate('onSale', filteredProducts);
179 | return filteredProducts;
180 | },
181 | };
182 |
--------------------------------------------------------------------------------
/qache-app/server/server.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 | const path = require('path');
3 | const express = require('express');
4 | const mongoose = require('mongoose');
5 | const { graphqlHTTP } = require('express-graphql');
6 |
7 | const schema = require('./typeDefs/schema');
8 | const resolvers = require('./resolvers/resolvers');
9 |
10 | const port = 3000;
11 | const app = express();
12 |
13 | const cors = require('cors');
14 | app.use(cors({ credentials: true, origin: 'http://localhost:8080' }));
15 |
16 | app.use(express.json());
17 | app.use(express.urlencoded({ extended: true }));
18 |
19 | mongoose.connect(
20 | `mongodb+srv://sdu1278:${process.env.PASSWORD}@cluster0.vubqx.mongodb.net/myFirstDatabase?retryWrites=true&w=majority`,
21 | { useNewUrlParser: true, useUnifiedTopology: true, dbName: 'qachengo' }
22 | );
23 | mongoose.connection.once('open', () => {
24 | console.log('Connected to MongoDB!');
25 | });
26 |
27 | app.use(
28 | '/graphql',
29 | graphqlHTTP({
30 | schema: schema,
31 | rootValue: resolvers,
32 | graphiql: process.env.NODE_ENV === 'development',
33 | })
34 | );
35 |
36 | if (process.env.NODE_ENV === 'development') {
37 | app.get('/', (req, res) => {
38 | res.status(200).send('Welcome to Demo App dev server!');
39 | });
40 | } else if (process.env.NODE_ENV === 'production') {
41 | app.use(express.static(path.join(__dirname, '../dist')));
42 | app.get('/', (req, res) => {
43 | return res.status(200).sendFile(path.join(__dirname, '../index.html'));
44 | });
45 | }
46 |
47 | app.use('*', (req, res) => {
48 | res.status(200).sendFile(path.join(__dirname, '../dist/index.html'));
49 | });
50 |
51 | app.use((error, req, res, next) => {
52 | const defaultError = {
53 | log: 'Express error handler caught unknown middleware error',
54 | status: 400,
55 | message: { error: 'An error occured' },
56 | };
57 |
58 | const errorObj = Object.assign(defaultError, error);
59 |
60 | res.status(errorObj.status).send(errorObj.message);
61 | });
62 |
63 | app.listen(process.env.PORT || port, (req, res) =>
64 | console.log(`Server is listening on port ${process.env.PORT || port}!`)
65 | );
66 |
--------------------------------------------------------------------------------
/qache-app/server/typeDefs/schema.js:
--------------------------------------------------------------------------------
1 | const { buildSchema } = require('graphql');
2 |
3 | module.exports = buildSchema(`
4 | type Query {
5 | invalidate: Boolean
6 | getAllProducts: [PopulatedProducts]
7 | getProductsBy(category: String!): [Product]
8 | filterProductsBy(filter: FilterProductsInput): [Product]
9 | getCategories: [PopulatedCategories]
10 | getCategoryBy(id: ID!): Category
11 | }
12 |
13 | type Product {
14 | id: ID!
15 | name: String!
16 | description: String!
17 | imageUrl: String!
18 | quantity: Int!
19 | price: Float!
20 | onSale: Boolean!
21 | category: [String!]!
22 | inCart: Boolean
23 | }
24 |
25 | type PopulatedProducts {
26 | id: ID!
27 | name: String!
28 | description: String!
29 | imageUrl: String!
30 | quantity: Int!
31 | price: Float!
32 | onSale: Boolean!
33 | category: [Category]!
34 | inCart: Boolean
35 | }
36 |
37 | type Category {
38 | id: ID!
39 | name: String!
40 | products: [String]
41 | }
42 |
43 | type PopulatedCategories {
44 | id: ID!
45 | name: String!
46 | description: String!
47 | imageUrl: String!
48 | quantity: Int!
49 | price: Float!
50 | onSale: Boolean!
51 | products: [Product]!
52 | inCart: Boolean
53 | }
54 |
55 | type Mutation {
56 | addProduct(product: AddProductInput): Product!
57 | addCategory(category: AddCategoryInput): Category!
58 | deleteProduct(id: ID!): Boolean
59 | deleteCategory(id: ID!): Boolean
60 | updateProduct(product: UpdateProductInput): PopulatedProducts
61 | updateCategory(category: UpdateCategoryInput): Category
62 | }
63 |
64 | input AddProductInput {
65 | name: String!
66 | description: String!
67 | imageUrl: String!
68 | quantity: Int!
69 | price: Float!
70 | onSale: Boolean!
71 | category: [String!]!
72 | inCart: Boolean
73 | }
74 |
75 | input AddCategoryInput {
76 | name: String
77 | products: [String]
78 | }
79 |
80 | input UpdateProductInput {
81 | id: ID!
82 | name: String
83 | description: String
84 | imageUrl: String
85 | quantity: Int
86 | price: Float
87 | onSale: Boolean
88 | category: [String!]
89 | inCart: Boolean
90 | }
91 |
92 | input UpdateCategoryInput {
93 | id: ID!
94 | name: String
95 | products: [String]
96 | }
97 |
98 | input FilterProductsInput {
99 | onSale: Boolean
100 | inCart: Boolean
101 | }
102 | `);
103 |
--------------------------------------------------------------------------------
/qache-app/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | content: ["./client/**/*.{html,js,ts,tsx,jsx}"],
3 | theme: {
4 | extend: {},
5 | },
6 | plugins: [],
7 | }
8 |
--------------------------------------------------------------------------------
/qache-app/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "./dist/",
4 | "sourceMap": true,
5 | "strict": true,
6 | "noImplicitReturns": true,
7 | "noImplicitAny": true,
8 | "module": "es6",
9 | "moduleResolution": "node",
10 | "target": "es6",
11 | "allowJs": true,
12 | "jsx": "react-jsx",
13 | "suppressImplicitAnyIndexErrors": true,
14 | },
15 | "include": ["client/*", "src", "index.d.ts"]
16 | }
17 |
--------------------------------------------------------------------------------
/qache-app/webpack.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const path = require('path');
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
5 | // const BundleAnalyzerPlugin =
6 | // require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
7 |
8 | const config = {
9 | entry: './client/index.tsx',
10 | output: {
11 | path: path.resolve(__dirname, 'dist'),
12 | filename: 'bundle.js',
13 | publicPath: '/',
14 | },
15 | module: {
16 | rules: [
17 | {
18 | test: /\.(js|jsx)$/,
19 | use: 'babel-loader',
20 | exclude: /node_modules/,
21 | },
22 | {
23 | test: /\.css$/,
24 | use: [
25 | MiniCssExtractPlugin.loader,
26 | {
27 | loader: 'css-loader',
28 | options: {
29 | importLoaders: 1,
30 | },
31 | },
32 | 'postcss-loader',
33 | ],
34 | },
35 | {
36 | test: /\.ts(x)?$/,
37 | loader: 'ts-loader',
38 | exclude: /node_modules/,
39 | },
40 | {
41 | test: /\.scss$/,
42 | use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
43 | },
44 | {
45 | test: /\.(png|svg|jpg|gif)$/,
46 | use: [
47 | {
48 | loader: 'file-loader',
49 | options: {
50 | name: 'images/[name].[ext]',
51 | },
52 | },
53 | ],
54 | },
55 | {
56 | test: /\.svg$/,
57 | use: 'file-loader',
58 | },
59 | ],
60 | },
61 | devServer: {
62 | historyApiFallback: true,
63 | },
64 | plugins: [
65 | new HtmlWebpackPlugin({
66 | templateContent: ({ htmlWebpackPlugin }) =>
67 | 'Qache
',
68 | filename: 'index.html',
69 | }),
70 | new MiniCssExtractPlugin(),
71 | new webpack.EnvironmentPlugin({
72 | NODE_ENV: 'production',
73 | }),
74 | // new BundleAnalyzerPlugin({
75 | // analyzerMode: 'static',
76 | // openAnalyzer: false,
77 | // }),
78 | ],
79 | resolve: {
80 | extensions: ['.tsx', '.ts', '.js', '.jsx'],
81 | },
82 | };
83 |
84 | module.exports = config;
85 |
--------------------------------------------------------------------------------