Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
b0225fa
Swap query variables with their values
kyunglee1 Jun 15, 2021
f62fe76
Store queries with a single variable in cache
kyunglee1 Jun 15, 2021
8e2582e
Store queries with multiple variables in cache
kyunglee1 Jun 15, 2021
b423920
Add comments to destructur.js
kyunglee1 Jun 16, 2021
a2e9900
Replace variables in directives with their values
kyunglee1 Jun 16, 2021
e930225
Destructure queries with directives
kyunglee1 Jun 17, 2021
51644d6
Implemented DoS security module enabling developers to set a maximum …
pjmsullivan Jun 19, 2021
4dbc2dd
Fix cache read() when query has nested types
kyunglee1 Jun 23, 2021
03b86ca
Refactored DoS Security module and added DoS Securuty tests
pjmsullivan Jun 24, 2021
4955bc4
Fix caching when directive is false
kyunglee1 Jun 24, 2021
1fcc3a0
Removed extraneous console log
pjmsullivan Jun 24, 2021
9617459
Add co-authors
pjmsullivan Jun 24, 2021
acf10bf
Fix queryDepthCheck return type (to remove undefined)
pjmsullivan Jun 24, 2021
86a5844
Fix import statement on destructure_test.ts to accommodate change in …
pjmsullivan Jun 24, 2021
09a734c
Fix DoSSecurity depth check level (change from 10 to 2)
pjmsullivan Jun 24, 2021
cc2e073
Implement support for skip directive
kyunglee1 Jun 24, 2021
170e1a8
Added tests for multiple queries or mutations for DoSSecurity module
pjmsullivan Jun 24, 2021
aac58fc
Removed extraneous console log
pjmsullivan Jun 24, 2021
94871b5
Add single/multi variable Rhum tests
kyunglee1 Jun 24, 2021
ed1cc42
Add single @include directive tests
kyunglee1 Jun 26, 2021
0384f18
Fix serverCache_test
kyunglee1 Jun 28, 2021
e97bfc8
Merge pull request #1 from pjmsullivan/security
cssim22 Jun 28, 2021
3db213c
Merge security features
kyunglee1 Jun 28, 2021
b76057b
Add @skip directives tests
kyunglee1 Jun 28, 2021
4a91436
Add comments to destructure.js
kyunglee1 Jun 28, 2021
bac39cd
Updated Readme to indicate new features and contributors
pjmsullivan Jun 28, 2021
126fae3
Updated Readme to indicate new features and contributors
pjmsullivan Jun 29, 2021
0a9e35f
Remove extra comments
kyunglee1 Jun 29, 2021
27435c4
Remove unnecessary comments
kyunglee1 Jun 29, 2021
99193fa
Merge branch 'master' of https://github.com/oslabs-beta/obsidian
pjmsullivan Jun 29, 2021
a24c9c2
Merge pull request #2 from pjmsullivan/security
justinwmckay Jun 29, 2021
3683bb9
Merge branch 'master' of https://github.com/oslabs-beta/obsidian
pjmsullivan Jun 29, 2021
906ee65
Remove unnecessary files
kyunglee1 Jun 29, 2021
07bed1f
Merge pull request #3 from kyunglee1/feature/directives
pjmsullivan Jun 29, 2021
53598b2
Merge branch 'master' of https://github.com/oslabs-beta/obsidian
pjmsullivan Jun 29, 2021
c88e53b
Fixed Readme typo
pjmsullivan Jun 29, 2021
5f5808b
Fixed Readme typo
pjmsullivan Jun 29, 2021
6731962
Merge pull request #4 from pjmsullivan/master
cssim22 Jun 30, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .prettierrc

This file was deleted.

6 changes: 3 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# sudo: required
# services:
# - docker
# - docker
# before_install:
# - docker build -t open-source-labs/obsidian .
# - docker build -t open-source-labs/obsidian .
# script:
# - docker run open-source-labs/obsidian test --allow-net --allow-read --allow-env --unstable deno.test.ts
# - docker run open-source-labs/obsidian test --allow-net --allow-read --allow-env --unstable deno.test.ts
# env:
# global:
# 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=
5 changes: 5 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"deno.enable": true,
"deno.lint": true,
"deno.unstable": true
}
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
- GraphQL query abstraction and caching in SSR React projects, improving the performance of your app
- Normalized caching, optimizing memory management to keep your site lightweight and fast
- Fullstack integration, leveraging client-side and server-side caching to streamline your caching strategy
- Support for GraphQL fragments, directives, and variables
- Optional GraphQL DoS attack mitigation security module

## Overview

Expand Down Expand Up @@ -64,7 +66,7 @@ const PORT = 8000;
const app = new Application();

const types = (gql as any)`
// Type definitions
// GraphQL type definitions
`;

