Skip to content

Commit 893f1a8

Browse files
authored
Merge pull request #54 from oslabs-beta/master
Obsidian 3.1 - Added support for variables and directives and the ability to limit query depth
2 parents fe3bf6e + 6731962 commit 893f1a8

16 files changed

+1159
-291
lines changed

.prettierrc

Lines changed: 0 additions & 1 deletion
This file was deleted.

.travis.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# sudo: required
22
# services:
3-
# - docker
3+
# - docker
44
# before_install:
5-
# - docker build -t open-source-labs/obsidian .
5+
# - docker build -t open-source-labs/obsidian .
66
# script:
7-
# - docker run open-source-labs/obsidian test --allow-net --allow-read --allow-env --unstable deno.test.ts
7+
# - docker run open-source-labs/obsidian test --allow-net --allow-read --allow-env --unstable deno.test.ts
88
# env:
99
# global:
1010
# secure: sfaHXoKGXnAkwS/QK2pdTPC1NVqd9+pVWImEcz8W9IXFRsOcHpt9lVsmB0dFvDpVm+9KFpcBwnpfOtiyoj6Q9NGIY71jG58kYHdbcWBlR3onS7/JBvgEu94DC7HZR+rQ4/GW+ROh4avBt6RjDSuLk4qQ73Yc3+SDKAl+M0PTADlVZpkicCID59qcdynbAjXu5W8lW2Hp0hqO72Prx/8hgmchI0I7zSYcPBFSy3WaEPJa52yKesVwsHcFtzOBMrDAdE+R028AzdBAXUoiqh6cTVeLSTL1jnIWbCBtfAROlTR82cZyo4c7PJxYyqT3mhRSZvBN/3hdW7+xMOzq6gmpmcl1UO2Q5i4xXEGnatfuzMVa/8SqJZoG2IFIWZ4mvelwufHVuLgF+6JvK2BKSpjFfSUGo0p9G0bMg+GHwRipTPIq1If3ELkflAM6QJwL7TritwtWzWXfAfoZ3KALdPTiFzJAKyQfFvSwWbfXqAgqZIbLjlzSgOJ4QKWD6CBksU7b4Oky6hr/+R+ZihzQLtWKkk/8cklEG/NJlknS2vPRG8xRRF7/C+vSFPrCkmsakPc8c1iGfai8J3Vc09Pg0UeShJDWkSQ6QP165ub6LEL5nz0Qzp0CD1sSQu5re5/M5ef9V69L2pdYhEj0RaZ241DF5efzYAgLI8SvMr5TcTr06+8=

.vscode/settings.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"deno.enable": true,
3+
"deno.lint": true,
4+
"deno.unstable": true
5+
}

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
- GraphQL query abstraction and caching in SSR React projects, improving the performance of your app
2626
- Normalized caching, optimizing memory management to keep your site lightweight and fast
2727
- Fullstack integration, leveraging client-side and server-side caching to streamline your caching strategy
28+
- Support for GraphQL fragments, directives, and variables
29+
- Optional GraphQL DoS attack mitigation security module
2830

2931
## Overview
3032

@@ -64,7 +66,7 @@ const PORT = 8000;
6466
const app = new Application();
6567

6668
const types = (gql as any)`
67-
// Type definitions
69+
// GraphQL type definitions
6870
`;
6971

