Skip to content

Commit

Permalink
Merge pull request #54 from oslabs-beta/master
Browse files Browse the repository at this point in the history
Obsidian 3.1 - Added support for variables and directives and the ability to limit query depth
  • Loading branch information
pjmsullivan authored Jul 1, 2021
2 parents fe3bf6e + 6731962 commit 893f1a8
Show file tree
Hide file tree
Showing 16 changed files with 1,159 additions and 291 deletions.
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

0 comments on commit 893f1a8

Please sign in to comment.