const resolvers = {
Expand Down Expand Up @@ -193,6 +195,11 @@ const MovieApp = () => {

_Lascaux_ Engineers

[Kyung Lee](https://github.com/kyunglee1)
[Justin McKay](https://github.com/justinwmckay)
[Patrick Sullivan](https://github.com/pjmsullivan)
[Cameron Simmons](https://github.com/cssim22)
[Raymond Ahn](https://github.com/raymondcodes)
[Alonso Garza](https://github.com/Alonsog66)
[Burak Caliskan](https://github.com/CaliskanBurak)
[Matt Meigs](https://github.com/mmeigs)
Expand Down
26 changes: 19 additions & 7 deletions src/CacheClassServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,22 @@ export class Cache {
}

// Main functionality methods
async read(queryStr) {
async read(queryStr, queryVars) {
if (typeof queryStr !== 'string')
throw TypeError('input should be a string');
// destructure the query string into an object
const queries = destructureQueries(queryStr).queries;
const queries = destructureQueries(queryStr, queryVars).queries;

// breaks out of function if queryStr is a mutation
if (!queries) return undefined;

const responseObject = {};
// iterate through each query in the input queries object
for (const query in queries) {
// get the entire str query from the name input query and arguments
const queryHash = queries[query].name.concat(queries[query].arguments);
const rootQuery = await this.cacheRead('ROOT_QUERY');

// match in ROOT_QUERY
if (rootQuery[queryHash]) {
// get the hashs to populate from the existent query in the cache
Expand All @@ -56,6 +59,7 @@ export class Cache {
arrayHashes,
queries[query].fields
);

if (!responseObject[respObjProp]) return undefined;

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

async write(queryStr, respObj, deleteFlag) {
const queryObj = destructureQueries(queryStr);
async write(queryStr, respObj, deleteFlag, queryVars) {
const queryObj = destructureQueries(queryStr, queryVars);
const resFromNormalize = normalizeResult(queryObj, respObj, deleteFlag);
// update the original cache with same reference
for (const hash in resFromNormalize) {
Expand All @@ -85,22 +89,26 @@ export class Cache {

// cache read/write helper methods
async cacheRead(hash) {
// returns value from either object cache or cache || 'DELETED' || undefined
// returns value from either object cache or cache || 'DELETED' || undefined
if (this.context === 'client') {
console.log('context === client HIT');
return this.storage[hash];
} else {
// logic to replace these storage keys if they have expired
if (hash === 'ROOT_QUERY' || hash === 'ROOT_MUTATION') {
const hasRootQuery = await redis.get('ROOT_QUERY');

if (!hasRootQuery) {
await redis.set('ROOT_QUERY', JSON.stringify({}));
}
const hasRootMutation = await redis.get('ROOT_MUTATION');

if (!hasRootMutation) {
await redis.set('ROOT_MUTATION', JSON.stringify({}));
}
}
let hashedQuery = await redis.get(hash);

// if cacheRead is a miss
if (hashedQuery === undefined) return undefined;
return JSON.parse(hashedQuery);
Expand Down Expand Up @@ -158,8 +166,10 @@ export class Cache {
async populateAllHashes(allHashesFromQuery, fields) {
// include the hashname for each hash
if (!allHashesFromQuery.length) return [];

const hyphenIdx = allHashesFromQuery[0].indexOf('~');
const typeName = allHashesFromQuery[0].slice(0, hyphenIdx);

return allHashesFromQuery.reduce(async (acc, hash) => {
// for each hash from the input query, build the response object
const readVal = await this.cacheRead(hash);
Expand All @@ -176,11 +186,13 @@ export class Cache {
// add the typename for the type
if (field === '__typename') {
dataObj[field] = typeName;
} else dataObj[field] = readVal[field];
} else {
dataObj[field] = readVal[field];
}
} else {
// case where the field from the input query is an array of hashes, recursively invoke populateAllHashes
dataObj[field] = await this.populateAllHashes(
readVal[field],
[readVal[field]],
fields[field]
);
if (dataObj[field] === undefined) return undefined;
Expand Down
59 changes: 59 additions & 0 deletions src/DoSSecurity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import destructureQueries from './destructure.js';

// Interface representing shape of query object after destructuring
interface queryObj {
queries?: Array<object>,
mutations?: Array<object>,
}

/**
* Tests whether a queryString (string representation of query) exceeds the maximum nested depth levels (queryDepthLimit) allowable for the instance of obsidian
* @param {*} queryString the string representation of the graphql query
* @param {*} queryDepthLimit number representation of the maximum query depth limit. Default 0 will return undefined. Root query doesn't count toward limit.
* @returns boolean indicating whether the query depth exceeded maximum allowed query depth
*/
export default function queryDepthLimiter(queryString: string, queryDepthLimit: number = 0): void {
const queryObj = destructureQueries(queryString) as queryObj;
/**
*Function that tests whether the query object debth exceeds maximum depth
* @param {*} qryObj an object representation of the query (after destructure)
* @param {*} qryDepthLim the maximum query depth
* @param {*} depth indicates current depth level
* @returns boolean indicating whether query depth exceeds maximum depth
*/
const queryDepthCheck = (qryObj: queryObj, qryDepthLim: number, depth: number = 0): boolean => {
// Base case 1: check to see if depth exceeds limit, if so, return error (true means depth limit was exceeded)
if (depth > qryDepthLim) return true;
// Recursive case: Iterate through values of queryObj, and check if each value is an object,
for (let value = 0; value < Object.values(qryObj).length; value++) {
// if the value is an object, return invokation queryDepthCheck on nested object and iterate depth
const currentValue = Object.values(qryObj)[value];
if (typeof currentValue === 'object') {
return queryDepthCheck(currentValue, qryDepthLim, depth + 1);
};
};
// Base case 2: reach end of object keys iteration,return false - depth has not been exceeded
return false;
};

// Check if queryObj has query or mutation root type, if so, call queryDepthCheck on each element, i.e. each query or mutation
if (queryObj.queries) {
for(let i = 0; i < queryObj.queries.length; i++) {
if(queryDepthCheck(queryObj.queries[i], queryDepthLimit)) {
throw new Error(
'Security Error: Query depth exceeded maximum query depth limit'
);
};
};
};

if (queryObj.mutations){
for (let i = 0; i < queryObj.mutations.length; i++) {
if (queryDepthCheck(queryObj.mutations[i], queryDepthLimit)) {
throw new Error(
'Security Error: Query depth exceeded maximum mutation depth limit'
);
};
};
};
}
Loading