7072
const resolvers = {
@@ -193,6 +195,11 @@ const MovieApp = () => {
193195
194196
_Lascaux_ Engineers
195197
198+
[Kyung Lee](https://github.com/kyunglee1)
199+
[Justin McKay](https://github.com/justinwmckay)
200+
[Patrick Sullivan](https://github.com/pjmsullivan)
201+
[Cameron Simmons](https://github.com/cssim22)
202+
[Raymond Ahn](https://github.com/raymondcodes)
196203
[Alonso Garza](https://github.com/Alonsog66)
197204
[Burak Caliskan](https://github.com/CaliskanBurak)
198205
[Matt Meigs](https://github.com/mmeigs)

src/CacheClassServer.js

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,22 @@ export class Cache {
3232
}
3333

3434
// Main functionality methods
35-
async read(queryStr) {
35+
async read(queryStr, queryVars) {
3636
if (typeof queryStr !== 'string')
3737
throw TypeError('input should be a string');
3838
// destructure the query string into an object
39-
const queries = destructureQueries(queryStr).queries;
39+
const queries = destructureQueries(queryStr, queryVars).queries;
40+
4041
// breaks out of function if queryStr is a mutation
4142
if (!queries) return undefined;
43+
4244
const responseObject = {};
4345
// iterate through each query in the input queries object
4446
for (const query in queries) {
4547
// get the entire str query from the name input query and arguments
4648
const queryHash = queries[query].name.concat(queries[query].arguments);
4749
const rootQuery = await this.cacheRead('ROOT_QUERY');
50+
4851
// match in ROOT_QUERY
4952
if (rootQuery[queryHash]) {
5053
// get the hashs to populate from the existent query in the cache
@@ -56,6 +59,7 @@ export class Cache {
5659
arrayHashes,
5760
queries[query].fields
5861
);
62+
5963
if (!responseObject[respObjProp]) return undefined;
6064

6165
// no match with ROOT_QUERY return null or ...
@@ -66,8 +70,8 @@ export class Cache {
6670
return { data: responseObject };
6771
}
6872

69-
async write(queryStr, respObj, deleteFlag) {
70-
const queryObj = destructureQueries(queryStr);
73+
async write(queryStr, respObj, deleteFlag, queryVars) {
74+
const queryObj = destructureQueries(queryStr, queryVars);
7175
const resFromNormalize = normalizeResult(queryObj, respObj, deleteFlag);
7276
// update the original cache with same reference
7377
for (const hash in resFromNormalize) {
@@ -85,22 +89,26 @@ export class Cache {
8589

8690
// cache read/write helper methods
8791
async cacheRead(hash) {
88-
// returns value from either object cache or cache || 'DELETED' || undefined
92+
// returns value from either object cache or cache || 'DELETED' || undefined
8993
if (this.context === 'client') {
94+
console.log('context === client HIT');
9095
return this.storage[hash];
9196
} else {
9297
// logic to replace these storage keys if they have expired
9398
if (hash === 'ROOT_QUERY' || hash === 'ROOT_MUTATION') {
9499
const hasRootQuery = await redis.get('ROOT_QUERY');
100+
95101
if (!hasRootQuery) {
96102
await redis.set('ROOT_QUERY', JSON.stringify({}));
97103
}
98104
const hasRootMutation = await redis.get('ROOT_MUTATION');
105+
99106
if (!hasRootMutation) {
100107
await redis.set('ROOT_MUTATION', JSON.stringify({}));
101108
}
102109
}
103110
let hashedQuery = await redis.get(hash);
111+
104112
// if cacheRead is a miss
105113
if (hashedQuery === undefined) return undefined;
106114
return JSON.parse(hashedQuery);
@@ -158,8 +166,10 @@ export class Cache {
158166
async populateAllHashes(allHashesFromQuery, fields) {
159167
// include the hashname for each hash
160168
if (!allHashesFromQuery.length) return [];
169+
161170
const hyphenIdx = allHashesFromQuery[0].indexOf('~');
162171
const typeName = allHashesFromQuery[0].slice(0, hyphenIdx);
172+
163173
return allHashesFromQuery.reduce(async (acc, hash) => {
164174
// for each hash from the input query, build the response object
165175
const readVal = await this.cacheRead(hash);
@@ -176,11 +186,13 @@ export class Cache {
176186
// add the typename for the type
177187
if (field === '__typename') {
178188
dataObj[field] = typeName;
179-
} else dataObj[field] = readVal[field];
189+
} else {
190+
dataObj[field] = readVal[field];
191+
}
180192
} else {
181193
// case where the field from the input query is an array of hashes, recursively invoke populateAllHashes
182194
dataObj[field] = await this.populateAllHashes(
183-
readVal[field],
195+
[readVal[field]],
184196
fields[field]
185197
);
186198
if (dataObj[field] === undefined) return undefined;

src/DoSSecurity.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import destructureQueries from './destructure.js';
2+
3+
// Interface representing shape of query object after destructuring
4+
interface queryObj {
5+
queries?: Array<object>,
6+
mutations?: Array<object>,
7+
}
8+
9+
/**
10+
* Tests whether a queryString (string representation of query) exceeds the maximum nested depth levels (queryDepthLimit) allowable for the instance of obsidian
11+
* @param {*} queryString the string representation of the graphql query
12+
* @param {*} queryDepthLimit number representation of the maximum query depth limit. Default 0 will return undefined. Root query doesn't count toward limit.
13+
* @returns boolean indicating whether the query depth exceeded maximum allowed query depth
14+
*/
15+
export default function queryDepthLimiter(queryString: string, queryDepthLimit: number = 0): void {
16+
const queryObj = destructureQueries(queryString) as queryObj;
17+
/**
18+
*Function that tests whether the query object debth exceeds maximum depth
19+
* @param {*} qryObj an object representation of the query (after destructure)
20+
* @param {*} qryDepthLim the maximum query depth
21+
* @param {*} depth indicates current depth level
22+
* @returns boolean indicating whether query depth exceeds maximum depth
23+
*/
24+
const queryDepthCheck = (qryObj: queryObj, qryDepthLim: number, depth: number = 0): boolean => {
25+
// Base case 1: check to see if depth exceeds limit, if so, return error (true means depth limit was exceeded)
26+
if (depth > qryDepthLim) return true;
27+
// Recursive case: Iterate through values of queryObj, and check if each value is an object,
28+
for (let value = 0; value < Object.values(qryObj).length; value++) {
29+
// if the value is an object, return invokation queryDepthCheck on nested object and iterate depth
30+
const currentValue = Object.values(qryObj)[value];
31+
if (typeof currentValue === 'object') {
32+
return queryDepthCheck(currentValue, qryDepthLim, depth + 1);
33+
};
34+
};
35+
// Base case 2: reach end of object keys iteration,return false - depth has not been exceeded
36+
return false;
37+
};
38+
39+
// Check if queryObj has query or mutation root type, if so, call queryDepthCheck on each element, i.e. each query or mutation
40+
if (queryObj.queries) {
41+
for(let i = 0; i < queryObj.queries.length; i++) {
42+
if(queryDepthCheck(queryObj.queries[i], queryDepthLimit)) {
43+
throw new Error(
44+
'Security Error: Query depth exceeded maximum query depth limit'
45+
);
46+
};
47+
};
48+
};
49+
50+
if (queryObj.mutations){
51+
for (let i = 0; i < queryObj.mutations.length; i++) {
52+
if (queryDepthCheck(queryObj.mutations[i], queryDepthLimit)) {
53+
throw new Error(
54+
'Security Error: Query depth exceeded maximum mutation depth limit'
55+
);
56+
};
57+
};
58+
};
59+
}

0 commit comments

Comments
 (0)