Skip to content

Commit cede780

Browse files
authored
Merge pull request #120 from oslabs-beta/main
quell-app-v9
2 parents ab7ae16 + b895b2c commit cede780

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+2555
-1950
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ Quell is divided into two npm packages:
4141
- Successfully handling cache mutations with cache invalidation.
4242
- Quell/server offers optional depth and cost limiting middleware to protect your GraphQL endpoint! To use, please explore the [@quell/server readme](./quell-server/README.md).
4343
- Server-side cache now caches entire queries in instances where it is unable to cache individual datapoints.
44-
- Client-side caching utilizing LokiJS.
44+
- Client-side caching utilizing JavaScript's built in Map data structure.
4545
- Client-side caching utilizing Least Recently Used (LRU) caching strategy.
4646
- Server-side caching utilizing a configurable Redis in-memory data store with batching.
4747
- Partial and exact match query caching.
@@ -56,7 +56,7 @@ Quell is divided into two npm packages:
5656

5757
### Demo
5858

59-
<p><img src="assets/QuellDemo-Client-Side.gif"></p>
59+
<p><img src="assets/QuellDemo-Client-Side-Demo.gif"></p>
6060
The first bar represents an uncached client-side query and the second and third bars represent the response time of a cached request (0 ms).
6161

6262
</br>
@@ -95,4 +95,4 @@ Thank you for your interest and support!
9595

9696
## Quell Contributors
9797

98-
Accelerated by [OS Labs](https://github.com/open-source-labs) and developed by [Jonah Weinbum](https://github.com/jonahpw), [Justin Hua](https://github.com/justinfhua), [Lenny Yambao](https://github.com/lennin6), [Michael Lav](https://github.com/mikelav258), [Angelo Chengcuenca](https://github.com/amchengcuenca), [Emily Hoang](https://github.com/emilythoang), [Keely Timms](https://github.com/keelyt), [Yusuf Bhaiyat](https://github.com/yusuf-bha), [Hannah Spencer](https://github.com/Hannahspen), [Garik Asplund](https://github.com/garikAsplund), [Katie Sandfort](https://github.com/katiesandfort), [Sarah Cynn](https://github.com/cynnsarah), [Rylan Wessel](https://github.com/XpIose), [Alex Martinez](https://github.com/alexmartinez123), [Cera Barrow](https://github.com/cerab), [Jackie He](https://github.com/Jckhe), [Zoe Harper](https://github.com/ContraireZoe), [David Lopez](https://github.com/DavidMPLopez), [Sercan Tuna](https://github.com/srcntuna), [Idan Michael](https://github.com/IdanMichael), [Tom Pryor](https://github.com/Turmbeoz), [Chang Cai](https://github.com/ccai89), [Robert Howton](https://github.com/roberthowton), [Joshua Jordan](https://github.com/jjordan-90), [Jinhee Choi](https://github.com/jcroadmovie), [Nayan Parmar](https://github.com/nparmar1), [Tashrif Sanil](https://github.com/tashrifsanil), [Tim Frenzel](https://github.com/TimFrenzel), [Robleh Farah](https://github.com/farahrobleh), [Angela Franco](https://github.com/ajfranco18), [Ken Litton](https://github.com/kenlitton), [Thomas Reeder](https://github.com/nomtomnom), [Andrei Cabrera](https://github.com/Andreicabrerao), [Dasha Kondratenko](https://github.com/dasha-k), [Derek Sirola](https://github.com/dsirola1), [Xiao Yu Omeara](https://github.com/xyomeara), [Nick Kruckenberg](https://github.com/kruckenberg), [Mike Lauri](https://github.com/MichaelLauri), [Rob Nobile](https://github.com/RobNobile) and [Justin Jaeger](https://github.com/justinjaeger).
98+
Accelerated by [OS Labs](https://github.com/open-source-labs) and developed by [Andrew Dai](https://github.com/andrewmdai), [Cassidy Komp](https://github.com/mimikomp), [Ian Weinholtz](https://github.com/itsHackinTime), [Stacey Lee](https://github.com/staceyjhlee), [Jonah Weinbum](https://github.com/jonahpw), [Justin Hua](https://github.com/justinfhua), [Lenny Yambao](https://github.com/lennin6), [Michael Lav](https://github.com/mikelav258), [Angelo Chengcuenca](https://github.com/amchengcuenca), [Emily Hoang](https://github.com/emilythoang), [Keely Timms](https://github.com/keelyt), [Yusuf Bhaiyat](https://github.com/yusuf-bha), [Hannah Spencer](https://github.com/Hannahspen), [Garik Asplund](https://github.com/garikAsplund), [Katie Sandfort](https://github.com/katiesandfort), [Sarah Cynn](https://github.com/cynnsarah), [Rylan Wessel](https://github.com/XpIose), [Alex Martinez](https://github.com/alexmartinez123), [Cera Barrow](https://github.com/cerab), [Jackie He](https://github.com/Jckhe), [Zoe Harper](https://github.com/ContraireZoe), [David Lopez](https://github.com/DavidMPLopez), [Sercan Tuna](https://github.com/srcntuna), [Idan Michael](https://github.com/IdanMichael), [Tom Pryor](https://github.com/Turmbeoz), [Chang Cai](https://github.com/ccai89), [Robert Howton](https://github.com/roberthowton), [Joshua Jordan](https://github.com/jjordan-90), [Jinhee Choi](https://github.com/jcroadmovie), [Nayan Parmar](https://github.com/nparmar1), [Tashrif Sanil](https://github.com/tashrifsanil), [Tim Frenzel](https://github.com/TimFrenzel), [Robleh Farah](https://github.com/farahrobleh), [Angela Franco](https://github.com/ajfranco18), [Ken Litton](https://github.com/kenlitton), [Thomas Reeder](https://github.com/nomtomnom), [Andrei Cabrera](https://github.com/Andreicabrerao), [Dasha Kondratenko](https://github.com/dasha-k), [Derek Sirola](https://github.com/dsirola1), [Xiao Yu Omeara](https://github.com/xyomeara), [Nick Kruckenberg](https://github.com/kruckenberg), [Mike Lauri](https://github.com/MichaelLauri), [Rob Nobile](https://github.com/RobNobile) and [Justin Jaeger](https://github.com/justinjaeger).

assets/QuellDemo-Client-Side-Demo.gif

5.35 MB
Loading

quell-client/.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
node_modules
22
dump.rdb
33
package-lock.json
4-
coverage
4+
coverage
5+
dist/
6+
dist
7+
.tgz

quell-client/README.md

Lines changed: 22 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/open-source-labs/Quell/blob/master/LICENSE)
2-
![AppVeyor](https://img.shields.io/badge/version-6.0.0-blue.svg)
2+
![AppVeyor](https://img.shields.io/badge/build-passing-brightgreen.svg)
3+
![AppVeyor](https://img.shields.io/badge/version-9.0.0-blue.svg)
34
[![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/open-source-labs/Quell/issues)
45

56
# @quell/client
67

7-
@quell/client is an easy-to-implement JavaScript library providing a simple, client-side caching solution and cache invalidation for GraphQL. Quell's client-side cache implementation caches whole queries as keys and saves their results as values in LokiJS.
8+
@quell/client is an easy-to-implement JavaScript library providing a client-side caching solution and cache invalidation for GraphQL. Quell's schema-governed, type-level normalization algorithm caches GraphQL query responses as flattened key-value representations of the graph's nodes, making it possible to partially satisfy queries from the client-side cache storage, reformulate the query, and then fetch additional data from other APIs or databases.
89

9-
@quell/client is an open-source NPM package accelerated by [OS Labs](https://github.com/open-source-labs) and developed by [Jonah Weinbaum](https://github.com/jonahpw), [Justin Hua](https://github.com/justinfhua), [Lenny Yambao](https://github.com/lennin6), [Michael Lav](https://github.com/mikelav258), [Angelo Chengcuenca](https://github.com/amchengcuenca), [Emily Hoang](https://github.com/emilythoang), [Keely Timms](https://github.com/keelyt), [Yusuf Bhaiyat](https://github.com/yusuf-bha), [Hannah Spencer](https://github.com/Hannahspen), [Garik Asplund](https://github.com/garikAsplund), [Katie Sandfort](https://github.com/katiesandfort), [Sarah Cynn](https://github.com/cynnsarah), [Rylan Wessel](https://github.com/XpIose), [Alex Martinez](https://github.com/alexmartinez123), [Cera Barrow](https://github.com/cerab), [Jackie He](https://github.com/Jckhe), [Zoe Harper](https://github.com/ContraireZoe), [David Lopez](https://github.com/DavidMPLopez), [Sercan Tuna](https://github.com/srcntuna), [Idan Michael](https://github.com/IdanMichael), [Tom Pryor](https://github.com/Turmbeoz), [Chang Cai](https://github.com/ccai89), [Robert Howton](https://github.com/roberthowton), [Joshua Jordan](https://github.com/jjordan-90), [Jinhee Choi](https://github.com/jcroadmovie), [Nayan Parmar](https://github.com/nparmar1), [Tashrif Sanil](https://github.com/tashrifsanil), [Tim Frenzel](https://github.com/TimFrenzel), [Robleh Farah](https://github.com/farahrobleh), [Angela Franco](https://github.com/ajfranco18), [Ken Litton](https://github.com/kenlitton), [Thomas Reeder](https://github.com/nomtomnom), [Andrei Cabrera](https://github.com/Andreicabrerao), [Dasha Kondratenko](https://github.com/dasha-k), [Derek Sirola](https://github.com/dsirola1), [Xiao Yu Omeara](https://github.com/xyomeara), [Nick Kruckenberg](https://github.com/kruckenberg), [Mike Lauri](https://github.com/MichaelLauri), [Rob Nobile](https://github.com/RobNobile) and [Justin Jaeger](https://github.com/justinjaeger).
10+
@quell/client is an open-source NPM package accelerated by [OS Labs](https://github.com/open-source-labs) and developed by [Cassidy Komp](https://github.com/mimikomp), [Andrew Dai](https://github.com/andrewmdai), [Stacey Lee](https://github.com/staceyjhlee), [Ian Weinholtz](https://github.com/itsHackinTime), [Angelo Chengcuenca](https://github.com/amchengcuenca), [Emily Hoang](https://github.com/emilythoang), [Keely Timms](https://github.com/keelyt), [Yusuf Bhaiyat](https://github.com/yusuf-bha), [David Lopez](https://github.com/DavidMPLopez), [Sercan Tuna](https://github.com/srcntuna), [Idan Michael](https://github.com/IdanMichael), [Tom Pryor](https://github.com/Turmbeoz), [Chang Cai](https://github.com/ccai89), [Robert Howton](https://github.com/roberthowton), [Joshua Jordan](https://github.com/jjordan-90), [Jinhee Choi](https://github.com/jcroadmovie), [Nayan Parmar](https://github.com/nparmar1), [Tashrif Sanil](https://github.com/tashrifsanil), [Tim Frenzel](https://github.com/TimFrenzel), [Robleh Farah](https://github.com/farahrobleh), [Angela Franco](https://github.com/ajfranco18), [Ken Litton](https://github.com/kenlitton), [Thomas Reeder](https://github.com/nomtomnom), [Andrei Cabrera](https://github.com/Andreicabrerao), [Dasha Kondratenko](https://github.com/dasha-k), [Derek Sirola](https://github.com/dsirola1), [Xiao Yu Omeara](https://github.com/xyomeara), [Nick Kruckenberg](https://github.com/kruckenberg), [Mike Lauri](https://github.com/MichaelLauri), [Rob Nobile](https://github.com/RobNobile), and [Justin Jaeger](https://github.com/justinjaeger).
1011

1112
## Installation
1213

@@ -30,60 +31,49 @@ const sampleQuery = `query {
3031
population
3132
}
3233
}
33-
}`;
34+
}`
35+
3436

3537
fetch('/graphQL', {
36-
method: 'POST',
37-
body: JSON.stringify(sampleQuery)
38-
});
38+
method: "POST",
39+
body: JSON.stringify(sampleQuery)
40+
})
3941

4042
costOptions = {
4143
maxCost: 50,
4244
maxDepth: 10,
43-
ipRate: 5
44-
};
45+
ipRate: 5
46+
}
4547
```
4648

4749
To make that same request with Quell:
4850

49-
1. Import Quell with `import { Quellify } from '@quell/client'`
51+
1. Import Quell with `import { Quellify } from '@quell/client/dist/Quellify'`
5052
2. Instead of calling `fetch(endpoint)` and passing the query through the request body, replace with `Quellify(endpoint, query, costOptions)`
5153

5254
- The `Quellify` method takes in three parameters
5355
1. **_endpoint_** - your GraphQL endpoint as a string (ex. '/graphQL')
5456
2. **_query_** - your GraphQL query as a string (ex. see sampleQuery, above)
55-
3. **_costOptions_** - your cost limit, depth limit, and ip rate limit for your queries (ex. see costOptions, above)
57+
3. **_costOptions_** - your cost limit, depth limit, and IP rate limit for your queries (ex. see costOptions, above)
58+
4. **_mutationMap_** - maps mutation names to corresponding parts of the schema.
59+
*(For more information, see the Schema section in @quell/server [README file](https://github.com/open-source-labs/Quell/tree/master/quell-server))*
60+
5661

5762
And in the end , your Quell-powered GraphQL fetch would look like this:
5863

5964
```javascript
60-
Quellify('/graphQL', sampleQuery, costOptions).then(/* use parsed response */);
65+
Quellify('/graphQL', sampleQuery, costOptions, mutationMap)
66+
.then( /* use parsed response */ );
6167
```
6268

6369
Note: Quell will return a promise that resolves into an array with two elements. The first element will be a JS object containing your data; this is in the same form as the response found on the 'data' key of a typical GraphQL response `{ data: // response }`. The second element will be a boolean indicating whether or not the data was found in the client-side cache.
6470

65-
That's it! You're now caching your GraphQL queries in the LokiJS client-side cache storage.
71+
That's it! You're now caching your GraphQL queries in the client-side cache storage.
6672

6773
### Usage Notes
6874

69-
- Quell Client now supports GraphQL mutations, simplifying the process of caching mutations and keeping the cache synchronized with the server. When a mutation is executed, the mutation request is sent to the GraphQL server as usual, and once the server responds with the mutation result, Quell Client updates the cache with the new data. Quell can handle create, edit, or delete mutations.
70-
71-
- The LRU Cache uses a MAX_CACHE_SIZE to determine the size of the cache. You can update this number to fit your needs.
72-
73-
74-
# Future Additions
75-
76-
77-
Goals for the future of Quell/client include:
78-
79-
- The caching logic for the server-side is multi-faceted and robust, while recent iterations of Quell removed much of the functionality on the client-side. Though the current iteration works to address some of these issues and rebuild functionality, starting with handling mutations and implementing LRU caching, more work can be done on the client-side. This includes:
80-
81-
1. The client-side DB could be transitioned from LokiDB to a newer DB technology that also offers in-memory storage for quick retrieval of data, as LokiDB has been largely deprecated.
82-
83-
2. Extending the algorithm beyond LRU, such as adding a Least Frequently Used (LFU) caching algorithm to account for different data access patterns.
84-
85-
3. Refactoring and removing any redundancies, in particular with refetching and updating the LRU cache.
75+
- @quell/client now client-side caching speed is 4-5 times faster than it used to be.
8676

87-
4. Increase testing coverage for the current implementation of the client-side cache.
77+
- Currently, Quell can cache any non-mutative query. Quell will still process other requests, but all mutations will cause cache invalidation for the entire client-side cache. Please report edge cases, issues, and other user stories to us, we would be grateful to expand on Quells use cases!
8878

89-
#### For information on @quell/server, please visit the corresponding [README file](../quell-server/README.md).
79+
#### For information on @quell/server, please visit the corresponding [README file](https://github.com/open-source-labs/Quell/tree/master/quell-server).
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export {};
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
"use strict";
2+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4+
return new (P || (P = Promise))(function (resolve, reject) {
5+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8+
step((generator = generator.apply(thisArg, _arguments || [])).next());
9+
});
10+
};
11+
Object.defineProperty(exports, "__esModule", { value: true });
12+
const Quellify_1 = require("../src/Quellify");
13+
const defaultCostOptions = {
14+
maxCost: 5000,
15+
mutationCost: 5,
16+
objectCost: 2,
17+
scalarCost: 1,
18+
depthCostFactor: 1.5,
19+
maxDepth: 10,
20+
ipRate: 3
21+
};
22+
// Command to run jest tests:
23+
// npx jest client/src/quell-client/client-tests/__tests__/quellify.test.ts
24+
describe('Quellify', () => {
25+
beforeEach(() => {
26+
// Clear the client cache before each test
27+
(0, Quellify_1.clearCache)();
28+
});
29+
it('checks that caching queries is working correctly', () => __awaiter(void 0, void 0, void 0, function* () {
30+
const endPoint = 'http://localhost:3000/api/graphql';
31+
const query = 'query { artist(name: "Frank Ocean") { id name albums { id name } } }';
32+
const costOptions = defaultCostOptions;
33+
const [data, foundInCache] = yield (0, Quellify_1.Quellify)(endPoint, query, costOptions);
34+
// Assertion: the data should not be found in the cache
35+
expect(foundInCache).toBe(false);
36+
// Invoke Quellify on query again
37+
const [cachedData, updatedCache] = yield (0, Quellify_1.Quellify)(endPoint, query, costOptions);
38+
// Assertion: Cached data should be the same as the original query
39+
expect(cachedData).toBe(data);
40+
// Assertion: The boolean should return true if it is found in the cache
41+
expect(updatedCache).toEqual(true);
42+
}));
43+
it('should update the cache for edit mutation queries', () => __awaiter(void 0, void 0, void 0, function* () {
44+
const endPoint = 'http://localhost:3000/api/graphql';
45+
const addQuery = 'mutation { addCity(name: "San Diego", country: "United States") { id name } }';
46+
const costOptions = defaultCostOptions;
47+
// Perform add mutation query to the cache
48+
const [addMutationData, addMutationfoundInCache] = yield (0, Quellify_1.Quellify)(endPoint, addQuery, costOptions);
49+
// Get the cityId on the mutation query
50+
const cityId = addMutationData.addCity.id;
51+
const city = "Las Vegas";
52+
// Perform edit mutation on query to update the name
53+
const editQuery = `mutation { editCity(id: "${cityId}", name: "${city}", country: "United States") { id name } }`;
54+
const [editMutationData, editMutationDataFoundInCache] = yield (0, Quellify_1.Quellify)(endPoint, editQuery, costOptions);
55+
//Assertion: The first mutation query name should be updated by the second edit mutation
56+
expect(addMutationData.name).toEqual(editMutationData.name);
57+
}));
58+
it('should delete an item from the server and invalidate the cache', () => __awaiter(void 0, void 0, void 0, function* () {
59+
const endPoint = 'http://localhost:3000/api/graphql';
60+
const addQuery = 'mutation { addCity(name: "Irvine", country: "United States") { id name } }';
61+
const costOptions = defaultCostOptions;
62+
// Perform add mutation query to the server
63+
const [addMutationData, addMutationfoundInCache] = yield (0, Quellify_1.Quellify)(endPoint, addQuery, costOptions);
64+
// Get the cityId on the mutation query
65+
const cityId = addMutationData.addCity.id;
66+
// Perform a delete mutation on the city
67+
const deleteQuery = `mutation { deleteCity(id: "${cityId}") { id name } }`;
68+
const [deleteMutationData, deleteMutationDataFoundInCache] = yield (0, Quellify_1.Quellify)(endPoint, deleteQuery, costOptions);
69+
//Assertion: The item should be removed from the cache
70+
expect(deleteMutationDataFoundInCache).toBe(false);
71+
}));
72+
it('should evict the LRU item from cache if cache size is exceeded', () => __awaiter(void 0, void 0, void 0, function* () {
73+
const endPoint = 'http://localhost:3000/api/graphql';
74+
const costOptions = defaultCostOptions;
75+
const query1 = 'query { artist(name: "Frank Ocean") { id name albums { id name } } }';
76+
const query2 = 'query { country(name: "United States") { id name cities { id name attractions { id name } } } }';
77+
const query3 = 'mutation { addCity(name: "San Diego", country: "United States") { id name } }';
78+
// Invoke Quellify on each query to add to cache
79+
yield (0, Quellify_1.Quellify)(endPoint, query1, costOptions);
80+
yield (0, Quellify_1.Quellify)(endPoint, query2, costOptions);
81+
// Assertion: lruCache should contain the queries
82+
expect(Quellify_1.lruCache.has(query1)).toBe(true);
83+
expect(Quellify_1.lruCache.has(query2)).toBe(true);
84+
// Invoke Quellify again on third query to exceed max cache size
85+
yield (0, Quellify_1.Quellify)(endPoint, query3, costOptions);
86+
// Assertion: lruCache should evict the LRU item
87+
expect(Quellify_1.lruCache.has(query1)).toBe(false);
88+
// Assertion: lruCache should still contain the most recently used items
89+
expect(Quellify_1.lruCache.has(query2)).toBe(true);
90+
expect(Quellify_1.lruCache.has(query3)).toBe(true);
91+
}));
92+
});

0 commit comments

Comments
 (0)